add navbar & advisers page & compatibility & meditation
fix hydration error on /compatibility/[id]
This commit is contained in:
gofnnp 2025-06-26 18:23:35 +04:00
parent b1c8d4f910
commit 37841bb92a
52 changed files with 645 additions and 127 deletions

View File

@ -202,12 +202,12 @@
"title": "Your Personality Type",
"error": "Something went wrong. Please try again later."
},
"Breath": {
"Meditation": {
"title": "Stop and breathe to help you relax and focus on what really matters.",
"subtitle": "Breathing practice will help improve your aura. Breath in the positive energy, breathe out the negative...",
"button": "BEGIN"
},
"BreathResult": {
"MeditationResult": {
"breath_relax": "Breath & Relax",
"breath_in": "Breath in",
"breath_out": "Breath out"

View File

@ -0,0 +1,15 @@
import { Suspense } from "react";
import {
AdvisersSection,
AdvisersSectionSkeleton,
} from "@/components/domains/dashboard";
import { loadAssistants } from "@/entities/dashboard/loaders";
export default function Advisers() {
return (
<Suspense fallback={<AdvisersSectionSkeleton />}>
<AdvisersSection promise={loadAssistants()} gridDisplayMode="vertical" />
</Suspense>
);
}

View File

@ -0,0 +1,18 @@
import { Suspense } from "react";
import {
CompatibilitySection,
CompatibilitySectionSkeleton,
} from "@/components/domains/dashboard";
import { loadCompatibility } from "@/entities/dashboard/loaders";
export default function Compatibility() {
return (
<Suspense fallback={<CompatibilitySectionSkeleton />}>
<CompatibilitySection
promise={loadCompatibility()}
gridDisplayMode="vertical"
/>
</Suspense>
);
}

View File

@ -1,6 +1,6 @@
.main {
padding: 16px;
padding-bottom: 64px;
padding-bottom: 120px;
}
.navBar {

View File

@ -1,4 +1,5 @@
import { DrawerProvider, NavigationBar } from "@/components/layout";
import { DrawerProvider, Header } from "@/components/layout";
import NavigationBar from "@/components/layout/NavigationBar/NavigationBar";
import styles from "./layout.module.scss";
@ -9,8 +10,9 @@ export default function CoreLayout({
}>) {
return (
<DrawerProvider>
<NavigationBar className={styles.navBar} />
<Header className={styles.navBar} />
<main className={styles.main}>{children}</main>
<NavigationBar />
</DrawerProvider>
);
}

View File

@ -1,9 +1,9 @@
import { BreathPage } from "@/components/domains/breath";
import { MeditationPage } from "@/components/domains/meditation";
import { startGeneration } from "@/entities/generations/api";
import styles from "./page.module.scss";
export default async function Breath({
export default async function Meditation({
params,
}: {
params: Promise<{ id: string }>;
@ -17,7 +17,7 @@ export default async function Breath({
});
return (
<section className={styles.container}>
<BreathPage id={id} resultId={result?.id} />
<MeditationPage id={id} resultId={result?.id} />
</section>
);
}

View File

@ -1,9 +1,9 @@
import { BreathResultPage } from "@/components/domains/breath";
import { MeditationResultPage } from "@/components/domains/meditation";
import { loadPalms } from "@/entities/dashboard/loaders";
import styles from "./page.module.scss";
export default async function BreathResult({
export default async function MeditationResult({
params,
}: {
params: Promise<{ id: string; resultId: string }>;
@ -15,7 +15,7 @@ export default async function BreathResult({
return (
<section className={styles.container}>
<BreathResultPage id={resultId} action={action} />
<MeditationResultPage id={resultId} action={action} />
</section>
);
}

View File

@ -1,6 +1,6 @@
import { FullScreenModalProvider } from "@/providers/fullscreen-blur-modal-provider";
export default function BreathLayout({
export default function MeditationLayout({
children,
}: {
children: React.ReactNode;

View File

@ -0,0 +1,18 @@
import { Suspense } from "react";
import {
MeditationSection,
MeditationSectionSkeleton,
} from "@/components/domains/dashboard";
import { loadMeditations } from "@/entities/dashboard/loaders";
export default function Meditation() {
return (
<Suspense fallback={<MeditationSectionSkeleton />}>
<MeditationSection
promise={loadMeditations()}
gridDisplayMode="vertical"
/>
</Suspense>
);
}

View File

@ -1,3 +0,0 @@
export { default as BreathPage } from "./BreathPage/BreathPage";
export { default as BreathResultPage } from "./BreathResultPage/BreathResultPage";
export { default as StartBreathModalChild } from "./StartBreathModalChild/StartBreathModalChild";

View File

@ -1,7 +1,6 @@
import Image from "next/image";
import { Card, MetaLabel, Typography } from "@/components/ui";
import { IconName } from "@/components/ui/Icon/Icon";
import { Card, IconName, MetaLabel, Typography } from "@/components/ui";
import { Action } from "@/entities/dashboard/types";
import styles from "./CompatibilityCard.module.scss";

View File

@ -1,27 +1,41 @@
import Image from "next/image";
import clsx from "clsx";
import { Button, Card, Icon, MetaLabel, Typography } from "@/components/ui";
import { IconName } from "@/components/ui/Icon/Icon";
import {
Button,
Card,
Icon,
IconName,
MetaLabel,
Typography,
} from "@/components/ui";
import { Action } from "@/entities/dashboard/types";
import styles from "./MeditationCard.module.scss";
type MeditationCardProps = Action;
interface MeditationCardProps extends Action {
className?: string;
}
export default function MeditationCard({
imageUrl,
title,
type,
minutes,
className,
}: MeditationCardProps) {
return (
<Card className={styles.card}>
<Card className={clsx(styles.card, className)}>
<Image
className={styles.meditationImage}
src={imageUrl}
alt="Meditation image"
width={342}
height={216}
style={{
width: "auto",
height: "216px",
}}
/>
<div className={styles.content}>
<div className={styles.info}>

View File

@ -1,7 +1,6 @@
import Image from "next/image";
import { Card, MetaLabel, Typography } from "@/components/ui";
import { IconName } from "@/components/ui/Icon/Icon";
import { Card, IconName, MetaLabel, Typography } from "@/components/ui";
import { Action } from "@/entities/dashboard/types";
import styles from "./PalmCard.module.scss";

View File

@ -9,6 +9,10 @@
.grid {
padding-right: 16px;
&.vertical {
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)) !important;
}
}
.skeleton.skeleton {

View File

@ -1,4 +1,5 @@
import { use } from "react";
import clsx from "clsx";
import { Grid, Section, Skeleton } from "@/components/ui";
import { Assistant } from "@/entities/dashboard/types";
@ -7,17 +8,24 @@ import { AdviserCard } from "../../cards";
import styles from "./AdvisersSection.module.scss";
interface AdvisersSectionProps {
promise: Promise<Assistant[]>;
gridDisplayMode?: "vertical" | "horizontal";
}
export default function AdvisersSection({
promise,
}: {
promise: Promise<Assistant[]>;
}) {
gridDisplayMode = "horizontal",
}: AdvisersSectionProps) {
const assistants = use(promise);
const columns = Math.ceil(assistants?.length / 2);
return (
<Section title="Advisers" contentClassName={styles.sectionContent}>
<Grid columns={columns} className={styles.grid}>
<Grid
columns={columns}
className={clsx(styles.grid, styles[gridDisplayMode])}
>
{assistants.map(adviser => (
<AdviserCard key={adviser._id} {...adviser} />
))}

View File

@ -9,6 +9,10 @@
.grid {
padding-right: 16px;
&.vertical {
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)) !important;
}
}
.skeleton.skeleton {

View File

@ -2,6 +2,7 @@
import { use } from "react";
import Link from "next/link";
import clsx from "clsx";
import { Grid, Section, Skeleton } from "@/components/ui";
import { Action } from "@/entities/dashboard/types";
@ -11,17 +12,24 @@ import { CompatibilityCard } from "../../cards";
import styles from "./CompatibilitySection.module.scss";
interface CompatibilitySectionProps {
promise: Promise<Action[]>;
gridDisplayMode?: "vertical" | "horizontal";
}
export default function CompatibilitySection({
promise,
}: {
promise: Promise<Action[]>;
}) {
gridDisplayMode = "horizontal",
}: CompatibilitySectionProps) {
const compatibilities = use(promise);
const columns = Math.ceil(compatibilities?.length / 2);
return (
<Section title="Compatibility" contentClassName={styles.sectionContent}>
<Grid columns={columns} className={styles.grid}>
<Grid
columns={columns}
className={clsx(styles.grid, styles[gridDisplayMode])}
>
{compatibilities.map(compatibility => (
<Link
href={ROUTES.compatibility(compatibility._id)}

View File

@ -9,6 +9,14 @@
.grid {
padding-right: 16px;
&.vertical {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)) !important;
}
}
.cardVertical.cardVertical {
min-width: 250px;
}
.skeleton.skeleton {

View File

@ -1,5 +1,6 @@
import { use } from "react";
import Link from "next/link";
import clsx from "clsx";
import { Grid, Section, Skeleton } from "@/components/ui";
import { Action } from "@/entities/dashboard/types";
@ -9,20 +10,33 @@ import { MeditationCard } from "../../cards";
import styles from "./MeditationSection.module.scss";
interface MeditationSectionProps {
promise: Promise<Action[]>;
gridDisplayMode?: "vertical" | "horizontal";
}
export default function MeditationSection({
promise,
}: {
promise: Promise<Action[]>;
}) {
gridDisplayMode = "horizontal",
}: MeditationSectionProps) {
const meditations = use(promise);
const columns = meditations?.length;
return (
<Section title="Meditations" contentClassName={styles.sectionContent}>
<Grid columns={columns} className={styles.grid}>
<Grid
columns={columns}
className={clsx(styles.grid, styles[gridDisplayMode])}
>
{meditations.map(meditation => (
<Link href={ROUTES.breath(meditation._id)} key={meditation._id}>
<MeditationCard key={meditation._id} {...meditation} />
<Link href={ROUTES.meditation(meditation._id)} key={meditation._id}>
<MeditationCard
className={clsx(
gridDisplayMode === "vertical" && styles.cardVertical
)}
key={meditation._id}
{...meditation}
/>
</Link>
))}
</Grid>

View File

@ -8,17 +8,17 @@ import { ROUTES } from "@/shared/constants/client-routes";
import { StartBreathModalChild } from "..";
interface BreathPageProps {
interface MeditationPageProps {
id: string;
resultId: string;
}
export default function BreathPage({ id, resultId }: BreathPageProps) {
export default function MeditationPage({ id, resultId }: MeditationPageProps) {
const router = useRouter();
const { openModal, closeModal } = useFullScreenModal();
const handleBegin = useCallback(() => {
router.push(ROUTES.breathResult(id, resultId));
router.push(ROUTES.meditationResult(id, resultId));
closeModal();
}, [closeModal, id, resultId, router]);

View File

@ -9,18 +9,18 @@ import { Action } from "@/entities/dashboard/types";
import { useGenerationPolling } from "@/hooks/generation/useGenerationPolling";
import { useToast } from "@/providers/toast-provider";
import styles from "./BreathResultPage.module.scss";
import styles from "./MeditationResultPage.module.scss";
interface BreathResultPageProps {
interface MeditationResultPageProps {
id: string;
action: Action | undefined;
}
export default function BreathResultPage({
export default function MeditationResultPage({
id,
action,
}: BreathResultPageProps) {
const t = useTranslations("BreathResult");
}: MeditationResultPageProps) {
const t = useTranslations("MeditationResult");
const { data, error, isLoading } = useGenerationPolling(id);
const { addToast } = useToast();

View File

@ -9,7 +9,7 @@ interface IStartBreathModalChildProps {
}
function StartBreathModalChild({ handleBegin }: IStartBreathModalChildProps) {
const t = useTranslations("Breath");
const t = useTranslations("Meditation");
return (
<section className={styles.container}>

View File

@ -0,0 +1,3 @@
export { default as MeditationPage } from "./MeditationPage/MeditationPage";
export { default as MeditationResultPage } from "./MeditationResultPage/MeditationResultPage";
export { default as StartBreathModalChild } from "./StartBreathModalChild/StartBreathModalChild";

View File

@ -3,8 +3,7 @@
import Link from "next/link";
import clsx from "clsx";
import { Button, Icon, Typography } from "@/components/ui";
import { IconName } from "@/components/ui/Icon/Icon";
import { Button, Icon, IconName, Typography } from "@/components/ui";
import { ROUTES } from "@/shared/constants/client-routes";
import styles from "./Drawer.module.scss";

View File

@ -0,0 +1,29 @@
.header {
width: 100%;
min-height: 56px;
height: fit-content;
padding: 16px;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
}
.header > :first-child {
justify-self: start;
}
.header > :nth-child(2) {
justify-self: center;
}
.header > :nth-child(n + 3) {
justify-self: end;
display: inline-flex;
gap: 16px;
}
.menuButton.menuButton {
padding: 0;
width: fit-content;
background: none;
}

View File

@ -0,0 +1,36 @@
"use client";
import Link from "next/link";
import clsx from "clsx";
import { Button, Icon, IconName } from "@/components/ui";
import { ROUTES } from "@/shared/constants/client-routes";
import Logo from "../Logo/Logo";
import styles from "./Header.module.scss";
import { useDrawer } from "..";
interface HeaderProps {
className?: string;
}
export default function Header({ className }: HeaderProps) {
const { open } = useDrawer();
return (
<header className={clsx(styles.header, className)}>
<Button className={styles.menuButton} onClick={open}>
<Icon name={IconName.Menu} />
</Button>
<Link href={ROUTES.home()}>
<Logo />
</Link>
<div>
<Icon name={IconName.Notification} />
<Icon name={IconName.Search} />
</div>
</header>
);
}

View File

@ -1,29 +1,43 @@
.header {
width: 100%;
min-height: 56px;
height: fit-content;
padding: 16px;
display: grid;
grid-template-columns: 1fr auto 1fr;
.container {
width: calc(100% - 32px);
max-width: 400px;
position: fixed;
bottom: calc(0dvh + 14px);
left: 50%;
z-index: 9995;
transform: translateX(-50%);
display: flex;
align-items: center;
justify-content: space-around;
// gap: 40px;
background-color: #e5e7eb;
border-radius: 24px;
padding: 16px 24px 12px;
}
.header > :first-child {
justify-self: start;
}
.item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 5px;
position: relative;
.header > :nth-child(2) {
justify-self: center;
}
& .badge {
position: absolute;
bottom: 7px;
left: 17px;
}
.header > :nth-child(n + 3) {
justify-self: end;
display: inline-flex;
gap: 16px;
}
& > .label {
font-size: 11px;
line-height: 14px;
color: #8a8d93;
}
.menuButton.menuButton {
padding: 0;
width: fit-content;
background: none;
&.active {
& > .label {
color: #007aff;
}
}
}

View File

@ -1,37 +1,51 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useLocale } from "next-intl";
import clsx from "clsx";
import { Button, Icon } from "@/components/ui";
import { IconName } from "@/components/ui/Icon/Icon";
import { Badge, Icon, Typography } from "@/components/ui";
import { ROUTES } from "@/shared/constants/client-routes";
import Logo from "../Logo/Logo";
import { navItems } from "@/shared/constants/navigation";
import { stripLocale } from "@/shared/utils/path";
import styles from "./NavigationBar.module.scss";
import { useDrawer } from "..";
interface NavigationBarProps {
className?: string;
}
export default function NavigationBar({ className }: NavigationBarProps) {
const { open } = useDrawer();
export default function NavigationBar() {
const pathname = usePathname();
const locale = useLocale();
const pathnameWithoutLocale = stripLocale(pathname, locale);
return (
<header className={clsx(styles.header, className)}>
<Button className={styles.menuButton} onClick={open}>
<Icon name={IconName.Menu} />
</Button>
<Link href={ROUTES.home()}>
<Logo />
</Link>
<div>
<Icon name={IconName.Notification} />
<Icon name={IconName.Search} />
</div>
</header>
<nav className={styles.container}>
{navItems.map(item => {
const isActive =
item.href === ROUTES.home()
? pathnameWithoutLocale === item.href
: pathnameWithoutLocale.startsWith(item.href);
return (
<Link
key={item.key}
href={item.href}
className={clsx(styles.item, { [styles.active]: isActive })}
>
<Icon name={item.icon} color={isActive ? "#007AFF" : "#8A8D93"}>
{item.badge && (
<Badge className={styles.badge}>
<Typography weight="medium" size="xs" color="white">
{item.badge}
</Typography>
</Badge>
)}
</Icon>
<Typography weight="medium" className={styles.label}>
{item.label}
</Typography>
</Link>
);
})}
</nav>
);
}

View File

@ -1,4 +1,4 @@
export { DrawerProvider, useDrawer } from "./Drawer/DrawerContext";
export { default as Header } from "./Header/Header";
export { default as Logo } from "./Logo/Logo";
export { default as NavigationBar } from "./NavigationBar/NavigationBar";
export { default as StepperBar } from "./StepperBar/StepperBar";

View File

@ -0,0 +1,11 @@
.badge {
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
min-width: fit-content;
min-height: fit-content;
aspect-ratio: 1/1;
border-radius: 50%;
background-color: #ff0028;
}

View File

@ -0,0 +1,12 @@
import clsx from "clsx";
import styles from "./Badge.module.scss";
interface BadgeProps {
children: React.ReactNode;
className?: string;
}
export default function Badge({ children, className }: BadgeProps) {
return <div className={clsx(styles.badge, className)}>{children}</div>;
}

View File

@ -1,14 +1,21 @@
import { CSSProperties, ReactNode } from "react";
import clsx from "clsx";
import ArticleIcon from "./icons/Article";
import ChevronIcon from "./icons/Chevron";
import CrossIcon from "./icons/Cross";
import MenuIcon from "./icons/Menu";
import NotificationIcon from "./icons/Notification";
import SearchIcon from "./icons/Search";
import StarIcon from "./icons/Star";
import VideoIcon from "./icons/Video";
import {
ArticleIcon,
ChatIcon,
ChevronIcon,
ClipboardIcon,
CrossIcon,
HeartIcon,
HomeIcon,
LeafIcon,
MenuIcon,
NotificationIcon,
SearchIcon,
StarIcon,
VideoIcon,
} from "./icons";
export enum IconName {
Notification,
@ -19,6 +26,11 @@ export enum IconName {
Chevron,
Star,
Cross,
Home,
Chat,
Clipboard,
Heart,
Leaf,
}
const icons: Record<
@ -33,6 +45,11 @@ const icons: Record<
[IconName.Chevron]: ChevronIcon,
[IconName.Star]: StarIcon,
[IconName.Cross]: CrossIcon,
[IconName.Home]: HomeIcon,
[IconName.Chat]: ChatIcon,
[IconName.Clipboard]: ClipboardIcon,
[IconName.Heart]: HeartIcon,
[IconName.Leaf]: LeafIcon,
};
export type IconProps = {

View File

@ -0,0 +1,37 @@
import { SVGProps } from "react";
export default function ChatIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
color={props.color !== "currentColor" ? props.color : "#8A8D93"}
>
<g clipPath="url(#clip0_2188_2062)">
<g clipPath="url(#clip1_2188_2062)">
<path
d="M24.7969 11.25C24.7969 16.6359 19.425 21 12.7969 21C11.0579 21 9.40786 20.7 7.91723 20.1609C7.35942 20.5688 6.45005 21.1266 5.37192 21.5953C4.24692 22.0828 2.89223 22.5 1.54692 22.5C1.24223 22.5 0.97036 22.3172 0.853172 22.0359C0.735985 21.7547 0.80161 21.4359 1.01255 21.2203L1.02661 21.2062C1.04067 21.1922 1.05942 21.1734 1.08755 21.1406C1.13911 21.0844 1.2188 20.9953 1.31723 20.8734C1.50942 20.6391 1.76723 20.2922 2.02973 19.8609C2.49848 19.0828 2.9438 18.0609 3.03286 16.9125C1.62661 15.3187 0.796922 13.3641 0.796922 11.25C0.796922 5.86406 6.1688 1.5 12.7969 1.5C19.425 1.5 24.7969 5.86406 24.7969 11.25Z"
fill="currentColor"
/>
</g>
</g>
<defs>
<clipPath id="clip0_2188_2062">
<rect
width="24"
height="24"
fill="white"
transform="translate(0.796875)"
/>
</clipPath>
<clipPath id="clip1_2188_2062">
<path d="M0.796875 0H24.7969V24H0.796875V0Z" fill="white" />
</clipPath>
</defs>
</svg>
);
}

View File

@ -0,0 +1,47 @@
import { SVGProps } from "react";
export default function ClipboardIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
width="19"
height="24"
viewBox="0 0 19 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
color={props.color !== "currentColor" ? props.color : "#8A8D93"}
>
<g clipPath="url(#clip0_2188_2069)">
<g clipPath="url(#clip1_2188_2069)">
<g clipPath="url(#clip2_2188_2069)">
<path
d="M9.78125 0C7.82188 0 6.15312 1.25156 5.53906 3H3.78125C2.12656 3 0.78125 4.34531 0.78125 6V21C0.78125 22.6547 2.12656 24 3.78125 24H15.7812C17.4359 24 18.7812 22.6547 18.7812 21V6C18.7812 4.34531 17.4359 3 15.7812 3H14.0234C13.4094 1.25156 11.7406 0 9.78125 0ZM9.78125 3C10.1791 3 10.5606 3.15804 10.8419 3.43934C11.1232 3.72064 11.2812 4.10218 11.2812 4.5C11.2812 4.89782 11.1232 5.27936 10.8419 5.56066C10.5606 5.84196 10.1791 6 9.78125 6C9.38343 6 9.00189 5.84196 8.72059 5.56066C8.43929 5.27936 8.28125 4.89782 8.28125 4.5C8.28125 4.10218 8.43929 3.72064 8.72059 3.43934C9.00189 3.15804 9.38343 3 9.78125 3ZM6.03125 9H13.5312C13.9438 9 14.2812 9.3375 14.2812 9.75C14.2812 10.1625 13.9438 10.5 13.5312 10.5H6.03125C5.61875 10.5 5.28125 10.1625 5.28125 9.75C5.28125 9.3375 5.61875 9 6.03125 9Z"
fill="currentColor"
/>
</g>
</g>
</g>
<defs>
<clipPath id="clip0_2188_2069">
<rect
width="18"
height="24"
fill="white"
transform="translate(0.78125)"
/>
</clipPath>
<clipPath id="clip1_2188_2069">
<rect
width="18"
height="24"
fill="white"
transform="translate(0.78125)"
/>
</clipPath>
<clipPath id="clip2_2188_2069">
<path d="M0.78125 0H18.7812V24H0.78125V0Z" fill="white" />
</clipPath>
</defs>
</svg>
);
}

View File

@ -0,0 +1,36 @@
import { SVGProps } from "react";
export default function HeartIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
color={props.color !== "currentColor" ? props.color : "#8A8D93"}
>
<g clipPath="url(#clip0_2188_2075)">
<g clipPath="url(#clip1_2188_2075)">
<path
d="M2.68437 14.081L11.1547 21.9888C11.5063 22.317 11.9703 22.4998 12.4531 22.4998C12.9359 22.4998 13.4 22.317 13.7516 21.9888L22.2219 14.081C23.6469 12.7545 24.4531 10.8935 24.4531 8.9482V8.67633C24.4531 5.39976 22.0859 2.60601 18.8563 2.06695C16.7188 1.7107 14.5437 2.40914 13.0156 3.93726L12.4531 4.49976L11.8906 3.93726C10.3625 2.40914 8.1875 1.7107 6.05 2.06695C2.82031 2.60601 0.453125 5.39976 0.453125 8.67633V8.9482C0.453125 10.8935 1.25937 12.7545 2.68437 14.081Z"
fill="currentColor"
/>
</g>
</g>
<defs>
<clipPath id="clip0_2188_2075">
<rect
width="24"
height="24"
fill="white"
transform="translate(0.453125)"
/>
</clipPath>
<clipPath id="clip1_2188_2075">
<path d="M0.453125 0H24.4531V24H0.453125V0Z" fill="white" />
</clipPath>
</defs>
</svg>
);
}

View File

@ -0,0 +1,37 @@
import { SVGProps } from "react";
export default function HomeIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
width="28"
height="24"
viewBox="0 0 28 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
color={props.color !== "currentColor" ? props.color : "#8A8D93"}
>
<g clipPath="url(#clip0_2188_2057)">
<g clipPath="url(#clip1_2188_2057)">
<path
d="M27.2875 11.9766C27.2875 12.8203 26.5844 13.4813 25.7875 13.4813H24.2875L24.3203 20.9906C24.3203 21.1172 24.3109 21.2437 24.2969 21.3703V22.125C24.2969 23.1609 23.4578 24 22.4219 24H21.6719C21.6203 24 21.5688 24 21.5172 23.9953C21.4516 24 21.3859 24 21.3203 24H19.7969H18.6719C17.6359 24 16.7969 23.1609 16.7969 22.125V21V18C16.7969 17.1703 16.1266 16.5 15.2969 16.5H12.2969C11.4672 16.5 10.7969 17.1703 10.7969 18V21V22.125C10.7969 23.1609 9.95781 24 8.92188 24H7.79688H6.30156C6.23125 24 6.16094 23.9953 6.09062 23.9906C6.03437 23.9953 5.97813 24 5.92188 24H5.17188C4.13594 24 3.29688 23.1609 3.29688 22.125V16.875C3.29688 16.8328 3.29688 16.7859 3.30156 16.7438V13.4813H1.79688C0.953125 13.4813 0.296875 12.825 0.296875 11.9766C0.296875 11.5547 0.4375 11.1797 0.765625 10.8516L12.7844 0.375C13.1125 0.046875 13.4875 0 13.8156 0C14.1437 0 14.5187 0.09375 14.8 0.328125L26.7719 10.8516C27.1469 11.1797 27.3344 11.5547 27.2875 11.9766Z"
fill="currentColor"
/>
</g>
</g>
<defs>
<clipPath id="clip0_2188_2057">
<rect
width="27"
height="24"
fill="white"
transform="translate(0.296875)"
/>
</clipPath>
<clipPath id="clip1_2188_2057">
<path d="M0.296875 0H27.2969V24H0.296875V0Z" fill="white" />
</clipPath>
</defs>
</svg>
);
}

View File

@ -0,0 +1,36 @@
import { SVGProps } from "react";
export default function Leaf(props: SVGProps<SVGSVGElement>) {
return (
<svg
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
color={props.color !== "currentColor" ? props.color : "#8A8D93"}
>
<g clipPath="url(#clip0_2188_2080)">
<g clipPath="url(#clip1_2188_2080)">
<path
d="M12.8594 4.50011C9.175 4.50011 6.05781 6.91417 4.99844 10.2423C6.57344 9.44542 8.35 9.00011 10.2344 9.00011H14.3594C14.7719 9.00011 15.1094 9.33761 15.1094 9.75011C15.1094 10.1626 14.7719 10.5001 14.3594 10.5001H13.6094H10.2344C9.45625 10.5001 8.70156 10.5892 7.975 10.7532C6.76094 11.0298 5.63125 11.522 4.62813 12.1923C1.90469 14.0064 0.109375 17.1048 0.109375 20.6251V21.3751C0.109375 21.9985 0.610937 22.5001 1.23438 22.5001C1.85781 22.5001 2.35938 21.9985 2.35938 21.3751V20.6251C2.35938 18.3423 3.32969 16.2892 4.88125 14.8501C5.80938 18.3892 9.02969 21.0001 12.8594 21.0001H12.9062C19.0984 20.9673 24.1094 14.8642 24.1094 7.34074C24.1094 5.34386 23.7578 3.44542 23.1203 1.73449C22.9984 1.41105 22.525 1.42511 22.3609 1.7298C21.4797 3.3798 19.7359 4.50011 17.7344 4.50011H12.8594Z"
fill="currentColor"
/>
</g>
</g>
<defs>
<clipPath id="clip0_2188_2080">
<rect
width="24"
height="24"
fill="white"
transform="translate(0.109375)"
/>
</clipPath>
<clipPath id="clip1_2188_2080">
<path d="M0.109375 0H24.1094V24H0.109375V0Z" fill="white" />
</clipPath>
</defs>
</svg>
);
}

View File

@ -0,0 +1,13 @@
export { default as ArticleIcon } from "./Article";
export { default as ChatIcon } from "./Chat";
export { default as ChevronIcon } from "./Chevron";
export { default as ClipboardIcon } from "./Clipboard";
export { default as CrossIcon } from "./Cross";
export { default as HeartIcon } from "./Heart";
export { default as HomeIcon } from "./Home";
export { default as LeafIcon } from "./Leaf";
export { default as MenuIcon } from "./Menu";
export { default as NotificationIcon } from "./Notification";
export { default as SearchIcon } from "./Search";
export { default as StarIcon } from "./Star";
export { default as VideoIcon } from "./Video";

View File

@ -1,10 +1,10 @@
import { ReactNode } from "react";
import clsx from "clsx";
import Icon, { IconProps } from "../Icon/Icon";
import styles from "./IconLabel.module.scss";
import { Icon, IconProps } from "..";
export type IconLabelProps = {
iconProps: IconProps;
children: ReactNode;

View File

@ -1,11 +1,12 @@
import { ReactNode } from "react";
import IconLabel, { IconLabelProps } from "../IconLabel/IconLabel";
import Typography from "../Typography/Typography";
import styles from "./MetaLabel.module.scss";
export type MetaLabelProps = {
import { IconLabel, IconLabelProps } from "..";
type MetaLabelProps = {
iconLabelProps: IconLabelProps;
children: ReactNode;
};

View File

@ -3,11 +3,9 @@
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
import clsx from "clsx";
import { IconName } from "../Icon/Icon";
import styles from "./Modal.module.scss";
import { Button, Icon } from "..";
import { Button, Icon, IconName } from "..";
interface ModalProps {
children: ReactNode;

View File

@ -1,9 +1,9 @@
import clsx from "clsx";
import Icon, { IconName } from "../Icon/Icon";
import styles from "./Stars.module.scss";
import { Icon, IconName } from "..";
interface StarsProps {
rating?: number;
size?: number;

View File

@ -1,3 +1,4 @@
export { default as Badge } from "./Badge/Badge";
export { default as Button } from "./Button/Button";
export { default as Card } from "./Card/Card";
export { default as CircleArrow } from "./CircleArrow/CircleArrow";
@ -5,8 +6,11 @@ export { default as EmailInput } from "./EmailInput/EmailInput";
export { default as FullScreenBlurModal } from "./FullScreenBlurModal/FullScreenBlurModal";
export { default as GPTAnimationText } from "./GPTAnimationText/GPTAnimationText";
export { default as Grid } from "./Grid/Grid";
export { default as Icon } from "./Icon/Icon";
export { default as IconLabel } from "./IconLabel/IconLabel";
export { default as Icon, IconName, type IconProps } from "./Icon/Icon";
export {
default as IconLabel,
type IconLabelProps,
} from "./IconLabel/IconLabel";
export { default as MetaLabel } from "./MetaLabel/MetaLabel";
export { default as Modal } from "./Modal/Modal";
export { default as NameInput } from "./NameInput/NameInput";

View File

@ -1,7 +1,7 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { useTranslations } from "next-intl";
import { useLocale, useTranslations } from "next-intl";
import { Typography } from "@/components/ui";
import { SelectInput } from "@/components/ui/SelectInput/SelectInput";
@ -20,9 +20,7 @@ const isValidDate = (year: number, month: number, day: number) => {
// Упрощенное определение порядка полей даты на основе локали.
// В реальном приложении здесь лучше использовать данные из next-intl.
const getDateInputLocaleFormat = (): ("d" | "m" | "y")[] => {
const locale =
typeof navigator !== "undefined" ? navigator.language : "en-US";
const getDateInputLocaleFormat = (locale: string): ("d" | "m" | "y")[] => {
const format = new Intl.DateTimeFormat(locale).format(new Date(2001, 1, 3)); // Используем 3/Feb/2001
if (/^3.*2/.test(format)) return ["d", "m", "y"]; // 3/2/2001 -> d/m/y
if (/^2.*3/.test(format)) return ["m", "d", "y"]; // 2/3/2001 -> m/d/y
@ -49,6 +47,8 @@ export default function DatePicker({
onBlur,
}: DatePickerProps) {
const t = useTranslations("DatePicker");
const locale = useLocale();
const [year, setYear] = useState("");
const [month, setMonth] = useState("");
const [day, setDay] = useState("");
@ -110,7 +110,10 @@ export default function DatePicker({
}));
}, [year, month]);
const localeFormat = useMemo(() => getDateInputLocaleFormat(), []);
const localeFormat = useMemo(
() => getDateInputLocaleFormat(locale),
[locale]
);
const inputs = {
d: (

View File

@ -39,7 +39,7 @@ const intlMiddleware = createMiddleware(routing);
export default async function middleware(request: NextRequest) {
const authResponse = await createAuthMiddleware()(request);
if (authResponse.status !== 200) {
if (authResponse.status !== 200 && process.env.NODE_ENV === "production") {
return authResponse;
}

View File

@ -3,8 +3,8 @@ const ROOT_ROUTE = "/";
const profilePrefix = "profile";
const retainingFunnelPrefix = "retaining";
const createRoute = (segments: string[]): string => {
return ROOT_ROUTE + segments.join("/");
const createRoute = (segments: Array<string | undefined>): string => {
return ROOT_ROUTE + segments.filter(Boolean).join("/");
};
export const ROUTES = {
@ -13,13 +13,16 @@ export const ROUTES = {
// Auth
authCallback: () => createRoute(["auth", "callback"]),
// Breath
breath: (id: string) => createRoute(["breath", id]),
breathResult: (id: string, resultId: string) =>
createRoute(["breath", id, "result", resultId]),
// Advisers
advisers: () => createRoute(["advisers"]),
// Meditation
meditation: (id?: string) => createRoute(["meditation", id]),
meditationResult: (id: string, resultId: string) =>
createRoute(["meditation", id, "result", resultId]),
// Compatibility
compatibility: (id: string) => createRoute(["compatibility", id]),
compatibility: (id?: string) => createRoute(["compatibility", id]),
compatibilityResult: (id: string, resultId: string) =>
createRoute(["compatibility", id, "result", resultId]),
@ -56,4 +59,13 @@ export const ROUTES = {
payment: () => createRoute(["payment"]),
paymentSuccess: () => createRoute(["payment", "success"]),
paymentFailed: () => createRoute(["payment", "failed"]),
// Chat
chat: () => createRoute(["chat"]),
// // Compatibility
// compatibilities: () => createRoute(["compatibilities"]),
// // Meditation
// meditations: () => createRoute(["meditations"]),
};

View File

@ -0,0 +1,45 @@
import { IconName } from "@/components/ui";
import { ROUTES } from "./client-routes";
interface NavItem {
key: string;
label: string;
icon: IconName;
href: string;
badge?: number;
}
export const navItems: NavItem[] = [
{
key: "home",
label: "Home",
icon: IconName.Home,
href: ROUTES.home(),
},
{
key: "chat",
label: "Chat",
icon: IconName.Chat,
href: ROUTES.chat(),
badge: 12,
},
{
key: "advisers",
label: "Advi...",
icon: IconName.Clipboard,
href: ROUTES.advisers(),
},
{
key: "compatibility",
label: "Comp...",
icon: IconName.Heart,
href: ROUTES.compatibility(),
},
{
key: "meditation",
label: "Medi...",
icon: IconName.Leaf,
href: ROUTES.meditation(),
},
];

6
src/shared/utils/path.ts Normal file
View File

@ -0,0 +1,6 @@
export function stripLocale(pathname: string, locale: string) {
if (pathname.startsWith(`/${locale}`)) {
return pathname.slice(locale.length + 1) || "/";
}
return pathname;
}