From 1577eabcde8ea65e1c0cb806c998cd8869680fe3 Mon Sep 17 00:00:00 2001 From: StepanovPlaton Date: Tue, 9 Jul 2024 12:10:18 +0400 Subject: [PATCH] 09-07 --- .env.development | 6 + next.config.mjs | 12 +- package-lock.json | 49 ++++- package.json | 13 +- src/app/[section]/[item_id]/page.tsx | 54 ++++++ src/app/[section]/page.tsx | 39 ++++ src/app/favicon.ico | Bin 25931 -> 0 bytes src/app/globals.css | 79 ++++++-- src/app/layout.tsx | 18 +- src/app/page.tsx | 172 +++++++----------- src/entities/item/beans/index.ts | 19 ++ src/entities/item/beans/schema.ts | 51 ++++++ src/entities/item/beans/service.ts | 29 +++ src/entities/item/combinations/index.ts | 19 ++ src/entities/item/combinations/schema.ts | 42 +++++ src/entities/item/combinations/service.ts | 29 +++ src/entities/item/facts/index.ts | 19 ++ src/entities/item/facts/schema.ts | 42 +++++ src/entities/item/facts/service.ts | 29 +++ src/entities/item/index.ts | 22 +++ src/entities/item/item.ts | 49 +++++ src/entities/item/mileStones/index.ts | 19 ++ src/entities/item/mileStones/schema.ts | 42 +++++ src/entities/item/mileStones/service.ts | 29 +++ src/entities/item/recipes/index.ts | 19 ++ src/entities/item/recipes/schema.ts | 53 ++++++ src/entities/item/recipes/service.ts | 29 +++ src/entities/item/types.ts | 37 ++++ .../colorSchemeSwitch/colorSchemeSwitch.tsx | 17 ++ src/features/colorSchemeSwitch/index.ts | 3 + src/features/itemCard/beanCard.tsx | 41 +++++ src/features/itemCard/combinationCard.tsx | 25 +++ src/features/itemCard/factCard.tsx | 27 +++ src/features/itemCard/index.ts | 2 + src/features/itemCard/itemCard.tsx | 39 ++++ src/features/itemCard/mileStoneCard.tsx | 26 +++ src/features/itemCard/recipeCard.tsx | 42 +++++ src/features/itemInfo/beanInfo.tsx | 71 ++++++++ src/features/itemInfo/index.ts | 2 + src/features/itemInfo/itemInfo.tsx | 32 ++++ src/features/itemInfo/recipeInfo.tsx | 102 +++++++++++ src/features/sections/index.ts | 2 + src/features/sections/sections.ts | 75 ++++++++ src/shared/assets/icons/check.tsx | 26 +++ src/shared/assets/icons/cross.tsx | 24 +++ src/shared/assets/icons/index.ts | 5 + src/shared/assets/icons/sunIcon.tsx | 31 ++++ src/shared/utils/http.ts | 93 ++++++++++ src/widgets/grid/grid.tsx | 64 +++++++ src/widgets/grid/index.ts | 2 + src/widgets/header/header.tsx | 59 ++++++ src/widgets/header/index.ts | 3 + src/widgets/header/mobileMenu/mobileMenu.tsx | 50 +++++ tailwind.config.ts | 32 ++++ tsconfig.json | 3 +- 55 files changed, 1779 insertions(+), 139 deletions(-) create mode 100644 .env.development create mode 100644 src/app/[section]/[item_id]/page.tsx create mode 100644 src/app/[section]/page.tsx delete mode 100644 src/app/favicon.ico create mode 100644 src/entities/item/beans/index.ts create mode 100644 src/entities/item/beans/schema.ts create mode 100644 src/entities/item/beans/service.ts create mode 100644 src/entities/item/combinations/index.ts create mode 100644 src/entities/item/combinations/schema.ts create mode 100644 src/entities/item/combinations/service.ts create mode 100644 src/entities/item/facts/index.ts create mode 100644 src/entities/item/facts/schema.ts create mode 100644 src/entities/item/facts/service.ts create mode 100644 src/entities/item/index.ts create mode 100644 src/entities/item/item.ts create mode 100644 src/entities/item/mileStones/index.ts create mode 100644 src/entities/item/mileStones/schema.ts create mode 100644 src/entities/item/mileStones/service.ts create mode 100644 src/entities/item/recipes/index.ts create mode 100644 src/entities/item/recipes/schema.ts create mode 100644 src/entities/item/recipes/service.ts create mode 100644 src/entities/item/types.ts create mode 100644 src/features/colorSchemeSwitch/colorSchemeSwitch.tsx create mode 100644 src/features/colorSchemeSwitch/index.ts create mode 100644 src/features/itemCard/beanCard.tsx create mode 100644 src/features/itemCard/combinationCard.tsx create mode 100644 src/features/itemCard/factCard.tsx create mode 100644 src/features/itemCard/index.ts create mode 100644 src/features/itemCard/itemCard.tsx create mode 100644 src/features/itemCard/mileStoneCard.tsx create mode 100644 src/features/itemCard/recipeCard.tsx create mode 100644 src/features/itemInfo/beanInfo.tsx create mode 100644 src/features/itemInfo/index.ts create mode 100644 src/features/itemInfo/itemInfo.tsx create mode 100644 src/features/itemInfo/recipeInfo.tsx create mode 100644 src/features/sections/index.ts create mode 100644 src/features/sections/sections.ts create mode 100644 src/shared/assets/icons/check.tsx create mode 100644 src/shared/assets/icons/cross.tsx create mode 100644 src/shared/assets/icons/index.ts create mode 100644 src/shared/assets/icons/sunIcon.tsx create mode 100644 src/shared/utils/http.ts create mode 100644 src/widgets/grid/grid.tsx create mode 100644 src/widgets/grid/index.ts create mode 100644 src/widgets/header/header.tsx create mode 100644 src/widgets/header/index.ts create mode 100644 src/widgets/header/mobileMenu/mobileMenu.tsx diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..d29bb5a --- /dev/null +++ b/.env.development @@ -0,0 +1,6 @@ +IMAGES_PROTOCOL=https +IMAGES_DOMAIN=cdn-tp1.mozu.com +IMAGES_PORT=80 + +NEXT_PUBLIC_BASE_URL=http://127.0.0.1:3000 +NEXT_PUBLIC_API_URL=https://jellybellywikiapi.onrender.com/api \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index 4678774..e6d8945 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,14 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + + images: { + remotePatterns: [ + { + protocol: process.env.IMAGES_PROTOCOL, + hostname: process.env.IMAGES_DOMAIN, + }, + ], + }, +}; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 142ccd4..952bd5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,19 @@ "name": "jelly_belly_wiki", "version": "0.1.0", "dependencies": { + "clsx": "^2.1.1", "next": "14.2.4", + "next-themes": "^0.3.0", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "react-responsive-masonry": "^2.2.1", + "zod": "^3.23.8" }, "devDependencies": { "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@types/react-responsive-masonry": "^2.1.3", "eslint": "^8", "eslint-config-next": "14.2.4", "postcss": "^8", @@ -472,6 +477,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-responsive-masonry": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/react-responsive-masonry/-/react-responsive-masonry-2.1.3.tgz", + "integrity": "sha512-aOFUtv3QwNMmy0BgpQpvivQ/+vivMTB6ARrzf9eTSXsLzXpVnfEtjpHpSknYDnr8KaQmlgeauAj8E7wo/qMOTg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@typescript-eslint/parser": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", @@ -1080,6 +1094,14 @@ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3213,6 +3235,15 @@ } } }, + "node_modules/next-themes": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz", + "integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18", + "react-dom": "^16.8 || ^17 || ^18" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -3799,6 +3830,14 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/react-responsive-masonry": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-responsive-masonry/-/react-responsive-masonry-2.2.1.tgz", + "integrity": "sha512-QY1vH8vWd8YpW2g40zsFp4CjttK2NWw2btzHbxks8vDRe+0JZfsrtK7Ob3siCtg+9mttwsofmAB6dp9ujSYwKw==", + "dependencies": { + "caniuse-lite": "^1.0.30001638" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4895,6 +4934,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 74f3f38..ec815fa 100644 --- a/package.json +++ b/package.json @@ -9,18 +9,23 @@ "lint": "next lint" }, "dependencies": { + "clsx": "^2.1.1", + "next": "14.2.4", + "next-themes": "^0.3.0", "react": "^18", "react-dom": "^18", - "next": "14.2.4" + "react-responsive-masonry": "^2.2.1", + "zod": "^3.23.8" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@types/react-responsive-masonry": "^2.1.3", + "eslint": "^8", + "eslint-config-next": "14.2.4", "postcss": "^8", "tailwindcss": "^3.4.1", - "eslint": "^8", - "eslint-config-next": "14.2.4" + "typescript": "^5" } } diff --git a/src/app/[section]/[item_id]/page.tsx b/src/app/[section]/[item_id]/page.tsx new file mode 100644 index 0000000..c17fafc --- /dev/null +++ b/src/app/[section]/[item_id]/page.tsx @@ -0,0 +1,54 @@ +import { ItemService } from "@/entities/item"; +import { SectionService } from "@/features/sections"; +import { redirect } from "next/navigation"; +import { Metadata } from "next"; +import { ItemInfo } from "@/features/itemInfo"; +import { ItemCard } from "@/features/itemCard"; + +export async function generateMetadata({ + params: { section, item_id }, +}: { + params: { section: string; item_id: number }; +}): Promise { + if (!SectionService.isSection(section)) redirect("/"); // + return { + title: `JellyBelly: ${SectionService.sectionsConfiguration[section].sectionName}`, + }; +} + +export default async function Item({ + params: { section, item_id }, +}: { + params: { section: string; item_id: number }; +}) { + const item = SectionService.isSection(section) + ? await ItemService.itemsConfiguration[ + SectionService.sectionsConfiguration[section].itemType + ].service.Get(item_id) + : redirect("/"); + + const firstPage = + SectionService.isSection(section) && + (await ItemService.itemsConfiguration[ + SectionService.sectionsConfiguration[section].itemType + ].service.GetPage(1)); + + return ( + <> + {item && } + + {SectionService.isSection(section) && firstPage && ( + <> +

