mirror of
https://github.com/StepanovPlaton/jelly_belly_wiki.git
synced 2026-04-03 20:30: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} */
|
||||
const nextConfig = {};
|
||||
const nextConfig = {
|
||||
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: process.env.IMAGES_PROTOCOL,
|
||||
hostname: process.env.IMAGES_DOMAIN,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
49
package-lock.json
generated
49
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
package.json
13
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"
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
: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);
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
// suppressHydrationWarning for theme support
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
172
src/app/page.tsx
172
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 (
|
||||
<main className="flex min-h-screen flex-col items-center justify-between p-24">
|
||||
<div className="z-10 w-full max-w-5xl items-center justify-between font-mono text-sm lg:flex">
|
||||
<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">
|
||||
Get started by editing
|
||||
<code className="font-mono font-bold">src/app/page.tsx</code>
|
||||
</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">
|
||||
<a
|
||||
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
|
||||
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
|
||||
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>
|
||||
<p className="m-0 max-w-[30ch] text-sm opacity-50">
|
||||
Find in-depth information about Next.js features and API.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&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">
|
||||
Learn{" "}
|
||||
<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">
|
||||
Learn about Next.js in an interactive course with quizzes!
|
||||
</p>
|
||||
</a>
|
||||
|
||||
<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>
|
||||
</main>
|
||||
<div className="h-full overflow-auto pb-4">
|
||||
{items &&
|
||||
SectionService.sections.map((section, i) => (
|
||||
<section key={section}>
|
||||
{items[section] && (
|
||||
<>
|
||||
<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">
|
||||
{items[section].map((item) => (
|
||||
<ItemCard item={item} key={ItemService.GetItemId(item)} />
|
||||
))}
|
||||
</div>
|
||||
<div className="w-full flex justify-end p-2">
|
||||
<Link
|
||||
className="text-2xl text-fg4 cursor-pointer hover:underline"
|
||||
href={
|
||||
"/" +
|
||||
SectionService.sectionsConfiguration[section].sectionUrl
|
||||
}
|
||||
>
|
||||
{
|
||||
SectionService.sectionsConfiguration[section]
|
||||
.sectionInviteText
|
||||
}
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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/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: [],
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
Reference in New Issue
Block a user