main
add api
This commit is contained in:
parent
db1dd03f41
commit
342af0c71b
8
.prettierignore
Normal file
8
.prettierignore
Normal file
@ -0,0 +1,8 @@
|
||||
# Ignore artifacts:
|
||||
build
|
||||
.next/
|
||||
dist
|
||||
node_modules/
|
||||
|
||||
# Ignore files:
|
||||
package-lock.json
|
||||
7
.prettierrc.json
Normal file
7
.prettierrc.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@ -1,7 +1,18 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
env: {
|
||||
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "aura-node.s3.eu-west-2.amazonaws.com",
|
||||
pathname: "/**",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
17
package-lock.json
generated
17
package-lock.json
generated
@ -21,6 +21,7 @@
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.3",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5"
|
||||
}
|
||||
},
|
||||
@ -4626,6 +4627,22 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
|
||||
@ -13,7 +13,8 @@
|
||||
"next": "15.3.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"sass": "^1.89.2"
|
||||
"sass": "^1.89.2",
|
||||
"zod": "^3.25.64"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
@ -22,6 +23,7 @@
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.3.3",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
6
src/app/(core)/error.module.scss
Normal file
6
src/app/(core)/error.module.scss
Normal file
@ -0,0 +1,6 @@
|
||||
.coreError {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
32
src/app/(core)/error.tsx
Normal file
32
src/app/(core)/error.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/components/ui/Button/Button';
|
||||
import Typography from '@/components/ui/Typography/Typography';
|
||||
import { useEffect } from 'react';
|
||||
import styles from "./error.module.scss"
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<div className={styles.coreError}>
|
||||
<Typography as='h2' size='xl' weight='bold'>Something went wrong!</Typography>
|
||||
<Typography as='p' align='center'>{error.message}</Typography>
|
||||
<Button
|
||||
onClick={
|
||||
() => reset()
|
||||
}
|
||||
>
|
||||
<Typography color='white'>Try again</Typography>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
src/app/(core)/loading.module.scss
Normal file
5
src/app/(core)/loading.module.scss
Normal file
@ -0,0 +1,5 @@
|
||||
.coreSpinnerContainer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
10
src/app/(core)/loading.tsx
Normal file
10
src/app/(core)/loading.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import Spinner from "@/components/ui/Spinner/Spinner";
|
||||
import styles from "./loading.module.scss"
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className={styles.coreSpinnerContainer}>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,16 +1,32 @@
|
||||
import { Suspense } from "react";
|
||||
import Horoscope from "@/components/Horoscope/Horoscope";
|
||||
import styles from "./page.module.scss"
|
||||
import AdvisersSection from "@/components/AdvisersSection/AdvisersSection";
|
||||
import CompatibilitySection from "@/components/CompatibilitySection/CompatibilitySection";
|
||||
import MeditationSection from "@/components/MeditationSection/MeditationSection";
|
||||
import PalmSection from "@/components/PalmSection/PalmSection";
|
||||
import styles from "./page.module.scss";
|
||||
import AdvisersSection, { AdvisersSectionSkeleton } from "@/components/AdvisersSection/AdvisersSection";
|
||||
import CompatibilitySection, { CompatibilitySectionSkeleton } from "@/components/CompatibilitySection/CompatibilitySection";
|
||||
import MeditationSection, { MeditationSectionSkeleton } from "@/components/MeditationSection/MeditationSection";
|
||||
import PalmSection, { PalmSectionSkeleton } from "@/components/PalmSection/PalmSection";
|
||||
import { loadAssistants, loadCompatibility, loadMeditations, loadPalms } from "@/entities/dashboard/loaders";
|
||||
|
||||
export default function Home() {
|
||||
return <section className={styles.page}>
|
||||
<Horoscope />
|
||||
<AdvisersSection />
|
||||
<CompatibilitySection />
|
||||
<MeditationSection />
|
||||
<PalmSection />
|
||||
</section>;
|
||||
return (
|
||||
<section className={styles.page}>
|
||||
<Horoscope />
|
||||
|
||||
<Suspense fallback={<AdvisersSectionSkeleton />}>
|
||||
<AdvisersSection promise={loadAssistants()} />
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={<CompatibilitySectionSkeleton />}>
|
||||
<CompatibilitySection promise={loadCompatibility()} />
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={<MeditationSectionSkeleton />}>
|
||||
<MeditationSection promise={loadMeditations()} />
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={<PalmSectionSkeleton />}>
|
||||
<PalmSection promise={loadPalms()} />
|
||||
</Suspense>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -8,6 +8,8 @@
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
@ -46,12 +48,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&>.stars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,44 +3,38 @@ import Button from "../ui/Button/Button"
|
||||
import Card from "../ui/Card/Card"
|
||||
import Typography from "../ui/Typography/Typography"
|
||||
import styles from "./AdviserCard.module.scss"
|
||||
import { Assistant } from "@/entities/dashboard/types"
|
||||
import Stars from "../ui/Stars/Stars"
|
||||
|
||||
export default function AdviserCard() {
|
||||
type AdviserCardProps = Assistant;
|
||||
|
||||
export default function AdviserCard({
|
||||
name,
|
||||
photoUrl,
|
||||
rating,
|
||||
reviewCount,
|
||||
description
|
||||
}: AdviserCardProps) {
|
||||
return (
|
||||
<Card className={clsx(styles.card, styles.adviserCard)} style={{ backgroundImage: `url(/adviser-card.png)` }}>
|
||||
<Card className={clsx(styles.card, styles.adviserCard)} style={{ backgroundImage: `url(${photoUrl})` }}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.name}>
|
||||
<Typography color="white" weight="bold">
|
||||
Marta
|
||||
{name}
|
||||
</Typography>
|
||||
<div className={styles.indicator} />
|
||||
</div>
|
||||
<Typography className={styles.description} color="white" weight="medium" size="xs">
|
||||
Astrologer - 7 years
|
||||
{description}
|
||||
</Typography>
|
||||
<div className={styles.rating}>
|
||||
<Typography color="white" weight="medium" size="xs">
|
||||
4.8
|
||||
{rating}
|
||||
</Typography>
|
||||
<div className={styles.stars}>
|
||||
<svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.57382 0.781219C4.49102 0.609363 4.31603 0.5 4.12387 0.5C3.9317 0.5 3.75828 0.609363 3.67392 0.781219L2.66934 2.84817L0.425835 3.17939C0.238356 3.20751 0.082123 3.33874 0.0243168 3.51841C-0.0334894 3.69808 0.0133805 3.8965 0.147741 4.02929L1.77569 5.64005L1.39135 7.91636C1.36011 8.10384 1.43822 8.29444 1.5929 8.40537C1.74757 8.51629 1.95223 8.53035 2.12096 8.4413L4.12543 7.37111L6.1299 8.4413C6.29863 8.53035 6.5033 8.51785 6.65797 8.40537C6.81264 8.29288 6.89075 8.10384 6.85951 7.91636L6.47361 5.64005L8.10156 4.02929C8.23592 3.8965 8.28435 3.69808 8.22498 3.51841C8.16561 3.33874 8.01094 3.20751 7.82346 3.17939L5.5784 2.84817L4.57382 0.781219Z" fill="#FFD600" />
|
||||
</svg>
|
||||
<svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.57382 0.781219C4.49102 0.609363 4.31603 0.5 4.12387 0.5C3.9317 0.5 3.75828 0.609363 3.67392 0.781219L2.66934 2.84817L0.425835 3.17939C0.238356 3.20751 0.082123 3.33874 0.0243168 3.51841C-0.0334894 3.69808 0.0133805 3.8965 0.147741 4.02929L1.77569 5.64005L1.39135 7.91636C1.36011 8.10384 1.43822 8.29444 1.5929 8.40537C1.74757 8.51629 1.95223 8.53035 2.12096 8.4413L4.12543 7.37111L6.1299 8.4413C6.29863 8.53035 6.5033 8.51785 6.65797 8.40537C6.81264 8.29288 6.89075 8.10384 6.85951 7.91636L6.47361 5.64005L8.10156 4.02929C8.23592 3.8965 8.28435 3.69808 8.22498 3.51841C8.16561 3.33874 8.01094 3.20751 7.82346 3.17939L5.5784 2.84817L4.57382 0.781219Z" fill="#FFD600" />
|
||||
</svg>
|
||||
<svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.57382 0.781219C4.49102 0.609363 4.31603 0.5 4.12387 0.5C3.9317 0.5 3.75828 0.609363 3.67392 0.781219L2.66934 2.84817L0.425835 3.17939C0.238356 3.20751 0.082123 3.33874 0.0243168 3.51841C-0.0334894 3.69808 0.0133805 3.8965 0.147741 4.02929L1.77569 5.64005L1.39135 7.91636C1.36011 8.10384 1.43822 8.29444 1.5929 8.40537C1.74757 8.51629 1.95223 8.53035 2.12096 8.4413L4.12543 7.37111L6.1299 8.4413C6.29863 8.53035 6.5033 8.51785 6.65797 8.40537C6.81264 8.29288 6.89075 8.10384 6.85951 7.91636L6.47361 5.64005L8.10156 4.02929C8.23592 3.8965 8.28435 3.69808 8.22498 3.51841C8.16561 3.33874 8.01094 3.20751 7.82346 3.17939L5.5784 2.84817L4.57382 0.781219Z" fill="#FFD600" />
|
||||
</svg>
|
||||
<svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.57382 0.781219C4.49102 0.609363 4.31603 0.5 4.12387 0.5C3.9317 0.5 3.75828 0.609363 3.67392 0.781219L2.66934 2.84817L0.425835 3.17939C0.238356 3.20751 0.082123 3.33874 0.0243168 3.51841C-0.0334894 3.69808 0.0133805 3.8965 0.147741 4.02929L1.77569 5.64005L1.39135 7.91636C1.36011 8.10384 1.43822 8.29444 1.5929 8.40537C1.74757 8.51629 1.95223 8.53035 2.12096 8.4413L4.12543 7.37111L6.1299 8.4413C6.29863 8.53035 6.5033 8.51785 6.65797 8.40537C6.81264 8.29288 6.89075 8.10384 6.85951 7.91636L6.47361 5.64005L8.10156 4.02929C8.23592 3.8965 8.28435 3.69808 8.22498 3.51841C8.16561 3.33874 8.01094 3.20751 7.82346 3.17939L5.5784 2.84817L4.57382 0.781219Z" fill="#FFD600" />
|
||||
</svg>
|
||||
<svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.57382 0.781219C4.49102 0.609363 4.31603 0.5 4.12387 0.5C3.9317 0.5 3.75828 0.609363 3.67392 0.781219L2.66934 2.84817L0.425835 3.17939C0.238356 3.20751 0.082123 3.33874 0.0243168 3.51841C-0.0334894 3.69808 0.0133805 3.8965 0.147741 4.02929L1.77569 5.64005L1.39135 7.91636C1.36011 8.10384 1.43822 8.29444 1.5929 8.40537C1.74757 8.51629 1.95223 8.53035 2.12096 8.4413L4.12543 7.37111L6.1299 8.4413C6.29863 8.53035 6.5033 8.51785 6.65797 8.40537C6.81264 8.29288 6.89075 8.10384 6.85951 7.91636L6.47361 5.64005L8.10156 4.02929C8.23592 3.8965 8.28435 3.69808 8.22498 3.51841C8.16561 3.33874 8.01094 3.20751 7.82346 3.17939L5.5784 2.84817L4.57382 0.781219Z" fill="#FFD600" />
|
||||
</svg>
|
||||
</div>
|
||||
<Stars rating={rating} />
|
||||
<Typography color="white" weight="medium" size="xs">
|
||||
(5762)
|
||||
({reviewCount})
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -9,4 +9,8 @@
|
||||
|
||||
.grid {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.advisersSkeleton.skeleton {
|
||||
height: 486px;
|
||||
}
|
||||
@ -1,29 +1,31 @@
|
||||
import { Assistant } from "@/entities/dashboard/types";
|
||||
import AdviserCard from "../AdviserCard/AdviserCard";
|
||||
import Grid from "../ui/Grid/Grid"
|
||||
import Section from "../ui/Section/Section"
|
||||
import styles from "./AdvisersSection.module.scss"
|
||||
import { use } from "react";
|
||||
import Skeleton from "../ui/Skeleton/Skeleton";
|
||||
import clsx from "clsx";
|
||||
|
||||
const advisers = [
|
||||
{ name: "Marta", img: "/marta.jpg", rating: 4.8, years: 7 },
|
||||
{ name: "Marta", img: "/marta.jpg", rating: 4.8, years: 7 },
|
||||
{ name: "Marta", img: "/marta.jpg", rating: 4.8, years: 7 },
|
||||
{ name: "Marta", img: "/marta.jpg", rating: 4.8, years: 7 },
|
||||
{ name: "Marta", img: "/marta.jpg", rating: 4.8, years: 7 },
|
||||
{ name: "Marta", img: "/marta.jpg", rating: 4.8, years: 7 },
|
||||
{ name: "Marta", img: "/marta.jpg", rating: 4.8, years: 7 },
|
||||
{ name: "Marta", img: "/marta.jpg", rating: 4.8, years: 7 },
|
||||
{ name: "Marta", img: "/marta.jpg", rating: 4.8, years: 7 },
|
||||
{ name: "Marta", img: "/marta.jpg", rating: 4.8, years: 7 },
|
||||
];
|
||||
export default function AdvisersSection({ promise }: { promise: Promise<Assistant[]> }) {
|
||||
const assistants = use(promise);
|
||||
const columns = Math.ceil(assistants?.length / 2);
|
||||
|
||||
export default function AdvisersSection() {
|
||||
return (
|
||||
<Section title="Advisers" contentClassName={styles.sectionContent}>
|
||||
<Grid columns={5} className={styles.grid}>
|
||||
{advisers.map((adviser, index) => (
|
||||
<AdviserCard key={index} {...adviser} />
|
||||
<Grid columns={columns} className={styles.grid}>
|
||||
{assistants.map((adviser) => (
|
||||
<AdviserCard key={adviser._id} {...adviser} />
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
export function AdvisersSectionSkeleton() {
|
||||
return (
|
||||
<Section title="Advisers" contentClassName={styles.sectionContent}>
|
||||
<Skeleton className={clsx(styles.advisersSkeleton, styles.skeleton)} />
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
@ -13,4 +13,9 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.compatibilityImage {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
@ -4,28 +4,36 @@ import Typography from "../ui/Typography/Typography"
|
||||
import styles from "./CompatibilityCard.module.scss"
|
||||
import { IconName } from "../ui/Icon/Icon";
|
||||
import MetaLabel from "../ui/MetaLabel/MetaLabel";
|
||||
import { CompatibilityAction } from "@/entities/dashboard/types";
|
||||
|
||||
export default function CompatibilityCard() {
|
||||
type CompatibilityCardProps = CompatibilityAction;
|
||||
|
||||
export default function CompatibilityCard({
|
||||
imageUrl,
|
||||
title,
|
||||
type,
|
||||
minutes
|
||||
}: CompatibilityCardProps) {
|
||||
return (
|
||||
<Card className={styles.card}>
|
||||
<Image
|
||||
className={styles.logo}
|
||||
src="/compatibility-card.png"
|
||||
className={styles.compatibilityImage}
|
||||
src={imageUrl}
|
||||
alt="Compatibility image"
|
||||
width={120}
|
||||
height={110}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
<Typography size="lg" weight="medium">
|
||||
Compatibility
|
||||
<Typography size="lg" weight="medium" align="left">
|
||||
{title}
|
||||
</Typography>
|
||||
<MetaLabel iconLabelProps={{
|
||||
iconProps: {
|
||||
name: IconName.Article,
|
||||
},
|
||||
children: <Typography color="secondary">Article</Typography>
|
||||
children: <Typography color="secondary">{type}</Typography>
|
||||
}}>
|
||||
5 min
|
||||
{minutes} min
|
||||
</MetaLabel>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@ -7,4 +7,8 @@
|
||||
|
||||
.grid {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.compatibilitySkeleton.skeleton {
|
||||
height: 236px;
|
||||
}
|
||||
@ -1,29 +1,31 @@
|
||||
import { CompatibilityAction } from "@/entities/dashboard/types";
|
||||
import CompatibilityCard from "../CompatibilityCard/CompatibilityCard";
|
||||
import Grid from "../ui/Grid/Grid"
|
||||
import Section from "../ui/Section/Section"
|
||||
import styles from "./CompatibilitySection.module.scss"
|
||||
import { use } from "react";
|
||||
import Skeleton from "../ui/Skeleton/Skeleton";
|
||||
import clsx from "clsx";
|
||||
|
||||
const compatibilities = [
|
||||
{ title: "Compatibility" },
|
||||
{ title: "Compatibility" },
|
||||
{ title: "Compatibility" },
|
||||
{ title: "Compatibility" },
|
||||
{ title: "Compatibility" },
|
||||
{ title: "Compatibility" },
|
||||
{ title: "Compatibility" },
|
||||
{ title: "Compatibility" },
|
||||
{ title: "Compatibility" },
|
||||
{ title: "Compatibility" },
|
||||
];
|
||||
export default function CompatibilitySection({ promise }: { promise: Promise<CompatibilityAction[]> }) {
|
||||
const compatibilities = use(promise);
|
||||
const columns = Math.ceil(compatibilities?.length / 2);
|
||||
|
||||
export default function CompatibilitySection() {
|
||||
return (
|
||||
<Section title="Compatibility" contentClassName={styles.sectionContent}>
|
||||
<Grid columns={5} className={styles.grid}>
|
||||
{compatibilities.map((compatibility, index) => (
|
||||
<CompatibilityCard key={index} {...compatibility} />
|
||||
<Grid columns={columns} className={styles.grid}>
|
||||
{compatibilities.map((compatibility) => (
|
||||
<CompatibilityCard key={compatibility._id} {...compatibility} />
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
export function CompatibilitySectionSkeleton() {
|
||||
return (
|
||||
<Section title="Compatibility" contentClassName={styles.sectionContent}>
|
||||
<Skeleton className={clsx(styles.compatibilitySkeleton, styles.skeleton)} />
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
@ -33,4 +33,9 @@
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.meditationImage {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
@ -5,13 +5,21 @@ import styles from "./MeditationCard.module.scss"
|
||||
import Icon, { IconName } from "../ui/Icon/Icon";
|
||||
import MetaLabel from "../ui/MetaLabel/MetaLabel";
|
||||
import Button from "../ui/Button/Button";
|
||||
import { Meditation } from "@/entities/dashboard/types";
|
||||
|
||||
export default function MeditationCard() {
|
||||
type MeditationCardProps = Meditation;
|
||||
|
||||
export default function MeditationCard({
|
||||
imageUrl,
|
||||
title,
|
||||
type,
|
||||
minutes
|
||||
}: MeditationCardProps) {
|
||||
return (
|
||||
<Card className={styles.card}>
|
||||
<Image
|
||||
className={styles.logo}
|
||||
src="/meditation-card.png"
|
||||
className={styles.meditationImage}
|
||||
src={imageUrl}
|
||||
alt="Meditation image"
|
||||
width={342}
|
||||
height={216}
|
||||
@ -19,7 +27,7 @@ export default function MeditationCard() {
|
||||
<div className={styles.content}>
|
||||
<div className={styles.info}>
|
||||
<Typography size="lg" weight="regular">
|
||||
Reset
|
||||
{title}
|
||||
</Typography>
|
||||
<MetaLabel iconLabelProps={{
|
||||
iconProps: {
|
||||
@ -30,9 +38,9 @@ export default function MeditationCard() {
|
||||
height: 25
|
||||
}
|
||||
},
|
||||
children: <Typography color="secondary">Therapy</Typography>
|
||||
children: <Typography color="secondary">{type}</Typography>
|
||||
}}>
|
||||
15 min
|
||||
{minutes} min
|
||||
</MetaLabel>
|
||||
</div>
|
||||
<Button className={styles.button}>
|
||||
|
||||
@ -7,4 +7,8 @@
|
||||
|
||||
.grid {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.meditationSkeleton.skeleton {
|
||||
height: 308px;
|
||||
}
|
||||
@ -1,24 +1,31 @@
|
||||
import { Meditation } from "@/entities/dashboard/types";
|
||||
import MeditationCard from "../MeditationCard/MeditationCard";
|
||||
import Grid from "../ui/Grid/Grid"
|
||||
import Section from "../ui/Section/Section"
|
||||
import styles from "./MeditationSection.module.scss"
|
||||
import { use } from "react";
|
||||
import Skeleton from "../ui/Skeleton/Skeleton";
|
||||
import clsx from "clsx";
|
||||
|
||||
const meditations = [
|
||||
{ title: "Meditation" },
|
||||
{ title: "Meditation" },
|
||||
{ title: "Meditation" },
|
||||
{ title: "Meditation" },
|
||||
{ title: "Meditation" },
|
||||
];
|
||||
export default function MeditationSection({ promise }: { promise: Promise<Meditation[]> }) {
|
||||
const meditations = use(promise);
|
||||
const columns = meditations?.length;
|
||||
|
||||
export default function MeditationSection() {
|
||||
return (
|
||||
<Section title="Meditations" contentClassName={styles.sectionContent}>
|
||||
<Grid columns={5} className={styles.grid}>
|
||||
{meditations.map((meditation, index) => (
|
||||
<MeditationCard key={index} {...meditation} />
|
||||
<Grid columns={columns} className={styles.grid}>
|
||||
{meditations.map((meditation) => (
|
||||
<MeditationCard key={meditation._id} {...meditation} />
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
export function MeditationSectionSkeleton() {
|
||||
return (
|
||||
<Section title="Meditations" contentClassName={styles.sectionContent}>
|
||||
<Skeleton className={clsx(styles.meditationSkeleton, styles.skeleton)} />
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
@ -25,4 +25,9 @@
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.palmImage {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
@ -4,14 +4,22 @@ import Typography from "../ui/Typography/Typography"
|
||||
import styles from "./PalmCard.module.scss"
|
||||
import { IconName } from "../ui/Icon/Icon";
|
||||
import MetaLabel from "../ui/MetaLabel/MetaLabel";
|
||||
import { PalmAction } from "@/entities/dashboard/types";
|
||||
|
||||
export default function PalmCard() {
|
||||
type PalmCardProps = PalmAction;
|
||||
|
||||
export default function PalmCard({
|
||||
imageUrl,
|
||||
title,
|
||||
type,
|
||||
minutes
|
||||
}: PalmCardProps) {
|
||||
return (
|
||||
<Card className={styles.card}>
|
||||
<div className={styles.image}>
|
||||
<Image
|
||||
className={styles.logo}
|
||||
src="/palm-card.png"
|
||||
className={styles.palmImage}
|
||||
src={imageUrl}
|
||||
alt="Palm image"
|
||||
width={99}
|
||||
height={123}
|
||||
@ -20,7 +28,7 @@ export default function PalmCard() {
|
||||
<div className={styles.content}>
|
||||
<div className={styles.info}>
|
||||
<Typography size="lg" align="left">
|
||||
Код рождения в линиях
|
||||
{title}
|
||||
</Typography>
|
||||
<MetaLabel iconLabelProps={{
|
||||
iconProps: {
|
||||
@ -31,9 +39,9 @@ export default function PalmCard() {
|
||||
height: 25
|
||||
}
|
||||
},
|
||||
children: <Typography color="secondary">Article</Typography>
|
||||
children: <Typography color="secondary">{type}</Typography>
|
||||
}}>
|
||||
5 min
|
||||
{minutes} min
|
||||
</MetaLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -7,4 +7,8 @@
|
||||
|
||||
.grid {
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.palmSkeleton.skeleton {
|
||||
height: 227px;
|
||||
}
|
||||
@ -1,24 +1,31 @@
|
||||
import { PalmAction } from "@/entities/dashboard/types";
|
||||
import PalmCard from "../PalmCard/PalmCard";
|
||||
import Grid from "../ui/Grid/Grid"
|
||||
import Section from "../ui/Section/Section"
|
||||
import styles from "./PalmSection.module.scss"
|
||||
import { use } from "react";
|
||||
import Skeleton from "../ui/Skeleton/Skeleton";
|
||||
import clsx from "clsx";
|
||||
|
||||
const palms = [
|
||||
{ title: "Palm" },
|
||||
{ title: "Palm" },
|
||||
{ title: "Palm" },
|
||||
{ title: "Palm" },
|
||||
{ title: "Palm" },
|
||||
];
|
||||
export default function PalmSection({ promise }: { promise: Promise<PalmAction[]> }) {
|
||||
const palms = use(promise);
|
||||
const columns = palms?.length;
|
||||
|
||||
export default function PalmSection() {
|
||||
return (
|
||||
<Section title="Palm" contentClassName={styles.sectionContent}>
|
||||
<Grid columns={5} className={styles.grid}>
|
||||
{palms.map((palm, index) => (
|
||||
<PalmCard key={index} {...palm} />
|
||||
<Grid columns={columns} className={styles.grid}>
|
||||
{palms.map((palm) => (
|
||||
<PalmCard key={palm._id} {...palm} />
|
||||
))}
|
||||
</Grid>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
export function PalmSectionSkeleton() {
|
||||
return (
|
||||
<Section title="Palm" contentClassName={styles.sectionContent}>
|
||||
<Skeleton className={clsx(styles.palmSkeleton, styles.skeleton)} />
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { ReactNode } from "react";
|
||||
import { CSSProperties, ReactNode } from "react";
|
||||
import NotificationIcon from "./icons/Notification";
|
||||
import clsx from "clsx";
|
||||
import SearchIcon from "./icons/Search";
|
||||
@ -6,6 +6,7 @@ import MenuIcon from "./icons/Menu";
|
||||
import ArticleIcon from "./icons/Article";
|
||||
import VideoIcon from "./icons/Video";
|
||||
import ChevronIcon from "./icons/Chevron";
|
||||
import StarIcon from "./icons/Star";
|
||||
|
||||
export enum IconName {
|
||||
Notification,
|
||||
@ -13,7 +14,8 @@ export enum IconName {
|
||||
Menu,
|
||||
Article,
|
||||
Video,
|
||||
Chevron
|
||||
Chevron,
|
||||
Star
|
||||
};
|
||||
|
||||
const icons: Record<IconName, React.ComponentType<React.SVGProps<SVGSVGElement>>> = {
|
||||
@ -23,6 +25,7 @@ const icons: Record<IconName, React.ComponentType<React.SVGProps<SVGSVGElement>>
|
||||
[IconName.Article]: ArticleIcon,
|
||||
[IconName.Video]: VideoIcon,
|
||||
[IconName.Chevron]: ChevronIcon,
|
||||
[IconName.Star]: StarIcon,
|
||||
};
|
||||
|
||||
export type IconProps = {
|
||||
@ -35,6 +38,8 @@ export type IconProps = {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
cursor?: "pointer" | "auto"
|
||||
iconStyle?: CSSProperties
|
||||
style?: CSSProperties
|
||||
};
|
||||
|
||||
export default function Icon({
|
||||
@ -47,17 +52,22 @@ export default function Icon({
|
||||
className,
|
||||
children,
|
||||
cursor = "pointer",
|
||||
style,
|
||||
...rest
|
||||
}: IconProps) {
|
||||
const Component = icons[name];
|
||||
return (
|
||||
<span style={{ position: "relative", display: "inline-block", cursor, width: size.width, height: size.height }} className={clsx(className)}>
|
||||
<span style={{ position: "relative", display: "inline-block", cursor, width: size.width, height: size.height, ...style }} className={clsx(className)}>
|
||||
<Component
|
||||
width={size.width}
|
||||
height={size.height}
|
||||
color={color}
|
||||
aria-hidden="true"
|
||||
{...rest}
|
||||
style={{
|
||||
display: "block",
|
||||
...rest.iconStyle
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
</span>
|
||||
|
||||
8
src/components/ui/Icon/icons/Star.tsx
Normal file
8
src/components/ui/Icon/icons/Star.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import { SVGProps } from "react";
|
||||
|
||||
export default function StarIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return <svg width="9" height="9" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path d="M4.57382 0.781219C4.49102 0.609363 4.31603 0.5 4.12387 0.5C3.9317 0.5 3.75828 0.609363 3.67392 0.781219L2.66934 2.84817L0.425835 3.17939C0.238356 3.20751 0.082123 3.33874 0.0243168 3.51841C-0.0334894 3.69808 0.0133805 3.8965 0.147741 4.02929L1.77569 5.64005L1.39135 7.91636C1.36011 8.10384 1.43822 8.29444 1.5929 8.40537C1.74757 8.51629 1.95223 8.53035 2.12096 8.4413L4.12543 7.37111L6.1299 8.4413C6.29863 8.53035 6.5033 8.51785 6.65797 8.40537C6.81264 8.29288 6.89075 8.10384 6.85951 7.91636L6.47361 5.64005L8.10156 4.02929C8.23592 3.8965 8.28435 3.69808 8.22498 3.51841C8.16561 3.33874 8.01094 3.20751 7.82346 3.17939L5.5784 2.84817L4.57382 0.781219Z" fill="currentColor" />
|
||||
</svg>
|
||||
|
||||
}
|
||||
26
src/components/ui/Skeleton/Skeleton.module.scss
Normal file
26
src/components/ui/Skeleton/Skeleton.module.scss
Normal file
@ -0,0 +1,26 @@
|
||||
.skeleton {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #e5e7eb;
|
||||
border-radius: 24px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.6) 50%,
|
||||
transparent 100%);
|
||||
transform: translateX(-100%);
|
||||
animation: shimmer 1.6s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
12
src/components/ui/Skeleton/Skeleton.tsx
Normal file
12
src/components/ui/Skeleton/Skeleton.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import type { CSSProperties } from "react";
|
||||
import clsx from "clsx";
|
||||
import styles from "./Skeleton.module.scss";
|
||||
|
||||
interface SkeletonProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export default function Skeleton({ className, style }: SkeletonProps) {
|
||||
return <div className={clsx(styles.skeleton, className)} style={style} />;
|
||||
}
|
||||
33
src/components/ui/Spinner/Spinner.tsx
Normal file
33
src/components/ui/Spinner/Spinner.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import clsx from "clsx";
|
||||
import styles from "./styles.module.scss";
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export interface SpinnerProps {
|
||||
size?: number | string;
|
||||
color?: string;
|
||||
thickness?: number;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export default function Spinner({
|
||||
size = 28,
|
||||
color = "currentColor",
|
||||
thickness = 3,
|
||||
className,
|
||||
style,
|
||||
}: SpinnerProps) {
|
||||
const resolvedSize = typeof size === "number" ? `${size}px` : size;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={clsx(styles.spinner, className)}
|
||||
style={{
|
||||
"--spinner-size": resolvedSize,
|
||||
"--spinner-color": color,
|
||||
"--spinner-thickness": `${thickness}px`,
|
||||
...style,
|
||||
} as CSSProperties}
|
||||
/>
|
||||
);
|
||||
}
|
||||
19
src/components/ui/Spinner/styles.module.scss
Normal file
19
src/components/ui/Spinner/styles.module.scss
Normal file
@ -0,0 +1,19 @@
|
||||
.spinner {
|
||||
--spinner-size: 32px;
|
||||
--spinner-color: currentColor;
|
||||
--spinner-thickness: 3px;
|
||||
|
||||
width: var(--spinner-size);
|
||||
height: var(--spinner-size);
|
||||
border: var(--spinner-thickness) solid var(--spinner-color);
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
17
src/components/ui/Stars/Stars.module.scss
Normal file
17
src/components/ui/Stars/Stars.module.scss
Normal file
@ -0,0 +1,17 @@
|
||||
.stars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.star {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.starFill {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
48
src/components/ui/Stars/Stars.tsx
Normal file
48
src/components/ui/Stars/Stars.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import Icon, { IconName } from "../Icon/Icon";
|
||||
import clsx from "clsx";
|
||||
import styles from "./Stars.module.scss";
|
||||
|
||||
interface StarsProps {
|
||||
rating?: number;
|
||||
size?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Stars({ rating = 5, size = 9, className }: StarsProps) {
|
||||
const total = 5;
|
||||
|
||||
const fills = Array.from({ length: total }).map((_, i) => {
|
||||
const diff = rating - i;
|
||||
return Math.max(Math.min(diff, 1), 0) * 100;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.stars, className)}>
|
||||
{fills.map((fill, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={styles.star}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
<Icon
|
||||
name={IconName.Star}
|
||||
size={{ width: size, height: size }}
|
||||
color="#E5E7EB"
|
||||
cursor="auto"
|
||||
/>
|
||||
|
||||
{fill > 0 && (
|
||||
<span className={styles.starFill} style={{ width: `${fill}%` }}>
|
||||
<Icon
|
||||
name={IconName.Star}
|
||||
size={{ width: size, height: size }}
|
||||
color="#FFD600"
|
||||
cursor="auto"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
src/entities/dashboard/api.ts
Normal file
12
src/entities/dashboard/api.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { http } from "@/shared/api/httpClient";
|
||||
import { DashboardSchema, DashboardData } from "./types";
|
||||
import { delay } from "@/shared/utils/delay";
|
||||
|
||||
export const getDashboard = async () => {
|
||||
await delay(3_000);
|
||||
|
||||
return http.get<DashboardData>("/dashboard", {
|
||||
tags: ["dashboard"],
|
||||
schema: DashboardSchema,
|
||||
});
|
||||
}
|
||||
17
src/entities/dashboard/loaders.ts
Normal file
17
src/entities/dashboard/loaders.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { cache } from "react";
|
||||
import { getDashboard } from "./api";
|
||||
|
||||
export const loadDashboard = cache(getDashboard);
|
||||
|
||||
export const loadAssistants = cache(() =>
|
||||
loadDashboard().then(d => d.assistants)
|
||||
);
|
||||
export const loadCompatibility = cache(() =>
|
||||
loadDashboard().then(d => d.compatibilityActions)
|
||||
);
|
||||
export const loadMeditations = cache(() =>
|
||||
loadDashboard().then(d => d.meditations)
|
||||
);
|
||||
export const loadPalms = cache(() =>
|
||||
loadDashboard().then(d => d.palmActions)
|
||||
);
|
||||
83
src/entities/dashboard/types.ts
Normal file
83
src/entities/dashboard/types.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/* ---------- Assistant ---------- */
|
||||
export const AssistantSchema = z.object({
|
||||
_id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
rating: z.number(),
|
||||
reviewCount: z.number(),
|
||||
experience: z.number(),
|
||||
readings: z.number(),
|
||||
category: z.string(),
|
||||
price: z.number(),
|
||||
gender: z.enum(["male", "female"]),
|
||||
photoUrl: z.string().url(),
|
||||
externalId: z.string(),
|
||||
clientSource: z.string(),
|
||||
createdAt: z.string(), // ISO-строка даты
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
export type Assistant = z.infer<typeof AssistantSchema>;
|
||||
|
||||
/* ---------- Field (для compatibilityActions) ---------- */
|
||||
export const FieldSchema = z.object({
|
||||
_id: z.string(),
|
||||
actionId: z.string(),
|
||||
key: z.string(),
|
||||
title: z.string(),
|
||||
inputType: z.string(), // text | date | time …
|
||||
model: z.string().optional(), // присутствует не всегда
|
||||
property: z.string().optional(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
export type Field = z.infer<typeof FieldSchema>;
|
||||
|
||||
/* ---------- CompatibilityAction ---------- */
|
||||
export const CompatibilityActionSchema = z.object({
|
||||
_id: z.string(),
|
||||
title: z.string(),
|
||||
minutes: z.number(),
|
||||
type: z.string(),
|
||||
imageUrl: z.string().url(),
|
||||
prompt: z.string(),
|
||||
fields: z.array(FieldSchema),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
export type CompatibilityAction = z.infer<typeof CompatibilityActionSchema>;
|
||||
|
||||
/* ---------- PalmAction ---------- */
|
||||
export const PalmActionSchema = z.object({
|
||||
_id: z.string(),
|
||||
title: z.string(),
|
||||
minutes: z.number(),
|
||||
type: z.string(),
|
||||
imageUrl: z.string().url(),
|
||||
prompt: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
export type PalmAction = z.infer<typeof PalmActionSchema>;
|
||||
|
||||
/* ---------- Meditation ---------- */
|
||||
export const MeditationSchema = z.object({
|
||||
_id: z.string(),
|
||||
title: z.string(),
|
||||
minutes: z.number(),
|
||||
type: z.string(),
|
||||
imageUrl: z.string().url(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
export type Meditation = z.infer<typeof MeditationSchema>;
|
||||
|
||||
/* ---------- Итоговый ответ /dashboard ---------- */
|
||||
export const DashboardSchema = z.object({
|
||||
assistants: z.array(AssistantSchema),
|
||||
compatibilityActions: z.array(CompatibilityActionSchema),
|
||||
palmActions: z.array(PalmActionSchema),
|
||||
meditations: z.array(MeditationSchema),
|
||||
});
|
||||
export type DashboardData = z.infer<typeof DashboardSchema>;
|
||||
61
src/shared/api/httpClient.ts
Normal file
61
src/shared/api/httpClient.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
public status: number,
|
||||
public data: unknown,
|
||||
message = "API Error"
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
}
|
||||
}
|
||||
|
||||
type RequestOpts = Omit<RequestInit, "method" | "body"> & {
|
||||
tags?: string[]; // next.js cache-tag
|
||||
query?: Record<string, unknown>; // query-string
|
||||
schema?: z.ZodTypeAny; // runtime validation
|
||||
};
|
||||
|
||||
class HttpClient {
|
||||
constructor(private baseUrl: string) { }
|
||||
|
||||
private buildUrl(path: string, query?: Record<string, unknown>) {
|
||||
const url = new URL(path, this.baseUrl);
|
||||
if (query)
|
||||
Object.entries(query).forEach(([k, v]) =>
|
||||
url.searchParams.append(k, String(v))
|
||||
);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
method: "GET" | "POST" | "PATCH" | "DELETE",
|
||||
path: string,
|
||||
opts: RequestOpts = {},
|
||||
body?: unknown
|
||||
): Promise<T> {
|
||||
const { tags = [], schema, query, ...rest } = opts;
|
||||
const res = await fetch(this.buildUrl(path, query), {
|
||||
method,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
next: { revalidate: 3600, tags },
|
||||
...rest,
|
||||
});
|
||||
|
||||
const payload = await res.json().catch(() => null);
|
||||
|
||||
if (!res.ok) throw new ApiError(res.status, payload);
|
||||
|
||||
const data = payload as T;
|
||||
return schema ? schema.parse(data) : data;
|
||||
}
|
||||
|
||||
get = <T>(p: string, o?: RequestOpts) => this.request<T>("GET", p, o);
|
||||
post = <T>(p: string, b: unknown, o?: RequestOpts) => this.request<T>("POST", p, o, b);
|
||||
patch = <T>(p: string, b: unknown, o?: RequestOpts) => this.request<T>("PATCH", p, o, b);
|
||||
delete = <T>(p: string, o?: RequestOpts) => this.request<T>("DELETE", p, o);
|
||||
}
|
||||
|
||||
export const http = new HttpClient(process.env.NEXT_PUBLIC_API_URL!);
|
||||
2
src/shared/utils/delay.ts
Normal file
2
src/shared/utils/delay.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const delay = (ms: number) =>
|
||||
new Promise<void>((r) => setTimeout(r, ms));
|
||||
Loading…
Reference in New Issue
Block a user