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 718d6fe..0000000 Binary files a/src/app/favicon.ico and /dev/null differ 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"]