+ {SectionService.sectionsConfiguration[section].partOfSectionName} +

+
+ {firstPage.items.map((item) => ( + + ))} +
+ + )} + + ); +} diff --git a/src/app/[section]/page.tsx b/src/app/[section]/page.tsx new file mode 100644 index 0000000..a29557c --- /dev/null +++ b/src/app/[section]/page.tsx @@ -0,0 +1,39 @@ +import { ItemService } from "@/entities/item"; +import { redirect } from "next/navigation"; +import { Metadata } from "next"; +import { SectionService } from "@/features/sections"; +import { Grid } from "@/widgets/grid"; + +export async function generateMetadata({ + params: { section }, +}: { + params: { section: string }; +}): Promise { + if (!SectionService.isSection(section)) redirect("/"); + return { + title: `JellyBelly: ${SectionService.sectionsConfiguration[section].sectionName}`, + }; +} + +export default async function SectionPage({ + params: { section }, +}: { + params: { section: string }; +}) { + const pageOfItems = SectionService.isSection(section) + ? await ItemService.itemsConfiguration[ + SectionService.sectionsConfiguration[section].itemType + ].service.GetPage(1) + : redirect("/"); + + return ( + <> + {SectionService.isSection(section) && pageOfItems && ( + + )} + + ); +} diff --git a/src/app/favicon.ico b/src/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m diff --git a/src/app/globals.css b/src/app/globals.css index 875c01e..da8b36f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3,31 +3,72 @@ @tailwind utilities; :root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; + --color-bg0: #fdf6e3; + --color-bg1: #eee8d5; + --color-bg4: #839496; + + --color-fg0: #002b36; + --color-fg1: #073642; + --color-fg4: #657b83; + + --color-ac0: #268bd2; + --color-ac1: #859900; + --color-ac2: #b58900; + + --color-err: #dc322f; + + --app-width: 70%; + font-size: calc((100vw / 1920) * 20); } -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } +[data-theme="dark"] { + --color-bg0: #002b36; + --color-bg1: #073642; + --color-bg4: #657b83; + + --color-fg0: #fdf6e3; + --color-fg1: #eee8d5; + --color-fg4: #839496; + + --color-ac0: #268bd2; + --color-ac1: #859900; + --color-ac2: #b58900; + + --color-err: #dc322f; +} + +html, +body { + padding: 0; + margin: 0; + width: 100%; + height: 100%; } body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + transition-property: color, background-color, border-color; + transition-duration: 0.3s; + + color: var(--color-fg1); + background-color: var(--color-bg0); + + } -@layer utilities { - .text-balance { - text-wrap: balance; +body * { + scrollbar-color: var(--color-ac0) var(--color-bg1); + scrollbar-width: thin; +} + +@media (max-width: 1024px) { + :root { + font-size: calc((100vw / 1920) * 56); + --app-width: 100%; } } + +@media (max-width: 640px) { + :root { + font-size: calc((100vw / 1920) * 64); + } +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3314e47..0dc27b1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,12 +1,14 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; import "./globals.css"; +import { ThemeProvider } from "next-themes"; +import { Header } from "@/widgets/header"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Jelly Belly Wiki", + description: "Information about everything Jelly belly", }; export default function RootLayout({ @@ -15,8 +17,16 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - - {children} + // suppressHydrationWarning for theme support + + + +
+
+ {children} +
+ + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 2acfd44..117cc72 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,113 +1,67 @@ -import Image from "next/image"; +import { ItemService, ItemType } from "@/entities/item"; +import { ItemCard } from "@/features/itemCard"; +import { SectionService, SectionType } from "@/features/sections"; +import { Metadata } from "next"; +import Link from "next/link"; + +export const metadata: Metadata = { + title: ".Torrent", + description: + ".Torrent - сервис обмена .torrent файлами видеоигр, фильмов и аудиокниг", +}; + +export default async function Home() { + const requests = SectionService.sections.map((section) => + ItemService.itemsConfiguration[ + SectionService.sectionsConfiguration[section].itemType + ].service.GetPage(1) + ); + const data = await Promise.all(requests); + + const items = await SectionService.sections.reduce( + (cards, section, i) => ({ + ...cards, + [section]: data[i]?.items, + }), + {} as { [k in SectionType]: ItemType[] | null } + ); -export default function Home() { return ( -
-
-

- Get started by editing  - src/app/page.tsx -

- -
- -
- Next.js Logo -
- - -
+
+ {items && + SectionService.sections.map((section, i) => ( +
+ {items[section] && ( + <> +

+ { + SectionService.sectionsConfiguration[section] + .partOfSectionName + } +

+
+ {items[section].map((item) => ( + + ))} +
+
+ + { + SectionService.sectionsConfiguration[section] + .sectionInviteText + } + +
+ + )} +
+ ))} +
); } diff --git a/src/entities/item/beans/index.ts b/src/entities/item/beans/index.ts new file mode 100644 index 0000000..14322f5 --- /dev/null +++ b/src/entities/item/beans/index.ts @@ -0,0 +1,19 @@ +import { + beanSchema, + beansSchema, + pageOfBeansSchema, + thisItemIsBean, + type BeanType, + type PageOfBeansType, +} from "./schema"; +export { + beanSchema, + beansSchema, + pageOfBeansSchema, + thisItemIsBean, + type BeanType, + type PageOfBeansType, +}; + +import { BeansService } from "./service"; +export { BeansService }; diff --git a/src/entities/item/beans/schema.ts b/src/entities/item/beans/schema.ts new file mode 100644 index 0000000..a3e2db6 --- /dev/null +++ b/src/entities/item/beans/schema.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; +import { ItemType, TypesOfItems } from "../types"; + +export const beanSchema = z.object({ + beanId: z.number(), + groupName: z.array(z.string()), + ingredients: z.array(z.string()), + flavorName: z.string(), + description: z.string(), + colorGroup: z.string(), + backgroundColor: z.string(), + imageUrl: z.string(), + glutenFree: z.boolean(), + sugarFree: z.boolean(), + seasonal: z.boolean(), + kosher: z.boolean(), + + // Показывает, что этот item - bean + type: z + .any() + .optional() + .transform(() => TypesOfItems.bean), +}); +export type BeanType = z.infer; + +export const isBean = (a: any): a is BeanType => { + return beanSchema.safeParse(a).success; +}; + +export const beansSchema = z.array(z.any()).transform((a) => { + const beans: BeanType[] = []; + a.forEach((e) => { + if (isBean(e)) beans.push(beanSchema.parse(e)); + else console.error("Bean parse error - ", e); + }); + return beans; +}); + +export const pageOfBeansSchema = z.object({ + totalCount: z.number(), + pageSize: z.number(), + currentPage: z.number(), + totalPages: z.number(), + items: beansSchema, +}); + +export type PageOfBeansType = z.infer; + +export const thisItemIsBean = (i: ItemType): i is BeanType => { + return (i as BeanType).type === TypesOfItems.bean; +}; diff --git a/src/entities/item/beans/service.ts b/src/entities/item/beans/service.ts new file mode 100644 index 0000000..5b1dd5f --- /dev/null +++ b/src/entities/item/beans/service.ts @@ -0,0 +1,29 @@ +import { HTTPService } from "@/shared/utils/http"; +import { IItemService, staticImplements } from "../types"; +import { beanSchema, pageOfBeansSchema } from "./schema"; + +@staticImplements() +export abstract class BeansService { + public static urlPrefix = "beans"; + public static cacheOptions = { + next: { + revalidate: 60 * 5, + }, + }; + + public static async Get(id: number) { + return await HTTPService.get( + `/${this.urlPrefix}/${id}`, + beanSchema, + this.cacheOptions + ); + } + + public static async GetPage(page: number, pageSize?: number) { + return await HTTPService.get( + `/${this.urlPrefix}?pageIndex=${page}&pageSize=${pageSize ?? 2 * 3 * 2}`, + pageOfBeansSchema, + this.cacheOptions + ); + } +} diff --git a/src/entities/item/combinations/index.ts b/src/entities/item/combinations/index.ts new file mode 100644 index 0000000..46c4fb3 --- /dev/null +++ b/src/entities/item/combinations/index.ts @@ -0,0 +1,19 @@ +import { + combinationSchema, + combinationsSchema, + pageOfCombinationsSchema, + thisItemIsCombination, + type CombinationType, + type PageOfCombinationsType, +} from "./schema"; +export { + combinationSchema, + combinationsSchema, + pageOfCombinationsSchema, + thisItemIsCombination, + type CombinationType, + type PageOfCombinationsType, +}; + +import { CombinationsService } from "./service"; +export { CombinationsService }; diff --git a/src/entities/item/combinations/schema.ts b/src/entities/item/combinations/schema.ts new file mode 100644 index 0000000..746e70f --- /dev/null +++ b/src/entities/item/combinations/schema.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; +import { ItemType, TypesOfItems } from "../types"; + +export const combinationSchema = z.object({ + combinationId: z.number(), + name: z.string(), + tag: z.array(z.string()), + + // Показывает, что этот item - combination + type: z + .any() + .optional() + .transform(() => TypesOfItems.combination), +}); +export type CombinationType = z.infer; + +export const isCombination = (a: any): a is CombinationType => { + return combinationSchema.safeParse(a).success; +}; + +export const combinationsSchema = z.array(z.any()).transform((a) => { + const combinations: CombinationType[] = []; + a.forEach((e) => { + if (isCombination(e)) combinations.push(combinationSchema.parse(e)); + else console.error("Combination parse error - ", e); + }); + return combinations; +}); + +export const pageOfCombinationsSchema = z.object({ + totalCount: z.number(), + pageSize: z.number(), + currentPage: z.number(), + totalPages: z.number(), + items: combinationsSchema, +}); + +export type PageOfCombinationsType = z.infer; + +export const thisItemIsCombination = (i: ItemType): i is CombinationType => { + return (i as CombinationType).type === TypesOfItems.combination; +}; diff --git a/src/entities/item/combinations/service.ts b/src/entities/item/combinations/service.ts new file mode 100644 index 0000000..97fd0f4 --- /dev/null +++ b/src/entities/item/combinations/service.ts @@ -0,0 +1,29 @@ +import { HTTPService } from "@/shared/utils/http"; +import { IItemService, staticImplements } from "../types"; +import { combinationSchema, pageOfCombinationsSchema } from "./schema"; + +@staticImplements() +export abstract class CombinationsService { + public static urlPrefix = "combinations"; + public static cacheOptions = { + next: { + revalidate: 60 * 5, + }, + }; + + public static async Get(id: number) { + return await HTTPService.get( + `/${this.urlPrefix}/${id}`, + combinationSchema, + this.cacheOptions + ); + } + + public static async GetPage(page: number, pageSize?: number) { + return await HTTPService.get( + `/${this.urlPrefix}?pageIndex=${page}&pageSize=${pageSize ?? 2 * 3 * 3}`, + pageOfCombinationsSchema, + this.cacheOptions + ); + } +} diff --git a/src/entities/item/facts/index.ts b/src/entities/item/facts/index.ts new file mode 100644 index 0000000..f7b9d64 --- /dev/null +++ b/src/entities/item/facts/index.ts @@ -0,0 +1,19 @@ +import { + factSchema, + factsSchema, + pageOfFactsSchema, + thisItemIsFact, + type FactType, + type PageOfFactsType, +} from "./schema"; +export { + factSchema, + factsSchema, + pageOfFactsSchema, + thisItemIsFact, + type FactType, + type PageOfFactsType, +}; + +import { FactsService } from "./service"; +export { FactsService }; diff --git a/src/entities/item/facts/schema.ts b/src/entities/item/facts/schema.ts new file mode 100644 index 0000000..ccb2f26 --- /dev/null +++ b/src/entities/item/facts/schema.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; +import { ItemType, TypesOfItems } from "../types"; + +export const factSchema = z.object({ + factId: z.number(), + title: z.string(), + description: z.string(), + + // Показывает, что этот item - fact + type: z + .any() + .optional() + .transform(() => TypesOfItems.fact), +}); +export type FactType = z.infer; + +export const isFact = (a: any): a is FactType => { + return factSchema.safeParse(a).success; +}; + +export const factsSchema = z.array(z.any()).transform((a) => { + const facts: FactType[] = []; + a.forEach((e) => { + if (isFact(e)) facts.push(factSchema.parse(e)); + else console.error("Fact parse error - ", e); + }); + return facts; +}); + +export const pageOfFactsSchema = z.object({ + totalCount: z.number(), + pageSize: z.number(), + currentPage: z.number(), + totalPages: z.number(), + items: factsSchema, +}); + +export type PageOfFactsType = z.infer; + +export const thisItemIsFact = (i: ItemType): i is FactType => { + return (i as FactType).type === TypesOfItems.fact; +}; diff --git a/src/entities/item/facts/service.ts b/src/entities/item/facts/service.ts new file mode 100644 index 0000000..8b7572e --- /dev/null +++ b/src/entities/item/facts/service.ts @@ -0,0 +1,29 @@ +import { HTTPService } from "@/shared/utils/http"; +import { IItemService, staticImplements } from "../types"; +import { factSchema, pageOfFactsSchema } from "./schema"; + +@staticImplements() +export abstract class FactsService { + public static urlPrefix = "facts"; + public static cacheOptions = { + next: { + revalidate: 60 * 5, + }, + }; + + public static async Get(id: number) { + return await HTTPService.get( + `/${this.urlPrefix}/${id}`, + factSchema, + this.cacheOptions + ); + } + + public static async GetPage(page: number, pageSize?: number) { + return await HTTPService.get( + `/${this.urlPrefix}?pageIndex=${page}&pageSize=${pageSize ?? 2 * 3 * 3}`, + pageOfFactsSchema, + this.cacheOptions + ); + } +} diff --git a/src/entities/item/index.ts b/src/entities/item/index.ts new file mode 100644 index 0000000..e228ab1 --- /dev/null +++ b/src/entities/item/index.ts @@ -0,0 +1,22 @@ +import { thisItemIsBean, type BeanType } from "./beans"; +export { thisItemIsBean, type BeanType }; + +import { thisItemIsFact, type FactType } from "./facts"; +export { thisItemIsFact, type FactType }; + +import { thisItemIsRecipe, type RecipeType } from "./recipes"; +export { thisItemIsRecipe, type RecipeType }; + +import { thisItemIsCombination, type CombinationType } from "./combinations"; +export { thisItemIsCombination, type CombinationType }; + +import { ItemService } from "./item"; +export { ItemService }; + +import { + TypesOfItems, + type IItemService, + type ItemType, + type PageOfItemsType, +} from "./types"; +export { TypesOfItems, type IItemService, type ItemType, type PageOfItemsType }; diff --git a/src/entities/item/item.ts b/src/entities/item/item.ts new file mode 100644 index 0000000..1abbb74 --- /dev/null +++ b/src/entities/item/item.ts @@ -0,0 +1,49 @@ +import { BeansService, thisItemIsBean } from "./beans"; +import { CombinationsService, thisItemIsCombination } from "./combinations"; +import { FactsService, thisItemIsFact } from "./facts"; +import { MileStonesService, thisItemIsMileStone } from "./mileStones"; +import { RecipesService, thisItemIsRecipe } from "./recipes"; +import { IItemService, ItemType, TypesOfItems } from "./types"; + +export abstract class ItemService { + static get itemsConfiguration(): { + [k in TypesOfItems]: { + service: IItemService; + }; + } { + return { + [TypesOfItems.bean]: { + service: BeansService, + }, + [TypesOfItems.fact]: { + service: FactsService, + }, + [TypesOfItems.recipe]: { + service: RecipesService, + }, + [TypesOfItems.combination]: { + service: CombinationsService, + }, + [TypesOfItems.mileStone]: { + service: MileStonesService, + }, + }; + } + + public static GetTypeOfItem(i: ItemType): TypesOfItems { + if (thisItemIsBean(i)) return TypesOfItems.bean; + if (thisItemIsFact(i)) return TypesOfItems.fact; + if (thisItemIsRecipe(i)) return TypesOfItems.recipe; + if (thisItemIsCombination(i)) return TypesOfItems.combination; + if (thisItemIsMileStone(i)) return TypesOfItems.mileStone; + throw Error("unknown Item"); + } + public static GetItemId(i: ItemType): number { + if (thisItemIsBean(i)) return i.beanId; + if (thisItemIsFact(i)) return i.factId; + if (thisItemIsRecipe(i)) return i.recipeId; + if (thisItemIsCombination(i)) return i.combinationId; + if (thisItemIsMileStone(i)) return i.mileStoneId; + throw Error("unknown Item"); + } +} diff --git a/src/entities/item/mileStones/index.ts b/src/entities/item/mileStones/index.ts new file mode 100644 index 0000000..a765fff --- /dev/null +++ b/src/entities/item/mileStones/index.ts @@ -0,0 +1,19 @@ +import { + mileStoneSchema, + mileStonesSchema, + pageOfMileStonesSchema, + thisItemIsMileStone, + type MileStoneType, + type PageOfMileStonesType, +} from "./schema"; +export { + mileStoneSchema, + mileStonesSchema, + pageOfMileStonesSchema, + thisItemIsMileStone, + type MileStoneType, + type PageOfMileStonesType, +}; + +import { MileStonesService } from "./service"; +export { MileStonesService }; diff --git a/src/entities/item/mileStones/schema.ts b/src/entities/item/mileStones/schema.ts new file mode 100644 index 0000000..fe40a35 --- /dev/null +++ b/src/entities/item/mileStones/schema.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; +import { ItemType, TypesOfItems } from "../types"; + +export const mileStoneSchema = z.object({ + mileStoneId: z.number(), + year: z.number(), + description: z.string(), + + // Показывает, что этот item - mileStone + type: z + .any() + .optional() + .transform(() => TypesOfItems.mileStone), +}); +export type MileStoneType = z.infer; + +export const isMileStone = (a: any): a is MileStoneType => { + return mileStoneSchema.safeParse(a).success; +}; + +export const mileStonesSchema = z.array(z.any()).transform((a) => { + const mileStones: MileStoneType[] = []; + a.forEach((e) => { + if (isMileStone(e)) mileStones.push(mileStoneSchema.parse(e)); + else console.error("MileStone parse error - ", e); + }); + return mileStones; +}); + +export const pageOfMileStonesSchema = z.object({ + totalCount: z.number(), + pageSize: z.number(), + currentPage: z.number(), + totalPages: z.number(), + items: mileStonesSchema, +}); + +export type PageOfMileStonesType = z.infer; + +export const thisItemIsMileStone = (i: ItemType): i is MileStoneType => { + return (i as MileStoneType).type === TypesOfItems.mileStone; +}; diff --git a/src/entities/item/mileStones/service.ts b/src/entities/item/mileStones/service.ts new file mode 100644 index 0000000..d415ee7 --- /dev/null +++ b/src/entities/item/mileStones/service.ts @@ -0,0 +1,29 @@ +import { HTTPService } from "@/shared/utils/http"; +import { IItemService, staticImplements } from "../types"; +import { mileStoneSchema, pageOfMileStonesSchema } from "./schema"; + +@staticImplements() +export abstract class MileStonesService { + public static urlPrefix = "mileStones"; + public static cacheOptions = { + next: { + revalidate: 60 * 5, + }, + }; + + public static async Get(id: number) { + return await HTTPService.get( + `/${this.urlPrefix}/${id}`, + mileStoneSchema, + this.cacheOptions + ); + } + + public static async GetPage(page: number, pageSize?: number) { + return await HTTPService.get( + `/${this.urlPrefix}?pageIndex=${page}&pageSize=${pageSize ?? 2 * 3 * 3}`, + pageOfMileStonesSchema, + this.cacheOptions + ); + } +} diff --git a/src/entities/item/recipes/index.ts b/src/entities/item/recipes/index.ts new file mode 100644 index 0000000..342b87c --- /dev/null +++ b/src/entities/item/recipes/index.ts @@ -0,0 +1,19 @@ +import { + recipeSchema, + recipesSchema, + pageOfRecipesSchema, + thisItemIsRecipe, + type RecipeType, + type PageOfRecipesType, +} from "./schema"; +export { + recipeSchema, + recipesSchema, + pageOfRecipesSchema, + thisItemIsRecipe, + type RecipeType, + type PageOfRecipesType, +}; + +import { RecipesService } from "./service"; +export { RecipesService }; diff --git a/src/entities/item/recipes/schema.ts b/src/entities/item/recipes/schema.ts new file mode 100644 index 0000000..bc01e38 --- /dev/null +++ b/src/entities/item/recipes/schema.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; +import { ItemType, TypesOfItems } from "../types"; + +export const recipeSchema = z.object({ + recipeId: z.number(), + name: z.string(), + description: z.string(), + prepTime: z.string(), + cookTime: z.string(), + totalTime: z.string(), + makingAmount: z.string(), + imageUrl: z.string(), + ingredients: z.array(z.string()), + additions1: z.array(z.string()), + additions2: z.array(z.unknown()), + additions3: z.array(z.unknown()), + directions: z.array(z.string()), + tips: z.array(z.string()), + + // Показывает, что этот item - recipe + type: z + .any() + .optional() + .transform(() => TypesOfItems.recipe), +}); +export type RecipeType = z.infer; + +export const isRecipe = (a: any): a is RecipeType => { + return recipeSchema.safeParse(a).success; +}; + +export const recipesSchema = z.array(z.any()).transform((a) => { + const recipes: RecipeType[] = []; + a.forEach((e) => { + if (isRecipe(e)) recipes.push(recipeSchema.parse(e)); + else console.error("Recipe parse error - ", e); + }); + return recipes; +}); + +export const pageOfRecipesSchema = z.object({ + totalCount: z.number(), + pageSize: z.number(), + currentPage: z.number(), + totalPages: z.number(), + items: recipesSchema, +}); + +export type PageOfRecipesType = z.infer; + +export const thisItemIsRecipe = (i: ItemType): i is RecipeType => { + return (i as RecipeType).type === TypesOfItems.recipe; +}; diff --git a/src/entities/item/recipes/service.ts b/src/entities/item/recipes/service.ts new file mode 100644 index 0000000..07c59de --- /dev/null +++ b/src/entities/item/recipes/service.ts @@ -0,0 +1,29 @@ +import { HTTPService } from "@/shared/utils/http"; +import { IItemService, staticImplements } from "../types"; +import { recipeSchema, pageOfRecipesSchema } from "./schema"; + +@staticImplements() +export abstract class RecipesService { + public static urlPrefix = "recipes"; + public static cacheOptions = { + next: { + revalidate: 60 * 5, + }, + }; + + public static async Get(id: number) { + return await HTTPService.get( + `/${this.urlPrefix}/${id}`, + recipeSchema, + this.cacheOptions + ); + } + + public static async GetPage(page: number, pageSize?: number) { + return await HTTPService.get( + `/${this.urlPrefix}?pageIndex=${page}&pageSize=${pageSize ?? 2 * 3 * 2}`, + pageOfRecipesSchema, + this.cacheOptions + ); + } +} diff --git a/src/entities/item/types.ts b/src/entities/item/types.ts new file mode 100644 index 0000000..493d329 --- /dev/null +++ b/src/entities/item/types.ts @@ -0,0 +1,37 @@ +import { BeanType, PageOfBeansType } from "./beans"; +import { CombinationType, PageOfCombinationsType } from "./combinations"; +import { FactType, PageOfFactsType } from "./facts"; +import { MileStoneType, PageOfMileStonesType } from "./mileStones"; +import { PageOfRecipesType, RecipeType } from "./recipes"; + +export type ItemType = + | BeanType + | FactType + | RecipeType + | CombinationType + | MileStoneType; +export type PageOfItemsType = + | PageOfBeansType + | PageOfFactsType + | PageOfRecipesType + | PageOfCombinationsType + | PageOfMileStonesType; + +export enum TypesOfItems { + bean, + fact, + recipe, + combination, + mileStone, +} + +export interface IItemService { + urlPrefix: string; + Get(id: number): Promise; + GetPage(page: number, pageSize?: number): Promise; +} + +export const staticImplements = + () => + (constructor: U) => + constructor; diff --git a/src/features/colorSchemeSwitch/colorSchemeSwitch.tsx b/src/features/colorSchemeSwitch/colorSchemeSwitch.tsx new file mode 100644 index 0000000..8255fa8 --- /dev/null +++ b/src/features/colorSchemeSwitch/colorSchemeSwitch.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { SunIcon } from "@/shared/assets/icons"; +import { useTheme } from "next-themes"; + +export const ColorSchemeSwitch = () => { + const { theme, setTheme } = useTheme(); + + return ( + <> + setTheme(theme == "light" ? "dark" : "light")} + /> + + ); +}; diff --git a/src/features/colorSchemeSwitch/index.ts b/src/features/colorSchemeSwitch/index.ts new file mode 100644 index 0000000..a063be5 --- /dev/null +++ b/src/features/colorSchemeSwitch/index.ts @@ -0,0 +1,3 @@ +import { ColorSchemeSwitch } from "./colorSchemeSwitch"; + +export { ColorSchemeSwitch }; diff --git a/src/features/itemCard/beanCard.tsx b/src/features/itemCard/beanCard.tsx new file mode 100644 index 0000000..4e4ab1d --- /dev/null +++ b/src/features/itemCard/beanCard.tsx @@ -0,0 +1,41 @@ +import { BeanType, TypesOfItems } from "@/entities/item"; +import Link from "next/link"; +import { SectionService } from "@/features/sections"; +import Image from "next/image"; +import React from "react"; + +export const BeanCard = React.forwardRef( + ({ item: bean }, ref) => { + return ( +
+ + +
+

+ {bean.flavorName} +

+
+

+ {bean.description} +

+ +
+ ); + } +); diff --git a/src/features/itemCard/combinationCard.tsx b/src/features/itemCard/combinationCard.tsx new file mode 100644 index 0000000..67205e6 --- /dev/null +++ b/src/features/itemCard/combinationCard.tsx @@ -0,0 +1,25 @@ +import { CombinationType, TypesOfItems } from "@/entities/item"; +import Link from "next/link"; +import { SectionService } from "@/features/sections"; +import Image from "next/image"; +import React from "react"; + +export const CombinationCard = React.forwardRef< + HTMLDivElement, + { item: CombinationType } +>(({ item: combination }, ref) => { + return ( +
+
+
+

+ {combination.name} +

+
+

+ {combination.tag.join(" ")} +

+
+
+ ); +}); diff --git a/src/features/itemCard/factCard.tsx b/src/features/itemCard/factCard.tsx new file mode 100644 index 0000000..115fcc3 --- /dev/null +++ b/src/features/itemCard/factCard.tsx @@ -0,0 +1,27 @@ +import { FactType, TypesOfItems } from "@/entities/item"; +import Link from "next/link"; +import { SectionService } from "@/features/sections"; +import Image from "next/image"; +import React from "react"; + +export const FactCard = React.forwardRef( + ({ item: fact }, ref) => { + return ( +
+
+
+

+ {fact.title} +

+
+

+ {fact.description} +

+
+
+ ); + } +); diff --git a/src/features/itemCard/index.ts b/src/features/itemCard/index.ts new file mode 100644 index 0000000..b6b022c --- /dev/null +++ b/src/features/itemCard/index.ts @@ -0,0 +1,2 @@ +import { ItemCard } from "./itemCard"; +export { ItemCard }; diff --git a/src/features/itemCard/itemCard.tsx b/src/features/itemCard/itemCard.tsx new file mode 100644 index 0000000..5062c67 --- /dev/null +++ b/src/features/itemCard/itemCard.tsx @@ -0,0 +1,39 @@ +import { + BeanType, + CombinationType, + FactType, + ItemService, + ItemType, + RecipeType, + TypesOfItems, +} from "@/entities/item"; +import { BeanCard } from "./beanCard"; +import React from "react"; +import { FactCard } from "./factCard"; +import { RecipeCard } from "./recipeCard"; +import { CombinationCard } from "./combinationCard"; +import { MileStoneCard } from "./mileStoneCard"; +import { MileStoneType } from "@/entities/item/mileStones"; + +const ItemTypeToCard = ( + item: ItemType, + ref: React.ForwardedRef +) => { + return { + [TypesOfItems.bean]: , + [TypesOfItems.fact]: , + [TypesOfItems.recipe]: , + [TypesOfItems.combination]: ( + + ), + [TypesOfItems.mileStone]: ( + + ), + }[ItemService.GetTypeOfItem(item)]; +}; + +export const ItemCard = React.forwardRef( + ({ item }, ref) => { + return ItemTypeToCard(item, ref); + } +); diff --git a/src/features/itemCard/mileStoneCard.tsx b/src/features/itemCard/mileStoneCard.tsx new file mode 100644 index 0000000..ab3ad79 --- /dev/null +++ b/src/features/itemCard/mileStoneCard.tsx @@ -0,0 +1,26 @@ +import { TypesOfItems } from "@/entities/item"; +import Link from "next/link"; +import { SectionService } from "@/features/sections"; +import Image from "next/image"; +import React from "react"; +import { MileStoneType } from "@/entities/item/mileStones"; + +export const MileStoneCard = React.forwardRef< + HTMLDivElement, + { item: MileStoneType } +>(({ item: mileStone }, ref) => { + return ( +
+
+
+

+ {mileStone.year} +

+
+

+ {mileStone.description} +

+
+
+ ); +}); diff --git a/src/features/itemCard/recipeCard.tsx b/src/features/itemCard/recipeCard.tsx new file mode 100644 index 0000000..3505bce --- /dev/null +++ b/src/features/itemCard/recipeCard.tsx @@ -0,0 +1,42 @@ +import { BeanType, RecipeType, TypesOfItems } from "@/entities/item"; +import Link from "next/link"; +import { SectionService } from "@/features/sections"; +import Image from "next/image"; +import React from "react"; + +export const RecipeCard = React.forwardRef< + HTMLDivElement, + { item: RecipeType } +>(({ item: recipe }, ref) => { + return ( +
+ + +
+

+ {recipe.name} +

+
+

+ {recipe.description} +

+ +
+ ); +}); diff --git a/src/features/itemInfo/beanInfo.tsx b/src/features/itemInfo/beanInfo.tsx new file mode 100644 index 0000000..f64379a --- /dev/null +++ b/src/features/itemInfo/beanInfo.tsx @@ -0,0 +1,71 @@ +import { BeanType } from "@/entities/item"; +import { CheckIcon, CrossIcon } from "@/shared/assets/icons"; +import Image from "next/image"; + +const BeanPropertyDescription: { [k in keyof BeanType]?: string } = { + glutenFree: "Gluten free", + sugarFree: "Sugar free", + seasonal: "Seasonal", + kosher: "Kosher", +}; + +export const BeanInfo = ({ item: bean }: { item: BeanType }) => { + return ( +
+ +
+

{bean.flavorName}

+ {bean.description} +
+
+ Ingredients: +
    + {bean.ingredients.length > 0 && + bean.ingredients.map((ingredient) => ( +
  • - {ingredient}
  • + ))} +
+ {bean.ingredients.length == 0 && ( + Classified + )} +
+
+
+ In theese groups: +
    + {bean.groupName.map((group) => ( +
  • - {group}
  • + ))} +
+
+
+ Properties: +
    + {( + Object.keys( + BeanPropertyDescription + ) as (keyof typeof BeanPropertyDescription)[] + ).map((property) => ( +
  • + - {BeanPropertyDescription[property]}:{" "} + {bean[property] ? ( + + ) : ( + + )} +
  • + ))} +
+
+
+
+
+
+ ); +}; diff --git a/src/features/itemInfo/index.ts b/src/features/itemInfo/index.ts new file mode 100644 index 0000000..709dff9 --- /dev/null +++ b/src/features/itemInfo/index.ts @@ -0,0 +1,2 @@ +import { ItemInfo } from "./itemInfo"; +export { ItemInfo }; diff --git a/src/features/itemInfo/itemInfo.tsx b/src/features/itemInfo/itemInfo.tsx new file mode 100644 index 0000000..919f3c5 --- /dev/null +++ b/src/features/itemInfo/itemInfo.tsx @@ -0,0 +1,32 @@ +import { + BeanType, + ItemService, + ItemType, + RecipeType, + TypesOfItems, +} from "@/entities/item"; +import React from "react"; +import { redirect } from "next/navigation"; +import { SectionService } from "../sections"; +import { BeanInfo } from "./beanInfo"; +import { RecipeInfo } from "./recipeInfo"; + +const ItemTypeToInfo = (item: ItemType) => { + const ItemInfoComponents = { + [TypesOfItems.bean]: , + [TypesOfItems.recipe]: , + }; + const typeOfItem = ItemService.GetTypeOfItem(item); + if (typeOfItem in ItemInfoComponents) + return ItemInfoComponents[typeOfItem as keyof typeof ItemInfoComponents]; + redirect( + "/" + + SectionService.sectionsConfiguration[ + SectionService.itemTypeToSection[ItemService.GetTypeOfItem(item)] + ].sectionUrl + ); +}; + +export const ItemInfo = ({ item }: { item: ItemType }) => { + return ItemTypeToInfo(item); +}; diff --git a/src/features/itemInfo/recipeInfo.tsx b/src/features/itemInfo/recipeInfo.tsx new file mode 100644 index 0000000..d5b60e4 --- /dev/null +++ b/src/features/itemInfo/recipeInfo.tsx @@ -0,0 +1,102 @@ +import { RecipeType } from "@/entities/item"; +import { CheckIcon, CrossIcon } from "@/shared/assets/icons"; +import Image from "next/image"; + +const RecipePropertyDescription: { [k in keyof RecipeType]?: string } = { + prepTime: "Prepare time", + cookTime: "Cook time", + totalTime: "Total time", +}; +const RecipeIngredientsDescription: { [k in keyof RecipeType]?: string } = { + ingredients: "Ingredients", + additions1: "Additional ingredients", + additions2: "Optional additives", + additions3: "You can add these products", +}; +const RecipeСookingDescription: { [k in keyof RecipeType]?: string } = { + directions: "Directions", + tips: "Tips", +}; + +export const RecipeInfo = ({ item: recipe }: { item: RecipeType }) => { + return ( +
+ +
+

{recipe.name}

+ {recipe.description} +
+
+ {( + Object.keys( + RecipeСookingDescription + ) as (keyof typeof RecipeСookingDescription)[] + ).map((property) => ( + <> + + {(recipe[property] as string[]).length > 0 && + RecipeСookingDescription[property] + ":"} + +
    + {(recipe[property] as string[]).length > 0 && + (recipe[property] as string[]).map((ingredient) => ( +
  • - {ingredient}
  • + ))} +
+ + ))} +
+
+
+ How long does it take: +
    + {( + Object.keys( + RecipePropertyDescription + ) as (keyof typeof RecipePropertyDescription)[] + ).map((property) => ( + <> + { +
  • + {`- ${RecipePropertyDescription[property]}: ${recipe[property]}`} + {recipe[property] == "" && ( + Classified + )} +
  • + } + + ))} +
+
+
+ {( + Object.keys( + RecipeIngredientsDescription + ) as (keyof typeof RecipeIngredientsDescription)[] + ).map((property) => ( + <> + + {(recipe[property] as string[]).length > 0 && + RecipeIngredientsDescription[property] + ":"} + +
    + {(recipe[property] as string[]).length > 0 && + (recipe[property] as string[]).map((ingredient) => ( +
  • - {ingredient}
  • + ))} +
+ + ))} +
+
+
+
+
+ ); +}; diff --git a/src/features/sections/index.ts b/src/features/sections/index.ts new file mode 100644 index 0000000..a878f51 --- /dev/null +++ b/src/features/sections/index.ts @@ -0,0 +1,2 @@ +import { SectionService, type SectionType } from "./sections"; +export { SectionService, type SectionType }; diff --git a/src/features/sections/sections.ts b/src/features/sections/sections.ts new file mode 100644 index 0000000..e24146a --- /dev/null +++ b/src/features/sections/sections.ts @@ -0,0 +1,75 @@ +import { TypesOfItems } from "@/entities/item"; + +export type SectionType = (typeof SectionService.sections)[number]; + +export abstract class SectionService { + static get itemTypeToSection(): { [k in TypesOfItems]: SectionType } { + return { + [TypesOfItems.bean]: "beans", + [TypesOfItems.fact]: "facts", + [TypesOfItems.recipe]: "recipes", + [TypesOfItems.combination]: "combinations", + [TypesOfItems.mileStone]: "history", + }; + } + + static get sectionsConfiguration(): { + [k in SectionType]: { + sectionName: string; + sectionUrl: string; + itemType: TypesOfItems; + partOfSectionName: string; + sectionInviteText: string; + }; + } { + return { + beans: { + sectionName: "Beans", + sectionUrl: "beans", + itemType: TypesOfItems.bean, + partOfSectionName: "Some beans", + sectionInviteText: 'Go to section "Beans"', + }, + facts: { + sectionName: "Facts", + sectionUrl: "facts", + itemType: TypesOfItems.fact, + partOfSectionName: "Some facts", + sectionInviteText: 'Go to section "Facts"', + }, + recipes: { + sectionName: "Recipes", + sectionUrl: "recipes", + itemType: TypesOfItems.recipe, + partOfSectionName: "Some recipes", + sectionInviteText: 'Go to section "Recipes"', + }, + combinations: { + sectionName: "Combinations", + sectionUrl: "combinations", + itemType: TypesOfItems.combination, + partOfSectionName: "Some combinations", + sectionInviteText: 'Go to section "Combinations"', + }, + history: { + sectionName: "History", + sectionUrl: "history", + itemType: TypesOfItems.mileStone, + partOfSectionName: "Some history", + sectionInviteText: 'Go to section "History"', + }, + }; + } + + static sections = [ + "beans", + "facts", + "recipes", + "combinations", + "history", + ] as const; + + static isSection = (a: string): a is SectionType => { + return this.sections.includes(a as SectionType); + }; +} diff --git a/src/shared/assets/icons/check.tsx b/src/shared/assets/icons/check.tsx new file mode 100644 index 0000000..d787ac3 --- /dev/null +++ b/src/shared/assets/icons/check.tsx @@ -0,0 +1,26 @@ +export const CheckIcon = ({ className }: { className?: string }) => { + return ( + + + + + {" "} + + + ); +}; diff --git a/src/shared/assets/icons/cross.tsx b/src/shared/assets/icons/cross.tsx new file mode 100644 index 0000000..4df8d8f --- /dev/null +++ b/src/shared/assets/icons/cross.tsx @@ -0,0 +1,24 @@ +export const CrossIcon = ({ className }: { className?: string }) => { + return ( + + + + + + + + ); +}; diff --git a/src/shared/assets/icons/index.ts b/src/shared/assets/icons/index.ts new file mode 100644 index 0000000..c1c6d76 --- /dev/null +++ b/src/shared/assets/icons/index.ts @@ -0,0 +1,5 @@ +import { SunIcon } from "./sunIcon"; +import { CheckIcon } from "./check"; +import { CrossIcon } from "./cross"; + +export { SunIcon, CheckIcon, CrossIcon }; diff --git a/src/shared/assets/icons/sunIcon.tsx b/src/shared/assets/icons/sunIcon.tsx new file mode 100644 index 0000000..cd0ef90 --- /dev/null +++ b/src/shared/assets/icons/sunIcon.tsx @@ -0,0 +1,31 @@ +export const SunIcon = ({ + className, + onClick, +}: { + className?: string; + onClick?: () => void; +}) => { + return ( + + + + ); +}; diff --git a/src/shared/utils/http.ts b/src/shared/utils/http.ts new file mode 100644 index 0000000..b0aa25e --- /dev/null +++ b/src/shared/utils/http.ts @@ -0,0 +1,93 @@ +import { z } from "zod"; + +export type RequestCacheOptions = { + cache?: RequestCache; + next?: NextFetchRequestConfig; +}; + +type GetRequestOptions = RequestCacheOptions & { + headers?: HeadersInit; +}; + +type RequestOptions = GetRequestOptions & { + body?: BodyInit | object; + stringify?: boolean; +}; + +export abstract class HTTPService { + private static deepUndefinedToNull(o?: object): object | undefined { + if (Array.isArray(o)) return o; + if (o) + return Object.fromEntries( + Object.entries(o).map(([k, v]) => { + if (v === undefined) return [k, null]; + if (typeof v === "object") return [k, this.deepUndefinedToNull(v)]; + return [k, v]; + }) + ); + } + + public static async request( + method: "GET" | "POST" | "PUT" | "DELETE", + url: string, + schema: Z, + options?: RequestOptions + ) { + return await fetch(process.env.NEXT_PUBLIC_API_URL + url, { + method: method, + headers: { + accept: "application/json", + ...((options?.stringify ?? true) != true + ? {} + : { "Content-Type": "application/json" }), + ...options?.headers, + }, + body: + (options?.stringify ?? true) != true + ? (options?.body as BodyInit) + : JSON.stringify( + this.deepUndefinedToNull(options?.body as object | undefined) + ), + cache: options?.cache ?? options?.next ? undefined : "no-cache", + next: options?.next ?? {}, + }) + .then((r) => { + if (r && r.ok) return r; + else throw Error("Response ok = false"); + }) + .then((r) => r.json()) + .then((d) => { + const parsed = schema.safeParse(d); + if (parsed.success) return parsed.data as z.infer; + else throw new Error(parsed.error.message); + }) + .catch((e) => { + console.error(e); + return null; + }); + } + + public static async get( + url: string, + schema: Z, + options?: GetRequestOptions + ) { + return await this.request("GET", url, schema, options); + } + + public static async post( + url: string, + schema: Z, + options?: RequestOptions + ) { + return await this.request("POST", url, schema, options); + } + + public static async put( + url: string, + schema: Z, + options?: RequestOptions + ) { + return await this.request("PUT", url, schema, options); + } +} diff --git a/src/widgets/grid/grid.tsx b/src/widgets/grid/grid.tsx new file mode 100644 index 0000000..9d819cc --- /dev/null +++ b/src/widgets/grid/grid.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { + ItemService, + ItemType, + PageOfItemsType, + TypesOfItems, +} from "@/entities/item"; +import { ItemCard } from "@/features/itemCard"; +import React, { useRef } from "react"; +import { useState } from "react"; +import Masonry, { ResponsiveMasonry } from "react-responsive-masonry"; + +export const Grid = ({ + firstPage, + typeOfItems, +}: { + firstPage: PageOfItemsType; + typeOfItems: TypesOfItems; +}) => { + const [pageIndex, changePageIndex] = useState(1); + const [totalPages, _] = useState(firstPage.totalPages); + const [items, changeItems] = useState(firstPage.items as U[]); + const [loadingPage, changeLoadingPage] = useState(false); + + const firstItemRef = useRef(null); + + const handleGridScroll = async ( + e: React.UIEvent + ) => { + const { scrollTop, scrollHeight, clientHeight } = + e.target as HTMLDivElement; + if ((scrollTop + clientHeight) / scrollHeight > 0.5) { + if (!loadingPage) { + changeLoadingPage(true); + setTimeout(() => changeLoadingPage(false), 1000); + if (pageIndex >= totalPages) return; + const nextPage = await ItemService.itemsConfiguration[ + typeOfItems + ].service.GetPage(pageIndex + 1); + if (nextPage) { + changePageIndex(pageIndex + 1); + changeItems([...items, ...(nextPage.items as U[])]); + console.log("new page"); + } + } + } + }; + + return ( +
+ {items.length > 0 && } + {items.length > 1 && + items + .slice(1) + .map((item, i) => ( + + ))} +
+ ); +}; diff --git a/src/widgets/grid/index.ts b/src/widgets/grid/index.ts new file mode 100644 index 0000000..41aa2e0 --- /dev/null +++ b/src/widgets/grid/index.ts @@ -0,0 +1,2 @@ +import { Grid } from "./grid"; +export { Grid }; diff --git a/src/widgets/header/header.tsx b/src/widgets/header/header.tsx new file mode 100644 index 0000000..ec532cb --- /dev/null +++ b/src/widgets/header/header.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { MobileMenu } from "./mobileMenu/mobileMenu"; +import Link from "next/link"; +import { useSelectedLayoutSegment } from "next/navigation"; +import clsx from "clsx"; +import { SectionService } from "@/features/sections"; +import { ColorSchemeSwitch } from "@/features/colorSchemeSwitch"; + +export const Header = () => { + const currentPageName = useSelectedLayoutSegment(); + + return ( +
+
+

+
+ +
+ JellyBelly +

+
+ {SectionService.sections.map((section) => ( + + {SectionService.sectionsConfiguration[section].sectionName} + + ))} +
+ + + {/* */} +
+
+ ); +}; diff --git a/src/widgets/header/index.ts b/src/widgets/header/index.ts new file mode 100644 index 0000000..a143001 --- /dev/null +++ b/src/widgets/header/index.ts @@ -0,0 +1,3 @@ +import { Header } from "./header"; + +export { Header }; diff --git a/src/widgets/header/mobileMenu/mobileMenu.tsx b/src/widgets/header/mobileMenu/mobileMenu.tsx new file mode 100644 index 0000000..b480368 --- /dev/null +++ b/src/widgets/header/mobileMenu/mobileMenu.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { SectionService } from "@/features/sections"; +import clsx from "clsx"; +import Link from "next/link"; +import { useState } from "react"; + +export const MobileMenu = () => { + const [open, changeMenuOpen] = useState(false); + + return ( +
+ +
changeMenuOpen(false)} + > + {SectionService.sections.map((section) => ( + + {SectionService.sectionsConfiguration[section].sectionName} + + ))} +
+
+ ); +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index e9a0944..46ca2cf 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -5,6 +5,7 @@ const config: Config = { "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", "./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + "./src/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { extend: { @@ -13,6 +14,37 @@ const config: Config = { "gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", }, + colors: { + bg0: "var(--color-bg0)", + bg1: "var(--color-bg1)", + bg4: "var(--color-bg4)", + fg0: "var(--color-fg0)", + fg1: "var(--color-fg1)", + fg4: "var(--color-fg4)", + ac0: "var(--color-ac0)", + ac1: "var(--color-ac1)", + ac2: "var(--color-ac2)", + err: "var(--color-err)", + }, + animation: { + fadeIn: "fadeIn 0.25s ease-in-out", + fadeOut: "fadeOut 0.25s ease-in-out", + }, + keyframes: () => ({ + fadeIn: { + "0%": { opacity: "0" }, + "100%": { opacity: "1" }, + }, + fadeOut: { + "0%": { opacity: "1" }, + "100%": { opacity: "0" }, + }, + }), + }, + screens: { + tb: "640px", + lp: "1024px", + dsk: "1280px", }, }, plugins: [], diff --git a/tsconfig.json b/tsconfig.json index 7b28589..8e364bc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,8 @@ ], "paths": { "@/*": ["./src/*"] - } + }, + "experimentalDecorators": true }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"]