mirror of
https://github.com/StepanovPlaton/jelly_belly_wiki.git
synced 2026-04-03 12:20:41 +04:00
09-07
This commit is contained in:
6
.env.development
Normal file
6
.env.development
Normal file
@@ -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
|
||||||
@@ -1,4 +1,14 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {};
|
const nextConfig = {
|
||||||
|
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: process.env.IMAGES_PROTOCOL,
|
||||||
|
hostname: process.env.IMAGES_DOMAIN,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
49
package-lock.json
generated
49
package-lock.json
generated
@@ -8,14 +8,19 @@
|
|||||||
"name": "jelly_belly_wiki",
|
"name": "jelly_belly_wiki",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"next": "14.2.4",
|
"next": "14.2.4",
|
||||||
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18"
|
"react-dom": "^18",
|
||||||
|
"react-responsive-masonry": "^2.2.1",
|
||||||
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
"@types/react-responsive-masonry": "^2.1.3",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.4",
|
"eslint-config-next": "14.2.4",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
@@ -472,6 +477,15 @@
|
|||||||
"@types/react": "*"
|
"@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": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
|
"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": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"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": {
|
"node_modules/next/node_modules/postcss": {
|
||||||
"version": "8.4.31",
|
"version": "8.4.31",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||||
@@ -3799,6 +3830,14 @@
|
|||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -4895,6 +4934,14 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
package.json
13
package.json
@@ -9,18 +9,23 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"next": "14.2.4",
|
||||||
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"next": "14.2.4"
|
"react-responsive-masonry": "^2.2.1",
|
||||||
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
"@types/react-responsive-masonry": "^2.1.3",
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "14.2.4",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"eslint": "^8",
|
"typescript": "^5"
|
||||||
"eslint-config-next": "14.2.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
54
src/app/[section]/[item_id]/page.tsx
Normal file
54
src/app/[section]/[item_id]/page.tsx
Normal file
@@ -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<Metadata> {
|
||||||
|
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 && <ItemInfo item={item} />}
|
||||||
|
|
||||||
|
{SectionService.isSection(section) && firstPage && (
|
||||||
|
<>
|
||||||
|
<h2 className="text-5xl p-2 pt-8">
|
||||||
|
{SectionService.sectionsConfiguration[section].partOfSectionName}
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 tb:grid-cols-2 lp:grid-cols-3 gap-3 px-2">
|
||||||
|
{firstPage.items.map((item) => (
|
||||||
|
<ItemCard item={item} key={ItemService.GetItemId(item)} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
src/app/[section]/page.tsx
Normal file
39
src/app/[section]/page.tsx
Normal file
@@ -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<Metadata> {
|
||||||
|
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 && (
|
||||||
|
<Grid
|
||||||
|
firstPage={pageOfItems}
|
||||||
|
typeOfItems={SectionService.sectionsConfiguration[section].itemType}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@@ -3,31 +3,72 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--foreground-rgb: 0, 0, 0;
|
--color-bg0: #fdf6e3;
|
||||||
--background-start-rgb: 214, 219, 220;
|
--color-bg1: #eee8d5;
|
||||||
--background-end-rgb: 255, 255, 255;
|
--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) {
|
[data-theme="dark"] {
|
||||||
:root {
|
--color-bg0: #002b36;
|
||||||
--foreground-rgb: 255, 255, 255;
|
--color-bg1: #073642;
|
||||||
--background-start-rgb: 0, 0, 0;
|
--color-bg4: #657b83;
|
||||||
--background-end-rgb: 0, 0, 0;
|
|
||||||
|
--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 {
|
body {
|
||||||
color: rgb(var(--foreground-rgb));
|
transition-property: color, background-color, border-color;
|
||||||
background: linear-gradient(
|
transition-duration: 0.3s;
|
||||||
to bottom,
|
|
||||||
transparent,
|
color: var(--color-fg1);
|
||||||
rgb(var(--background-end-rgb))
|
background-color: var(--color-bg0);
|
||||||
)
|
|
||||||
rgb(var(--background-start-rgb));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
body * {
|
||||||
.text-balance {
|
scrollbar-color: var(--color-ac0) var(--color-bg1);
|
||||||
text-wrap: balance;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import { ThemeProvider } from "next-themes";
|
||||||
|
import { Header } from "@/widgets/header";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Jelly Belly Wiki",
|
||||||
description: "Generated by create next app",
|
description: "Information about everything Jelly belly",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@@ -15,8 +17,16 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
// suppressHydrationWarning for theme support
|
||||||
<body className={inter.className}>{children}</body>
|
<html lang="ru" suppressHydrationWarning>
|
||||||
|
<body className={inter.className}>
|
||||||
|
<ThemeProvider enableSystem={false} defaultTheme="light">
|
||||||
|
<Header />
|
||||||
|
<main className="w-full h-[calc(100%_-_5rem)] max-w-[var(--app-width)] m-auto">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
168
src/app/page.tsx
168
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 (
|
return (
|
||||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
<div className="h-full overflow-auto pb-4">
|
||||||
<div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
|
{items &&
|
||||||
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
|
SectionService.sections.map((section, i) => (
|
||||||
Get started by editing
|
<section key={section}>
|
||||||
<code className="font-mono font-bold">src/app/page.tsx</code>
|
{items[section] && (
|
||||||
</p>
|
<>
|
||||||
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:size-auto lg:bg-none">
|
<h2 className="text-5xl p-2 pt-8">
|
||||||
<a
|
{
|
||||||
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
|
SectionService.sectionsConfiguration[section]
|
||||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
.partOfSectionName
|
||||||
target="_blank"
|
}
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
By{" "}
|
|
||||||
<Image
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel Logo"
|
|
||||||
className="dark:invert"
|
|
||||||
width={100}
|
|
||||||
height={24}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative z-[-1] flex place-items-center before:absolute before:h-[300px] before:w-full before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-full after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 sm:before:w-[480px] sm:after:w-[240px] before:lg:h-[360px]">
|
|
||||||
<Image
|
|
||||||
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js Logo"
|
|
||||||
width={180}
|
|
||||||
height={37}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-32 grid text-center lg:mb-0 lg:w-full lg:max-w-5xl lg:grid-cols-4 lg:text-left">
|
|
||||||
<a
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2 className="mb-3 text-2xl font-semibold">
|
|
||||||
Docs{" "}
|
|
||||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
|
||||||
->
|
|
||||||
</span>
|
|
||||||
</h2>
|
</h2>
|
||||||
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
<div className="grid grid-cols-1 tb:grid-cols-2 lp:grid-cols-3 gap-3 px-2">
|
||||||
Find in-depth information about Next.js features and API.
|
{items[section].map((item) => (
|
||||||
</p>
|
<ItemCard item={item} key={ItemService.GetItemId(item)} />
|
||||||
</a>
|
))}
|
||||||
|
</div>
|
||||||
<a
|
<div className="w-full flex justify-end p-2">
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<Link
|
||||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
className="text-2xl text-fg4 cursor-pointer hover:underline"
|
||||||
target="_blank"
|
href={
|
||||||
rel="noopener noreferrer"
|
"/" +
|
||||||
>
|
SectionService.sectionsConfiguration[section].sectionUrl
|
||||||
<h2 className="mb-3 text-2xl font-semibold">
|
}
|
||||||
Learn{" "}
|
>
|
||||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
{
|
||||||
->
|
SectionService.sectionsConfiguration[section]
|
||||||
</span>
|
.sectionInviteText
|
||||||
</h2>
|
}
|
||||||
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
</Link>
|
||||||
Learn about Next.js in an interactive course with quizzes!
|
</div>
|
||||||
</p>
|
</>
|
||||||
</a>
|
)}
|
||||||
|
</section>
|
||||||
<a
|
))}
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2 className="mb-3 text-2xl font-semibold">
|
|
||||||
Templates{" "}
|
|
||||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
|
||||||
->
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
|
||||||
Explore starter templates for Next.js.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
|
||||||
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<h2 className="mb-3 text-2xl font-semibold">
|
|
||||||
Deploy{" "}
|
|
||||||
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
|
|
||||||
->
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<p className="m-0 max-w-[30ch] text-balance text-sm opacity-50">
|
|
||||||
Instantly deploy your Next.js site to a shareable URL with Vercel.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/entities/item/beans/index.ts
Normal file
19
src/entities/item/beans/index.ts
Normal file
@@ -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 };
|
||||||
51
src/entities/item/beans/schema.ts
Normal file
51
src/entities/item/beans/schema.ts
Normal file
@@ -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<typeof beanSchema>;
|
||||||
|
|
||||||
|
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<typeof pageOfBeansSchema>;
|
||||||
|
|
||||||
|
export const thisItemIsBean = (i: ItemType): i is BeanType => {
|
||||||
|
return (i as BeanType).type === TypesOfItems.bean;
|
||||||
|
};
|
||||||
29
src/entities/item/beans/service.ts
Normal file
29
src/entities/item/beans/service.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { HTTPService } from "@/shared/utils/http";
|
||||||
|
import { IItemService, staticImplements } from "../types";
|
||||||
|
import { beanSchema, pageOfBeansSchema } from "./schema";
|
||||||
|
|
||||||
|
@staticImplements<IItemService>()
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/entities/item/combinations/index.ts
Normal file
19
src/entities/item/combinations/index.ts
Normal file
@@ -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 };
|
||||||
42
src/entities/item/combinations/schema.ts
Normal file
42
src/entities/item/combinations/schema.ts
Normal file
@@ -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<typeof combinationSchema>;
|
||||||
|
|
||||||
|
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<typeof pageOfCombinationsSchema>;
|
||||||
|
|
||||||
|
export const thisItemIsCombination = (i: ItemType): i is CombinationType => {
|
||||||
|
return (i as CombinationType).type === TypesOfItems.combination;
|
||||||
|
};
|
||||||
29
src/entities/item/combinations/service.ts
Normal file
29
src/entities/item/combinations/service.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { HTTPService } from "@/shared/utils/http";
|
||||||
|
import { IItemService, staticImplements } from "../types";
|
||||||
|
import { combinationSchema, pageOfCombinationsSchema } from "./schema";
|
||||||
|
|
||||||
|
@staticImplements<IItemService>()
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/entities/item/facts/index.ts
Normal file
19
src/entities/item/facts/index.ts
Normal file
@@ -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 };
|
||||||
42
src/entities/item/facts/schema.ts
Normal file
42
src/entities/item/facts/schema.ts
Normal file
@@ -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<typeof factSchema>;
|
||||||
|
|
||||||
|
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<typeof pageOfFactsSchema>;
|
||||||
|
|
||||||
|
export const thisItemIsFact = (i: ItemType): i is FactType => {
|
||||||
|
return (i as FactType).type === TypesOfItems.fact;
|
||||||
|
};
|
||||||
29
src/entities/item/facts/service.ts
Normal file
29
src/entities/item/facts/service.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { HTTPService } from "@/shared/utils/http";
|
||||||
|
import { IItemService, staticImplements } from "../types";
|
||||||
|
import { factSchema, pageOfFactsSchema } from "./schema";
|
||||||
|
|
||||||
|
@staticImplements<IItemService>()
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/entities/item/index.ts
Normal file
22
src/entities/item/index.ts
Normal file
@@ -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 };
|
||||||
49
src/entities/item/item.ts
Normal file
49
src/entities/item/item.ts
Normal file
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/entities/item/mileStones/index.ts
Normal file
19
src/entities/item/mileStones/index.ts
Normal file
@@ -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 };
|
||||||
42
src/entities/item/mileStones/schema.ts
Normal file
42
src/entities/item/mileStones/schema.ts
Normal file
@@ -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<typeof mileStoneSchema>;
|
||||||
|
|
||||||
|
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<typeof pageOfMileStonesSchema>;
|
||||||
|
|
||||||
|
export const thisItemIsMileStone = (i: ItemType): i is MileStoneType => {
|
||||||
|
return (i as MileStoneType).type === TypesOfItems.mileStone;
|
||||||
|
};
|
||||||
29
src/entities/item/mileStones/service.ts
Normal file
29
src/entities/item/mileStones/service.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { HTTPService } from "@/shared/utils/http";
|
||||||
|
import { IItemService, staticImplements } from "../types";
|
||||||
|
import { mileStoneSchema, pageOfMileStonesSchema } from "./schema";
|
||||||
|
|
||||||
|
@staticImplements<IItemService>()
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/entities/item/recipes/index.ts
Normal file
19
src/entities/item/recipes/index.ts
Normal file
@@ -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 };
|
||||||
53
src/entities/item/recipes/schema.ts
Normal file
53
src/entities/item/recipes/schema.ts
Normal file
@@ -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<typeof recipeSchema>;
|
||||||
|
|
||||||
|
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<typeof pageOfRecipesSchema>;
|
||||||
|
|
||||||
|
export const thisItemIsRecipe = (i: ItemType): i is RecipeType => {
|
||||||
|
return (i as RecipeType).type === TypesOfItems.recipe;
|
||||||
|
};
|
||||||
29
src/entities/item/recipes/service.ts
Normal file
29
src/entities/item/recipes/service.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { HTTPService } from "@/shared/utils/http";
|
||||||
|
import { IItemService, staticImplements } from "../types";
|
||||||
|
import { recipeSchema, pageOfRecipesSchema } from "./schema";
|
||||||
|
|
||||||
|
@staticImplements<IItemService>()
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/entities/item/types.ts
Normal file
37
src/entities/item/types.ts
Normal file
@@ -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<ItemType | null>;
|
||||||
|
GetPage(page: number, pageSize?: number): Promise<PageOfItemsType | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const staticImplements =
|
||||||
|
<T>() =>
|
||||||
|
<U extends T>(constructor: U) =>
|
||||||
|
constructor;
|
||||||
17
src/features/colorSchemeSwitch/colorSchemeSwitch.tsx
Normal file
17
src/features/colorSchemeSwitch/colorSchemeSwitch.tsx
Normal file
@@ -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 (
|
||||||
|
<>
|
||||||
|
<SunIcon
|
||||||
|
className="mr-5 h-8 w-8 cursor-pointer"
|
||||||
|
onClick={() => setTheme(theme == "light" ? "dark" : "light")}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
3
src/features/colorSchemeSwitch/index.ts
Normal file
3
src/features/colorSchemeSwitch/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { ColorSchemeSwitch } from "./colorSchemeSwitch";
|
||||||
|
|
||||||
|
export { ColorSchemeSwitch };
|
||||||
41
src/features/itemCard/beanCard.tsx
Normal file
41
src/features/itemCard/beanCard.tsx
Normal file
@@ -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<HTMLDivElement, { item: BeanType }>(
|
||||||
|
({ item: bean }, ref) => {
|
||||||
|
return (
|
||||||
|
<div ref={ref} className="p-4 shadow-md rounded-lg h-full w-full">
|
||||||
|
<Link
|
||||||
|
className="group/itemcard cursor-pointer flex flex-col justify-evenly h-full"
|
||||||
|
href={
|
||||||
|
"/" +
|
||||||
|
SectionService.sectionsConfiguration[
|
||||||
|
SectionService.itemTypeToSection[TypesOfItems.bean]
|
||||||
|
].sectionUrl +
|
||||||
|
"/" +
|
||||||
|
bean.beanId
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={bean.imageUrl}
|
||||||
|
alt=""
|
||||||
|
className="w-full rounded-lg object-contain p-4"
|
||||||
|
width={1280}
|
||||||
|
height={720}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-between pr-2">
|
||||||
|
<h2 className="text-3xl tb:text-xl py-1 group-hover/itemcard:underline underline-offset-1">
|
||||||
|
{bean.flavorName}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg tb:text-sm pr-2 text-justify line-clamp-5 text-fg4">
|
||||||
|
{bean.description}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
25
src/features/itemCard/combinationCard.tsx
Normal file
25
src/features/itemCard/combinationCard.tsx
Normal file
@@ -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 (
|
||||||
|
<div ref={ref} className="p-4 shadow-md rounded-lg h-full w-full">
|
||||||
|
<div className="flex flex-col justify-between h-full">
|
||||||
|
<div className="flex items-center justify-between pr-2">
|
||||||
|
<h2 className="text-3xl tb:text-xl py-1 underline-offset-1">
|
||||||
|
{combination.name}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg tb:text-sm pr-2 text-justify line-clamp-5 text-fg4">
|
||||||
|
{combination.tag.join(" ")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
27
src/features/itemCard/factCard.tsx
Normal file
27
src/features/itemCard/factCard.tsx
Normal file
@@ -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<HTMLDivElement, { item: FactType }>(
|
||||||
|
({ item: fact }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="p-4 shadow-lg shadow-bg1 rounded-lg h-full w-full"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col justify-evenly h-full">
|
||||||
|
<div className="flex items-center justify-between pr-2">
|
||||||
|
<h2 className="text-3xl tb:text-xl py-1 underline-offset-1">
|
||||||
|
{fact.title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg tb:text-sm pr-2 text-justify line-clamp-5 text-fg4">
|
||||||
|
{fact.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
2
src/features/itemCard/index.ts
Normal file
2
src/features/itemCard/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { ItemCard } from "./itemCard";
|
||||||
|
export { ItemCard };
|
||||||
39
src/features/itemCard/itemCard.tsx
Normal file
39
src/features/itemCard/itemCard.tsx
Normal file
@@ -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<HTMLDivElement>
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
[TypesOfItems.bean]: <BeanCard item={item as BeanType} ref={ref} />,
|
||||||
|
[TypesOfItems.fact]: <FactCard item={item as FactType} ref={ref} />,
|
||||||
|
[TypesOfItems.recipe]: <RecipeCard item={item as RecipeType} ref={ref} />,
|
||||||
|
[TypesOfItems.combination]: (
|
||||||
|
<CombinationCard item={item as CombinationType} ref={ref} />
|
||||||
|
),
|
||||||
|
[TypesOfItems.mileStone]: (
|
||||||
|
<MileStoneCard item={item as MileStoneType} ref={ref} />
|
||||||
|
),
|
||||||
|
}[ItemService.GetTypeOfItem(item)];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ItemCard = React.forwardRef<HTMLDivElement, { item: ItemType }>(
|
||||||
|
({ item }, ref) => {
|
||||||
|
return ItemTypeToCard(item, ref);
|
||||||
|
}
|
||||||
|
);
|
||||||
26
src/features/itemCard/mileStoneCard.tsx
Normal file
26
src/features/itemCard/mileStoneCard.tsx
Normal file
@@ -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 (
|
||||||
|
<div ref={ref} className="p-4 shadow-md rounded-lg h-full w-full">
|
||||||
|
<div className="flex flex-col justify-between h-full">
|
||||||
|
<div className="flex items-center justify-between pr-2">
|
||||||
|
<h2 className="text-3xl tb:text-xl py-1 underline-offset-1">
|
||||||
|
{mileStone.year}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg tb:text-sm pr-2 text-justify line-clamp-5 text-fg4">
|
||||||
|
{mileStone.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
42
src/features/itemCard/recipeCard.tsx
Normal file
42
src/features/itemCard/recipeCard.tsx
Normal file
@@ -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 (
|
||||||
|
<div ref={ref} className="p-4 shadow-md rounded-lg h-full w-full">
|
||||||
|
<Link
|
||||||
|
className="group/itemcard cursor-pointer flex flex-col justify-between h-full"
|
||||||
|
href={
|
||||||
|
"/" +
|
||||||
|
SectionService.sectionsConfiguration[
|
||||||
|
SectionService.itemTypeToSection[TypesOfItems.recipe]
|
||||||
|
].sectionUrl +
|
||||||
|
"/" +
|
||||||
|
recipe.recipeId
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={recipe.imageUrl}
|
||||||
|
alt=""
|
||||||
|
className="w-full rounded-lg object-cover aspect-video"
|
||||||
|
width={1280}
|
||||||
|
height={720}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-between pr-2">
|
||||||
|
<h2 className="text-3xl tb:text-base py-1 group-hover/itemcard:underline underline-offset-1">
|
||||||
|
{recipe.name}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-lg tb:text-sm pr-2 text-justify line-clamp-5 text-fg4">
|
||||||
|
{recipe.description}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
71
src/features/itemInfo/beanInfo.tsx
Normal file
71
src/features/itemInfo/beanInfo.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="w-full float-start flex flex-col lp:flex-row">
|
||||||
|
<Image
|
||||||
|
src={bean.imageUrl}
|
||||||
|
alt=""
|
||||||
|
width={1280}
|
||||||
|
height={720}
|
||||||
|
className="w-full lp:max-w-[50%] object-contain float-left p-4"
|
||||||
|
/>
|
||||||
|
<div className="px-4 mb-10">
|
||||||
|
<h1 className="text-4xl pt-4 pb-1">{bean.flavorName}</h1>
|
||||||
|
<span className="text-fg4">{bean.description}</span>
|
||||||
|
<div className="py-1 flex justify-between">
|
||||||
|
<div className="w-1/2">
|
||||||
|
Ingredients:
|
||||||
|
<ul>
|
||||||
|
{bean.ingredients.length > 0 &&
|
||||||
|
bean.ingredients.map((ingredient) => (
|
||||||
|
<li className="text-sm tb:text-xs">- {ingredient}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{bean.ingredients.length == 0 && (
|
||||||
|
<span className="text-xl text-fg4">Classified</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-fg1 w-[40%]">
|
||||||
|
<div className="py-2">
|
||||||
|
In theese groups:
|
||||||
|
<ul>
|
||||||
|
{bean.groupName.map((group) => (
|
||||||
|
<li className="text-sm">- {group}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="py-2">
|
||||||
|
Properties:
|
||||||
|
<ul>
|
||||||
|
{(
|
||||||
|
Object.keys(
|
||||||
|
BeanPropertyDescription
|
||||||
|
) as (keyof typeof BeanPropertyDescription)[]
|
||||||
|
).map((property) => (
|
||||||
|
<li className="text-sm flex items-center pt-1">
|
||||||
|
- {BeanPropertyDescription[property]}:{" "}
|
||||||
|
{bean[property] ? (
|
||||||
|
<CheckIcon className="h-5 pl-2" />
|
||||||
|
) : (
|
||||||
|
<CrossIcon className="h-5 pl-2" />
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
2
src/features/itemInfo/index.ts
Normal file
2
src/features/itemInfo/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { ItemInfo } from "./itemInfo";
|
||||||
|
export { ItemInfo };
|
||||||
32
src/features/itemInfo/itemInfo.tsx
Normal file
32
src/features/itemInfo/itemInfo.tsx
Normal file
@@ -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]: <BeanInfo item={item as BeanType} />,
|
||||||
|
[TypesOfItems.recipe]: <RecipeInfo item={item as RecipeType} />,
|
||||||
|
};
|
||||||
|
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);
|
||||||
|
};
|
||||||
102
src/features/itemInfo/recipeInfo.tsx
Normal file
102
src/features/itemInfo/recipeInfo.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="w-full flex flex-col lp:flex-row mb-10 px-4">
|
||||||
|
<Image
|
||||||
|
src={recipe.imageUrl}
|
||||||
|
alt=""
|
||||||
|
width={1280}
|
||||||
|
height={720}
|
||||||
|
className="w-full lp:max-w-[50%] object-cover rounded-lg mt-4"
|
||||||
|
/>
|
||||||
|
<div className="lp:pl-4 text-fg1">
|
||||||
|
<h1 className="text-4xl pt-4 pb-1">{recipe.name}</h1>
|
||||||
|
<span className="text-fg4">{recipe.description}</span>
|
||||||
|
<div className="py-1 flex flex-col-reverse tb:flex-row tb:justify-between">
|
||||||
|
<div className="w-full tb:w-[60%]">
|
||||||
|
{(
|
||||||
|
Object.keys(
|
||||||
|
RecipeСookingDescription
|
||||||
|
) as (keyof typeof RecipeСookingDescription)[]
|
||||||
|
).map((property) => (
|
||||||
|
<>
|
||||||
|
<span className="text-lg">
|
||||||
|
{(recipe[property] as string[]).length > 0 &&
|
||||||
|
RecipeСookingDescription[property] + ":"}
|
||||||
|
</span>
|
||||||
|
<ul>
|
||||||
|
{(recipe[property] as string[]).length > 0 &&
|
||||||
|
(recipe[property] as string[]).map((ingredient) => (
|
||||||
|
<li className="text-sm tb:text-xs">- {ingredient}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className=" w-full tb:w-[35%]">
|
||||||
|
<div className="py-2">
|
||||||
|
<span className="text-lg">How long does it take:</span>
|
||||||
|
<ul>
|
||||||
|
{(
|
||||||
|
Object.keys(
|
||||||
|
RecipePropertyDescription
|
||||||
|
) as (keyof typeof RecipePropertyDescription)[]
|
||||||
|
).map((property) => (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
<li className="text-sm flex items-center pt-1">
|
||||||
|
{`- ${RecipePropertyDescription[property]}: ${recipe[property]}`}
|
||||||
|
{recipe[property] == "" && (
|
||||||
|
<span className="text-fg4 pl-1">Classified</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="py-2">
|
||||||
|
{(
|
||||||
|
Object.keys(
|
||||||
|
RecipeIngredientsDescription
|
||||||
|
) as (keyof typeof RecipeIngredientsDescription)[]
|
||||||
|
).map((property) => (
|
||||||
|
<>
|
||||||
|
<span className="text-lg">
|
||||||
|
{(recipe[property] as string[]).length > 0 &&
|
||||||
|
RecipeIngredientsDescription[property] + ":"}
|
||||||
|
</span>
|
||||||
|
<ul>
|
||||||
|
{(recipe[property] as string[]).length > 0 &&
|
||||||
|
(recipe[property] as string[]).map((ingredient) => (
|
||||||
|
<li className="text-sm tb:text-xs">- {ingredient}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
2
src/features/sections/index.ts
Normal file
2
src/features/sections/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { SectionService, type SectionType } from "./sections";
|
||||||
|
export { SectionService, type SectionType };
|
||||||
75
src/features/sections/sections.ts
Normal file
75
src/features/sections/sections.ts
Normal file
@@ -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);
|
||||||
|
};
|
||||||
|
}
|
||||||
26
src/shared/assets/icons/check.tsx
Normal file
26
src/shared/assets/icons/check.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
export const CheckIcon = ({ className }: { className?: string }) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
||||||
|
<g
|
||||||
|
id="SVGRepo_tracerCarrier"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
></g>
|
||||||
|
<g id="SVGRepo_iconCarrier">
|
||||||
|
<path
|
||||||
|
stroke="var(--color-ac0)"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M17 5L8 15l-5-4"
|
||||||
|
></path>{" "}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
24
src/shared/assets/icons/cross.tsx
Normal file
24
src/shared/assets/icons/cross.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export const CrossIcon = ({ className }: { className?: string }) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||||
|
<g
|
||||||
|
id="SVGRepo_tracerCarrier"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
></g>
|
||||||
|
<g id="SVGRepo_iconCarrier">
|
||||||
|
<path
|
||||||
|
fill="var(--color-err)"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M16.293 17.707a1 1 0 001.414-1.414L11.414 10l6.293-6.293a1 1 0 00-1.414-1.414L10 8.586 3.707 2.293a1 1 0 00-1.414 1.414L8.586 10l-6.293 6.293a1 1 0 101.414 1.414L10 11.414l6.293 6.293z"
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
5
src/shared/assets/icons/index.ts
Normal file
5
src/shared/assets/icons/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { SunIcon } from "./sunIcon";
|
||||||
|
import { CheckIcon } from "./check";
|
||||||
|
import { CrossIcon } from "./cross";
|
||||||
|
|
||||||
|
export { SunIcon, CheckIcon, CrossIcon };
|
||||||
31
src/shared/assets/icons/sunIcon.tsx
Normal file
31
src/shared/assets/icons/sunIcon.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export const SunIcon = ({
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="800px"
|
||||||
|
height="800px"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={className}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 3V4M12 20V21M4 12H3M6.31412 6.31412L5.5 5.5M17.6859
|
||||||
|
6.31412L18.5 5.5M6.31412 17.69L5.5 18.5001M17.6859
|
||||||
|
17.69L18.5 18.5001M21 12H20M16 12C16 14.2091 14.2091
|
||||||
|
16 12 16C9.79086 16 8 14.2091 8 12C8 9.79086 9.79086
|
||||||
|
8 12 8C14.2091 8 16 9.79086 16 12Z"
|
||||||
|
stroke="var(--color-fg1)"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
93
src/shared/utils/http.ts
Normal file
93
src/shared/utils/http.ts
Normal file
@@ -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<Z extends z.ZodTypeAny>(
|
||||||
|
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<Z>;
|
||||||
|
else throw new Error(parsed.error.message);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async get<Z extends z.ZodTypeAny>(
|
||||||
|
url: string,
|
||||||
|
schema: Z,
|
||||||
|
options?: GetRequestOptions
|
||||||
|
) {
|
||||||
|
return await this.request<Z>("GET", url, schema, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async post<Z extends z.ZodTypeAny>(
|
||||||
|
url: string,
|
||||||
|
schema: Z,
|
||||||
|
options?: RequestOptions
|
||||||
|
) {
|
||||||
|
return await this.request<Z>("POST", url, schema, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async put<Z extends z.ZodType>(
|
||||||
|
url: string,
|
||||||
|
schema: Z,
|
||||||
|
options?: RequestOptions
|
||||||
|
) {
|
||||||
|
return await this.request<Z>("PUT", url, schema, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/widgets/grid/grid.tsx
Normal file
64
src/widgets/grid/grid.tsx
Normal file
@@ -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 = <U extends ItemType>({
|
||||||
|
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<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleGridScroll = async (
|
||||||
|
e: React.UIEvent<HTMLDivElement, 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 (
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-1 tb:grid-cols-2 lp:grid-cols-3 gap-3 p-2 h-full overflow-auto"
|
||||||
|
onScroll={handleGridScroll}
|
||||||
|
>
|
||||||
|
{items.length > 0 && <ItemCard item={items[0]} ref={firstItemRef} />}
|
||||||
|
{items.length > 1 &&
|
||||||
|
items
|
||||||
|
.slice(1)
|
||||||
|
.map((item, i) => (
|
||||||
|
<ItemCard item={item} key={ItemService.GetItemId(item)} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
2
src/widgets/grid/index.ts
Normal file
2
src/widgets/grid/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { Grid } from "./grid";
|
||||||
|
export { Grid };
|
||||||
59
src/widgets/header/header.tsx
Normal file
59
src/widgets/header/header.tsx
Normal file
@@ -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 (
|
||||||
|
<header className="w-full h-20 z-10 bg-bg1 sticky top-0 shadow-xl">
|
||||||
|
<div
|
||||||
|
className="w-full h-full max-w-[var(--app-width)] m-auto px-5
|
||||||
|
flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<h1 className="text-4xl font-bold flex items-center">
|
||||||
|
<div className="lp:hidden">
|
||||||
|
<MobileMenu />
|
||||||
|
</div>
|
||||||
|
<Link href="/">JellyBelly</Link>
|
||||||
|
</h1>
|
||||||
|
<div className="hidden text-2xl lp:block">
|
||||||
|
{SectionService.sections.map((section) => (
|
||||||
|
<Link
|
||||||
|
key={section}
|
||||||
|
className={clsx(
|
||||||
|
"px-5 cursor-pointer hover:underline underline-offset-2",
|
||||||
|
currentPageName === section && "underline"
|
||||||
|
)}
|
||||||
|
href={"/" + section}
|
||||||
|
>
|
||||||
|
{SectionService.sectionsConfiguration[section].sectionName}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<ColorSchemeSwitch />
|
||||||
|
|
||||||
|
{/* <label className="flex flex-col items-start relative w-36">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
className="peer/search w-full rounded-lg bg-bg4 px-2"
|
||||||
|
placeholder=" "
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="peer-focus/search:opacity-0
|
||||||
|
peer-[:not(:placeholder-shown)]/search:opacity-0
|
||||||
|
transition-opacity h-0 flex items-center relative bottom-3"
|
||||||
|
>
|
||||||
|
<SearchIcon className="w-4 h-4 mx-2" />
|
||||||
|
Поиск
|
||||||
|
</span>
|
||||||
|
</label> */}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
3
src/widgets/header/index.ts
Normal file
3
src/widgets/header/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { Header } from "./header";
|
||||||
|
|
||||||
|
export { Header };
|
||||||
50
src/widgets/header/mobileMenu/mobileMenu.tsx
Normal file
50
src/widgets/header/mobileMenu/mobileMenu.tsx
Normal file
@@ -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<boolean>(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
className="w-16 h-16 *:w-12 *:h-1 *:bg-fg1 *:my-3
|
||||||
|
*:transition-all *:duration-300 *:relative"
|
||||||
|
onClick={(e) => {
|
||||||
|
changeMenuOpen(!open);
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
onBlur={() => changeMenuOpen(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clsx(open && "rotate-45 top-4", !open && "top-0")}
|
||||||
|
></div>
|
||||||
|
<div className={clsx(open && "opacity-0")}></div>
|
||||||
|
<div
|
||||||
|
className={clsx(open && "-rotate-45 bottom-4", !open && "bottom-0")}
|
||||||
|
></div>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"h-0 absolute transition-all duration-300 overflow-hidden\
|
||||||
|
bg-bg4 rounded-lg px-4 flex flex-col shadow-xl",
|
||||||
|
open && "h-56"
|
||||||
|
)}
|
||||||
|
onClick={() => changeMenuOpen(false)}
|
||||||
|
>
|
||||||
|
{SectionService.sections.map((section) => (
|
||||||
|
<Link
|
||||||
|
key={section}
|
||||||
|
className="text-xl py-2 cursor-pointer hover:underline"
|
||||||
|
href={"/" + section}
|
||||||
|
>
|
||||||
|
{SectionService.sectionsConfiguration[section].sectionName}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ const config: Config = {
|
|||||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
@@ -13,6 +14,37 @@ const config: Config = {
|
|||||||
"gradient-conic":
|
"gradient-conic":
|
||||||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
"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: [],
|
plugins: [],
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
},
|
||||||
|
"experimentalDecorators": true
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
|
|||||||
Reference in New Issue
Block a user