From 516b45fad9a79c07068413f98608091cd7de790e Mon Sep 17 00:00:00 2001 From: Yevhen Odynets Date: Tue, 11 Mar 2025 02:54:09 +0200 Subject: [PATCH] stuff done --- .gitignore | 2 + .idea/dataSources.xml | 12 - .idea/sqldialects.xml | 9 - actions/admin/entity.ts | 107 + actions/admin/mailer.ts | 44 + actions/admin/order.tsx | 24 + actions/admin/place-order.ts | 77 + actions/admin/product.ts | 2 +- .../admin/category/[...slug]/page.tsx | 9 +- .../admin/entity/[...slug]/page.tsx | 23 + app/(protected)/admin/entity/page.tsx | 16 + app/(protected)/admin/order/page.tsx | 30 + .../admin/product/[...slug]/page.tsx | 16 +- app/[locale]/(root)/(shop)/about-us/page.tsx | 100 - app/[locale]/(root)/(shop)/cart/cart.tsx | 114 + app/[locale]/(root)/(shop)/cart/nova-post.tsx | 236 + app/[locale]/(root)/(shop)/cart/page.tsx | 54 +- .../(root)/(shop)/cart/post-submit.tsx | 23 + .../(root)/(shop)/pages/[slug]/page.tsx | 65 + .../(shop)/product/[[...slug]]/page.tsx | 9 +- app/[locale]/(root)/page.tsx | 18 +- app/api/nova-post/route.ts | 150 + app/globals.css | 126 +- app/layout.tsx | 41 +- .../(protected)/admin/entity/crud-form.tsx | 347 + components/(protected)/admin/sidebar.tsx | 16 +- components/cabinet/index.tsx | 25 +- components/pages/cart/cart.module.scss | 50 +- components/pages/cart/items.tsx | 64 +- .../pages/cart/registered-order-form.tsx | 290 + components/pages/cart/search-address.tsx | 152 + components/pages/product/index.tsx | 27 +- components/shared/above/index.tsx | 2 +- components/shared/footer.tsx | 26 +- components/shared/header/controls.tsx | 4 +- .../shared/header/shopping-cart-icon.tsx | 23 +- components/shared/home/logo.tsx | 13 +- components/shared/locale-switcher.tsx | 4 +- components/shared/navbar/navbar-menu.tsx | 6 +- .../shared/sidebar/app-catalog-render.tsx | 6 +- components/shared/store/card-buy-button.tsx | 2 +- .../shared/store/feature-card-front.tsx | 22 +- components/shared/store/feature-card.tsx | 13 +- components/shared/store/stars.tsx | 13 +- components/shared/terms/index.tsx | 55 + components/shared/youtube-component.tsx | 28 + components/ui/accordion.tsx | 57 + components/ui/command.tsx | 153 + components/ui/popover.tsx | 33 + components/ui/radio-group.tsx | 44 + lib/config/editor.ts | 8 +- lib/db/prisma/schema/entity.prisma | 32 + lib/db/prisma/schema/enum.prisma | 12 + lib/db/prisma/schema/meta.prisma | 1 + lib/db/prisma/schema/order.prisma | 26 + lib/db/prisma/schema/user.prisma | 1 + lib/nova-post-helper.ts | 88 + lib/schemas/admin/category.ts | 2 - lib/schemas/admin/entity.ts | 43 + lib/schemas/admin/order.ts | 38 + lib/utils.ts | 38 + messages/ru.json | 14 +- messages/uk.json | 21 +- next.config.ts | 26 +- package-lock.json | 9089 ----------------- package.json | 63 +- public/fonts/myriad-black.woff2 | Bin 0 -> 30920 bytes public/fonts/myriad-blackit.woff2 | Bin 0 -> 33552 bytes public/fonts/myriad-bold.otf | Bin 0 -> 95684 bytes public/fonts/myriad-bold.woff2 | Bin 0 -> 30856 bytes public/fonts/myriad-boldcond.otf | Bin 0 -> 94628 bytes public/fonts/myriad-boldcondit.otf | Bin 0 -> 100312 bytes public/fonts/myriad-boldit.otf | Bin 0 -> 100252 bytes public/fonts/myriad-boldit.woff2 | Bin 0 -> 33072 bytes public/fonts/myriad-cond.otf | Bin 0 -> 91964 bytes public/fonts/myriad-condit.otf | Bin 0 -> 97888 bytes public/fonts/myriad-it.woff2 | Bin 0 -> 33092 bytes public/fonts/myriad-light.otf | Bin 0 -> 94260 bytes public/fonts/myriad-light.woff2 | Bin 0 -> 30752 bytes public/fonts/myriad-lightit.woff2 | Bin 0 -> 32760 bytes public/fonts/myriad-regular.otf | Bin 0 -> 94360 bytes public/fonts/myriad-regular.woff2 | Bin 0 -> 30876 bytes public/fonts/myriad-semibold.otf | Bin 0 -> 95616 bytes public/fonts/myriad-semibold.woff2 | Bin 0 -> 31100 bytes public/fonts/myriad-semiboldit.otf | Bin 0 -> 100184 bytes public/fonts/myriad-semiboldit.woff2 | Bin 0 -> 33284 bytes public/images/empty-cart.svg | 41 + store/cart-store.ts | 5 + sv.js | 21 - tailwind.config.ts | 57 +- 90 files changed, 2950 insertions(+), 9458 deletions(-) delete mode 100644 .idea/dataSources.xml delete mode 100644 .idea/sqldialects.xml create mode 100644 actions/admin/entity.ts create mode 100644 actions/admin/mailer.ts create mode 100644 actions/admin/order.tsx create mode 100644 actions/admin/place-order.ts create mode 100644 app/(protected)/admin/entity/[...slug]/page.tsx create mode 100644 app/(protected)/admin/entity/page.tsx create mode 100644 app/(protected)/admin/order/page.tsx delete mode 100644 app/[locale]/(root)/(shop)/about-us/page.tsx create mode 100644 app/[locale]/(root)/(shop)/cart/cart.tsx create mode 100644 app/[locale]/(root)/(shop)/cart/nova-post.tsx create mode 100644 app/[locale]/(root)/(shop)/cart/post-submit.tsx create mode 100644 app/[locale]/(root)/(shop)/pages/[slug]/page.tsx create mode 100644 app/api/nova-post/route.ts create mode 100644 components/(protected)/admin/entity/crud-form.tsx create mode 100644 components/pages/cart/registered-order-form.tsx create mode 100644 components/pages/cart/search-address.tsx create mode 100644 components/shared/terms/index.tsx create mode 100644 components/shared/youtube-component.tsx create mode 100644 components/ui/accordion.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/popover.tsx create mode 100644 components/ui/radio-group.tsx create mode 100644 lib/db/prisma/schema/entity.prisma create mode 100644 lib/db/prisma/schema/order.prisma create mode 100644 lib/nova-post-helper.ts create mode 100644 lib/schemas/admin/entity.ts create mode 100644 lib/schemas/admin/order.ts delete mode 100644 package-lock.json create mode 100644 public/fonts/myriad-black.woff2 create mode 100644 public/fonts/myriad-blackit.woff2 create mode 100644 public/fonts/myriad-bold.otf create mode 100644 public/fonts/myriad-bold.woff2 create mode 100644 public/fonts/myriad-boldcond.otf create mode 100644 public/fonts/myriad-boldcondit.otf create mode 100644 public/fonts/myriad-boldit.otf create mode 100644 public/fonts/myriad-boldit.woff2 create mode 100644 public/fonts/myriad-cond.otf create mode 100644 public/fonts/myriad-condit.otf create mode 100644 public/fonts/myriad-it.woff2 create mode 100644 public/fonts/myriad-light.otf create mode 100644 public/fonts/myriad-light.woff2 create mode 100644 public/fonts/myriad-lightit.woff2 create mode 100644 public/fonts/myriad-regular.otf create mode 100644 public/fonts/myriad-regular.woff2 create mode 100644 public/fonts/myriad-semibold.otf create mode 100644 public/fonts/myriad-semibold.woff2 create mode 100644 public/fonts/myriad-semiboldit.otf create mode 100644 public/fonts/myriad-semiboldit.woff2 create mode 100644 public/images/empty-cart.svg delete mode 100644 sv.js diff --git a/.gitignore b/.gitignore index 7e6ed1a..7072c60 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,5 @@ dist /messages/*.d.json.ts /public/uploads/ /public/main-fallback.jpg +/sv.js +/package-lock.json diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml deleted file mode 100644 index 0a5407a..0000000 --- a/.idea/dataSources.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - mysql.8 - true - com.mysql.cj.jdbc.Driver - jdbc:mysql://10.14.88.14:3306 - $ProjectFileDir$ - - - \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml deleted file mode 100644 index 9120d8f..0000000 --- a/.idea/sqldialects.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/actions/admin/entity.ts b/actions/admin/entity.ts new file mode 100644 index 0000000..5efb403 --- /dev/null +++ b/actions/admin/entity.ts @@ -0,0 +1,107 @@ +'use server' + +import {EntityLocale, EntityType, Meta} from '@prisma/client' +import {z} from 'zod' + +import {i18nLocalesCodes} from '@/i18n-config' +import {STORE_ID} from '@/lib/config/constants' +import {db} from '@/lib/db/prisma/client' +import {createEntityFormSchema} from '@/lib/schemas/admin/entity' +import { + cleanEmptyParams, + dbErrorHandling, + slug as slugger, + toEmptyParams +} from '@/lib/utils' + +export const onEntityCreateEditAction = async ( + formData: z.infer +) => { + const validatedData = createEntityFormSchema.parse(formData) + + if (!validatedData) return {error: 'Недійсні вхідні дані'} + + if (validatedData.locales.length < i18nLocalesCodes.length) { + return {error: 'Заповніть всі мови'} + } + + const {published, media, type, slug, scopes} = validatedData + + const meta: Meta[] = [] + + for (const i in validatedData.meta) { + const normalizedMeta: any = cleanEmptyParams(validatedData.meta[i]) + + meta.push(normalizedMeta) + } + + const locales: EntityLocale[] = [] + + for (const i in validatedData.locales) { + const locale = validatedData.locales[i] + const {title, lang} = locale + const slug = slugger(title, lang) + + //const result = await getProductBySlug({slug, lang}) + const result = null + + if (!result) { + const normalized: any = cleanEmptyParams({slug, ...locale}) + + locales.push(normalized) + } else { + return {error: `Сутність з такою назвою ${title} вже існує`} + } + } + + try { + const newEntity = await db.entity.create({ + data: { + published, + scopes: scopes ? JSON.parse(scopes) : null, + type: type as EntityType, + slug: slug || null, + media: media || null, + locales: { + create: locales + } + } + }) + + return {success: JSON.stringify(newEntity, null, 2)} + } catch (error) { + return dbErrorHandling(error) + } +} + +export const getBlockEntity = async (scope: string) => { + return db.entity.findMany({ + where: { + published: true, + storeId: STORE_ID, + type: 'block', + scopes: { + array_contains: [scope] + } + }, + include: { + locales: true + }, + orderBy: { + position: 'asc' + } + }) +} +export const getPageEntityBySlug = async (slug: string) => { + return db.entity.findFirst({ + where: { + published: true, + storeId: STORE_ID, + type: 'page', + slug + }, + include: { + locales: true + } + }) +} diff --git a/actions/admin/mailer.ts b/actions/admin/mailer.ts new file mode 100644 index 0000000..4528fd4 --- /dev/null +++ b/actions/admin/mailer.ts @@ -0,0 +1,44 @@ +'use server' + +import nodemailer from 'nodemailer' + +const transporter = nodemailer.createTransport({ + host: 'smtp.gmail.com', + port: 465, + secure: true, + auth: { + user: 'vista@ugmail.org', + pass: 'hqhowacppifsefxl' + } +}) + +type SendMailProps = { + email: string + subject: string + text: string + html: string +} + +export async function sendMail({email, subject, text, html}: SendMailProps) { + try { + const info = await transporter.sendMail({ + from: `"BeWell" `, + to: email, + bcc: [ + 'yevhen.odynets@gmail.com', + 'shopping@amok.space', + { + name: 'Actus Septem', + address: 'actus.septem@ukr.net' + } + ], + subject, + text, + html + }) + + return {ok: true, messageId: info.messageId} + } catch (e) { + return {ok: false, message: JSON.stringify(e)} + } +} diff --git a/actions/admin/order.tsx b/actions/admin/order.tsx new file mode 100644 index 0000000..b2d6ecd --- /dev/null +++ b/actions/admin/order.tsx @@ -0,0 +1,24 @@ +import {db} from '@/lib/db/prisma/client' + +export const getAllOrders = async () => { + return db.order.findMany({ + orderBy: [ + { + createdAt: 'desc' + } + ] + }) +} + +export const getOrdersByUserId = async (userId: number) => { + return db.order.findMany({ + where: { + userId + }, + orderBy: [ + { + createdAt: 'desc' + } + ] + }) +} diff --git a/actions/admin/place-order.ts b/actions/admin/place-order.ts new file mode 100644 index 0000000..70af4b6 --- /dev/null +++ b/actions/admin/place-order.ts @@ -0,0 +1,77 @@ +'use server' + +import {DeliveryOption, Lang, Order} from '@prisma/client' +import {z} from 'zod' + +import {sendMail} from '@/actions/admin/mailer' +import {STORE_ID} from '@/lib/config/constants' +import dayjs from '@/lib/config/dayjs' +import {db} from '@/lib/db/prisma/client' +import {createOrderFormSchema} from '@/lib/schemas/admin/order' +import {dbErrorHandling} from '@/lib/utils' + +const generateOrderNo = (): string => { + const hex = Math.floor(Math.random() * 16777215) + .toString(16) + .slice(0, 3) + .toUpperCase() + + return `${dayjs().format('YYMM')}-${hex}` +} + +export const onPlacingOrder = async ( + formData: z.infer +) => { + const fields = createOrderFormSchema.parse(formData) + if (!fields) return {error: 'Недійсні вхідні дані'} + + const orderNo = generateOrderNo() + + try { + const newOrder: Order = await db.order.create({ + data: { + storeId: STORE_ID, + lang: fields.lang as Lang, + orderNo, + isQuick: fields.is_quick, + userId: fields.user_id ? parseInt(fields.user_id) : null, + firstName: fields.first_name, + surname: fields.surname, + deliveryOption: fields.delivery_option as DeliveryOption, + phone: fields.phone, + email: fields.email, + address: fields.address.length > 10 ? JSON.parse(fields.address) : null, + notes: fields.notes?.toString().trim() !== '' ? fields.notes : null, + details: fields.details ? JSON.parse(fields.details) : null + } + }) + + const text = JSON.stringify(newOrder, null, 2) + + const result = await sendMail({ + email: `${newOrder.firstName} ${newOrder.surname} <${newOrder.email as string}>`, + subject: `Замовлення № ${orderNo}`, + text, + html: `
${text}
` + }) + + const updated = await db.order.update({ + where: { + id: newOrder.id + }, + data: { + emailSent: result.ok + } + }) + + if (result.ok) { + return {success: newOrder.orderNo} + } else { + return { + error: result.message + } + } + } catch (error) { + return dbErrorHandling(error) + } +} diff --git a/actions/admin/product.ts b/actions/admin/product.ts index 0633d0c..3e193e0 100644 --- a/actions/admin/product.ts +++ b/actions/admin/product.ts @@ -75,7 +75,7 @@ export const onProductCreateAction = async ( } }) - return {success: 'JSON.stringify(newProduct, null, 2)'} + return {success: JSON.stringify(newProduct, null, 2)} } catch (error) { return dbErrorHandling(error) } diff --git a/app/(protected)/admin/category/[...slug]/page.tsx b/app/(protected)/admin/category/[...slug]/page.tsx index 2edf96a..f37b207 100644 --- a/app/(protected)/admin/category/[...slug]/page.tsx +++ b/app/(protected)/admin/category/[...slug]/page.tsx @@ -1,5 +1,7 @@ import {auth} from '@/auth' +import AdminPermission from '@/components/(protected)/admin/auth/permission' import {CreateForm} from '@/components/(protected)/admin/category/create-form' +import ProductCreateEditForm from '@/components/(protected)/admin/product/create-edit-form' import {dump} from '@/lib/utils' export default async function Page({ @@ -12,7 +14,12 @@ export default async function Page({ switch ((slug || [])[0]) { case 'create': - return + return ( + <> + + + + ) } return
{dump(slug)}
diff --git a/app/(protected)/admin/entity/[...slug]/page.tsx b/app/(protected)/admin/entity/[...slug]/page.tsx new file mode 100644 index 0000000..44f6360 --- /dev/null +++ b/app/(protected)/admin/entity/[...slug]/page.tsx @@ -0,0 +1,23 @@ +import AdminPermission from '@/components/(protected)/admin/auth/permission' +import {EntityCrudForm} from '@/components/(protected)/admin/entity/crud-form' +import {dump} from '@/lib/utils' + +export default async function Page({ + params +}: { + params: Promise<{slug?: string[]}> +}) { + const {slug} = await params + + switch ((slug || [])[0]) { + case 'create': + return ( + <> + + + + ) + } + + return
{dump(slug)}
+} diff --git a/app/(protected)/admin/entity/page.tsx b/app/(protected)/admin/entity/page.tsx new file mode 100644 index 0000000..3b72c83 --- /dev/null +++ b/app/(protected)/admin/entity/page.tsx @@ -0,0 +1,16 @@ +import Link from 'next/link' + +import AdminPermission from '@/components/(protected)/admin/auth/permission' + +export default function AdminEntityPage() { + return ( +
+ +

+ + Створити блок / статтю / сторінку + +

+
+ ) +} diff --git a/app/(protected)/admin/order/page.tsx b/app/(protected)/admin/order/page.tsx new file mode 100644 index 0000000..e0304a7 --- /dev/null +++ b/app/(protected)/admin/order/page.tsx @@ -0,0 +1,30 @@ +import {Order} from '@prisma/client' +import Link from 'next/link' + +import {getAllOrders} from '@/actions/admin/order' +import AdminPermission from '@/components/(protected)/admin/auth/permission' +import {dump} from '@/lib/utils' + +export default async function AdminOrderPage() { + const orders = await getAllOrders() + + return ( +
+ + {orders.map((order: Order) => ( +
+
{order.orderNo}
+
+ {order.firstName} {order.surname} +
+
{order.phone}
+
{order.email}
+
{order.notes}
+
+ ))} +
+ ) +} diff --git a/app/(protected)/admin/product/[...slug]/page.tsx b/app/(protected)/admin/product/[...slug]/page.tsx index 5c8ad0f..093ac55 100644 --- a/app/(protected)/admin/product/[...slug]/page.tsx +++ b/app/(protected)/admin/product/[...slug]/page.tsx @@ -1,3 +1,5 @@ +import AdminPermission from '@/components/(protected)/admin/auth/permission' +import {EntityCrudForm} from '@/components/(protected)/admin/entity/crud-form' import ProductCreateEditForm from '@/components/(protected)/admin/product/create-edit-form' import {getProductById} from '@/lib/data/models/product' import {dump} from '@/lib/utils' @@ -21,9 +23,19 @@ export default async function Page({ switch (method) { case 'create': - return + return ( + <> + + + + ) case 'update': - return + return ( + <> + + + + ) default: return
{dump(slug)}
} diff --git a/app/[locale]/(root)/(shop)/about-us/page.tsx b/app/[locale]/(root)/(shop)/about-us/page.tsx deleted file mode 100644 index b37bd99..0000000 --- a/app/[locale]/(root)/(shop)/about-us/page.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react' - -export default function AboutUsPage() { - return ( -
-
-

- Bewell: здоровий спосіб життя для всіх -

-
- Інтернет-магазин біологічних добавок Bewell — це - зручна і надійна крамниця для всіх, хто піклується про здоров’я. У нас - ви знайдете якісні дієтичні добавки від європейських виробників і - зможете подбати про себе і про рідних без зайвих клопотів. -
- -

- Асортимент магазину Bewell -

-

- У нас є все, що потрібно для профілактики різноманітних захворювань та - підтримки здоров’я. Це комплексні препарати, у складі яких - переважають: -

-
    -
  • екстракти рослин;
  • -
  • мікро- та макроелементи;
  • -
  • вітаміни.
  • -
-

- Усі ці компоненти необхідно споживати щоденно, щоб отримати добову - норму вітамінів, мікро- та макроелементів. Дієтичні добавки — просте - доповнення до харчування, яке забезпечує організм необхідними - поживними речовинами. -

-

- Щоб визначитися, яка з добавок підійде саме вам, пропонуємо зануритися - в наш каталог і вибрати відповідний розділ: -

-
    -
  • - Комплексні препарати. Універсальні добавки для поліпшення здоров’я. - Це вітаміни та мікроелементи для волосся, шкіри, підтримки - імунітету, серця, нервової системи, травлення тощо. -
  • -
  • - Добавки для підтримки жіночого здоров’я ( - вітаміни для жінок). Це зокрема препарати для - відновлення менструального циклу, для полегшення симптомів - менопаузи. -
  • -
  • - Добавки для підтримки здоров’я чоловіків ( - вітаміни для чоловіків). Препарати для покращення - статевої функції, здоров’я передміхурової залози. -
  • -
  • - Препарати для зміцнення імунітету. Такі добавки корисні не лише для - відновлення після захворювання та лікування, а також для - профілактики захворювань. -
  • -
  • - Добавки для відновлення енергії. Ці засоби допомагають боротися з - втомою, покращують обмін речовин, допомагають почуватися енергійніше - та краще спати.{' '} -
  • -
-

- Як замовити якісні дієтичні добавки? -

-

- В Bewell можна замовити продукцію відомих - європейських виробників. Дієтичні добавки не є лікарськими засобами, - але це хороша профілактика захворювань та підтримки здоров’я. - Препарати, представлені в нашому магазині, можна побачити в - асортименті аптек, адже це перевірена продукція, яка успішно - використовується на лише в Україні, а й в Європі. Щоб зробити - замовлення, виберіть потрібний препарат, додайте до кошика та зазначте - умови відправки та оплати. -

-

- Bewell: наша філософія та принцип роботи -

-

- Головний пріоритет Bewell — підтримка здорового - способу життя. Ми віримо, що ключ до гарного самопочуття та довголіття - можна знайти в природі, збалансованому харчуванні та усвідомлений - підтримці організму. Саме тому ми прагнемо допомогти кожному клієнту - знайти найкращі добавки для підтримки організму та профілактики - захворювань. -

-

- Ми дбаємо про чесність і прозорість — пропонуємо лише сертифіковані, - перевірені добавки, які сприяють зміцненню імунітету, відновленню - енергії та покращенню сну, а також — внутрішній гармонії. -

-
-
- ) -} diff --git a/app/[locale]/(root)/(shop)/cart/cart.tsx b/app/[locale]/(root)/(shop)/cart/cart.tsx new file mode 100644 index 0000000..e8c2c5e --- /dev/null +++ b/app/[locale]/(root)/(shop)/cart/cart.tsx @@ -0,0 +1,114 @@ +'use client' + +import {Trash2} from 'lucide-react' +import {useTranslations} from 'next-intl' +import Image from 'next/image' +import {useState} from 'react' + +import CartPostSubmit from '@/app/[locale]/(root)/(shop)/cart/post-submit' +import styles from '@/components/pages/cart/cart.module.scss' +import CartItems from '@/components/pages/cart/items' +import RegisteredOrderForm from '@/components/pages/cart/registered-order-form' +import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs' +import {Link} from '@/i18n/routing' +import {dump} from '@/lib/utils' +import EmptyCartImage from '@/public/images/empty-cart.svg' +import useCartStore from '@/store/cart-store' +import {SessionUser} from '@/types/auth' +import {Button} from '@/ui/button' + +export default function Cart({user}: {user?: SessionUser | null}) { + const t = useTranslations('cart') + const [submitResult, setSubmitResult] = useState({}) + const {cartItems, clearCart} = useCartStore() + const totalSum = cartItems.reduce( + (total, product) => total + parseFloat(product.price) * product.quantity, + 0 + ) + + const resultSubmit = (result: any) => { + setSubmitResult(result) + } + + return ( +
+
+
+ {cartItems && cartItems.length > 0 ? ( + <> +
+

{t('basket')}

+ +
+ +
+
{t('title')}
+
{t('quantity')}
+
{t('amount')}
+
+ + +
+
+
{t('total')}:
+
+ {totalSum.toFixed(2)} грн +
+
+ +
+

Оформлення замовлення

+ + + + Постійний клієнт + + + Швидке замовлення + + + + + + quick-order + +
+ + ) : Object.keys(submitResult).length === 0 ? ( +
+ {t('empty')} + + + +
+ ) : ( + + )} +
+
+
+ ) +} diff --git a/app/[locale]/(root)/(shop)/cart/nova-post.tsx b/app/[locale]/(root)/(shop)/cart/nova-post.tsx new file mode 100644 index 0000000..a1113c9 --- /dev/null +++ b/app/[locale]/(root)/(shop)/cart/nova-post.tsx @@ -0,0 +1,236 @@ +'use client' + +import { + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from 'cmdk' +import { + Check, + ChevronsUpDown, + MapPinCheck, + MapPinPlus, + Warehouse +} from 'lucide-react' +import {useLocale, useTranslations} from 'next-intl' +import {useState} from 'react' +import {useDebouncedCallback} from 'use-debounce' + +import { + type Settlement, + type Warehouse as WarehouseType, + formatSettlement, + getApi +} from '@/lib/nova-post-helper' +import {cn} from '@/lib/utils' +import {dump} from '@/lib/utils' +import {Button} from '@/ui/button' +import {Command} from '@/ui/command' +import {Popover, PopoverContent, PopoverTrigger} from '@/ui/popover' + +const url = '/api/nova-post' + +export default function NovaPost({onSelectHandler}: {onSelectHandler: any}) { + const t = useTranslations('cart.post') + const [citiesOpen, setCitiesOpen] = useState(false) + const [warehousesOpen, setWarehousesOpen] = useState(false) + const [citiesValue, setCitiesValue] = useState('') + const [cityRef, setCityRef] = useState('') + //const [warehouseRef, setWarehouseRef] = useState('') + const [warehousesValue, setWarehousesValue] = useState('') + const [cities, setCities] = useState([]) + const [warehouses, setWarehouses] = useState([]) + const locale = useLocale() + + const handleCitySearch = useDebouncedCallback( + async (e: string): Promise => { + if (e.length < 3) { + setCities([]) + return + } + + const response = await getApi(url + `?scope=cities&q=` + encodeURI(e)) + + if (response.ok) { + let json = await response.json() + setCities(json) + } else { + setCities([]) + } + }, + 1000 + ) + + const handleWarehouseSearch = async (e: string): Promise => { + const response = await getApi(`${url}?scope=warehouses&q=${e}`) + + if (response.ok) { + let json = await response.json() + setWarehouses(json) + } else { + setWarehouses([]) + } + } + + const cityDescription = (citiesValue: string): string => { + const city: Settlement | undefined = cities.find( + (city: Settlement) => city.Description === citiesValue + ) + + if (!city) { + return '' + } + + return formatSettlement(city, locale) + } + + return ( +
+
+ + + + + + + handleCitySearch(e)} + /> + + {t('notFount')} + + {cities.map((city: Settlement) => ( + { + setCitiesValue( + currentValue === citiesValue ? '' : currentValue + ) + setCityRef( + currentValue === citiesValue ? '' : city?.Ref + ) + + handleWarehouseSearch( + currentValue === citiesValue ? '' : city?.Ref + ).then(console.log) + + setCitiesOpen(false) + }} + > + {formatSettlement(city, locale)} + + + ))} + + + + + +
+ {cityRef !== '' && ( +
+ + + + + + + handleCitySearch(e)}*/ + /> + + {t('notFount')} + + {warehouses.map((warehouse: WarehouseType) => ( + { + setWarehousesValue( + currentValue === warehousesValue ? '' : currentValue + ) + /*setWarehouseRef( + currentValue === warehousesValue + ? '' + : warehouse.Ref + )*/ + onSelectHandler( + currentValue === warehousesValue + ? {} + : { + Ref: warehouse.Ref, + Description: warehouse.Description, + DescriptionRu: warehouse.DescriptionRu + } + ) + setWarehousesOpen(false) + }} + > + {warehouse.Description} + + + ))} + + + + + +
+ )} +
+ ) +} diff --git a/app/[locale]/(root)/(shop)/cart/page.tsx b/app/[locale]/(root)/(shop)/cart/page.tsx index 5d39718..1fc68c6 100644 --- a/app/[locale]/(root)/(shop)/cart/page.tsx +++ b/app/[locale]/(root)/(shop)/cart/page.tsx @@ -1,46 +1,18 @@ -'use client' +import Cart from '@/app/[locale]/(root)/(shop)/cart/cart' +import {auth} from '@/auth' +import {SessionUser} from '@/types/auth' -import {useTranslations} from 'next-intl' +export default async function Page() { + const session = await auth() + if (!session) { + return + } -import CartItems from '@/components/pages/cart/items' -import useCartStore from '@/store/cart-store' + const {user} = session -export default function Cart() { - const t = useTranslations('cart') - const {cartItems} = useCartStore() - const totalSum = cartItems.reduce( - (total, product) => total + parseFloat(product.price) * product.quantity, - 0 - ) - - // const subtotal = items.reduce( - // (total, item) => total + item.price * item.quantity, - // 0 - // ) - // const total = subtotal - - return ( -
-
-
-

- {t('basket')} -

-
-
{t('title')}
-
{t('quantity')}
-
{t('amount')}
-
- -
-
-
{t('total')}:
-
- {totalSum.toFixed(2)} грн -
-
-
-
-
+ return session ? ( + + ) : ( + ) } diff --git a/app/[locale]/(root)/(shop)/cart/post-submit.tsx b/app/[locale]/(root)/(shop)/cart/post-submit.tsx new file mode 100644 index 0000000..184232c --- /dev/null +++ b/app/[locale]/(root)/(shop)/cart/post-submit.tsx @@ -0,0 +1,23 @@ +export default function CartPostSubmit({result}: any) { + if (result?.success) { + return ( +
+

+ Номер Вашого замовлення:{' '} + + {result?.success} + {' '} +

+
+ ) + } + + return ( +
+

+ Сталася помилка:{' '} + {result?.error}{' '} +

+
+ ) +} diff --git a/app/[locale]/(root)/(shop)/pages/[slug]/page.tsx b/app/[locale]/(root)/(shop)/pages/[slug]/page.tsx new file mode 100644 index 0000000..69e9e0d --- /dev/null +++ b/app/[locale]/(root)/(shop)/pages/[slug]/page.tsx @@ -0,0 +1,65 @@ +import {EntityLocale} from '@prisma/client' +import type {Metadata} from 'next' +import {notFound} from 'next/navigation' +import {Suspense} from 'react' + +import {getPageEntityBySlug} from '@/actions/admin/entity' +import YoutubeComponent from '@/components/shared/youtube-component' +import {dump, normalizeData, thisLocale} from '@/lib/utils' +import {Skeleton} from '@/ui/skeleton' + +type Props = { + params: Promise<{slug?: string}> +} + +export const generateMetadata = async ({params}: Props): Promise => { + const {slug} = await params + const page = await getPageEntityBySlug(slug || '') + if (!page) { + notFound() + } + const {locales} = page + const locale: EntityLocale = await thisLocale(locales) + const {title, annotation} = locale + return { + title, + description: normalizeData(annotation, { + stripTags: true + }) + } +} + +export default async function Pages({params}: Props) { + const {slug} = await params + const page = await getPageEntityBySlug(slug || '') + + if (!page) { + notFound() + } + + const {locales} = page + const locale: EntityLocale = await thisLocale(locales) + const {title, annotation, body} = locale + + return ( +
+
+

{title}

+
+ }> + + +
+ +
+ + {/*{dump(locale)}*/} +
+
+ ) +} diff --git a/app/[locale]/(root)/(shop)/product/[[...slug]]/page.tsx b/app/[locale]/(root)/(shop)/product/[[...slug]]/page.tsx index 1000004..0983439 100644 --- a/app/[locale]/(root)/(shop)/product/[[...slug]]/page.tsx +++ b/app/[locale]/(root)/(shop)/product/[[...slug]]/page.tsx @@ -1,6 +1,9 @@ +import {getProductByIdWitData} from '@prisma/client/sql' import {notFound} from 'next/navigation' import ProductPageIndex from '@/components/pages/product' +import {CategoryPageSqlSchema} from '@/lib/data/models/sqlSchemas' +import {db} from '@/lib/db/prisma/client' export default async function Products({ params @@ -12,5 +15,9 @@ export default async function Products({ const id = (uri || '').match(/^(\d+)-./) if (!id) notFound() - return + const data: CategoryPageSqlSchema[] = await db.$queryRawTyped( + getProductByIdWitData(id[1]) + ) + + return } diff --git a/app/[locale]/(root)/page.tsx b/app/[locale]/(root)/page.tsx index dccc29a..81f1705 100644 --- a/app/[locale]/(root)/page.tsx +++ b/app/[locale]/(root)/page.tsx @@ -1,11 +1,13 @@ import {getCatalogIndexData} from '@prisma/client/sql' import {getLocale} from 'next-intl/server' import Image from 'next/image' +import React from 'react' import FeatureCards from '@/components/shared/home/feature-cards' import {HomeCarousel} from '@/components/shared/home/home-carousel' import AppCatalog from '@/components/shared/sidebar/app-catalog' import FeatureCardFront from '@/components/shared/store/feature-card-front' +import Terms from '@/components/shared/terms' import {carousels} from '@/lib/data' import {CategoryPageSqlSchema} from '@/lib/data/models/sqlSchemas' import {db} from '@/lib/db/prisma/client' @@ -31,9 +33,9 @@ import image from '@/public/uploads/products/IMG_6572.jpg' // } export default async function HomePage() { - const loc = await getLocale() + const locale = await getLocale() const catalog: CategoryPageSqlSchema[] = await db.$queryRawTyped( - getCatalogIndexData(loc) + getCatalogIndexData(locale) ) return ( @@ -51,8 +53,18 @@ export default async function HomePage() { -
+
+
+ + + +
+

+ {locale !== 'ru' ? "Цікаво про здоров'я" : 'Интересно о здоровье'} +

+
+
{''} { + return await fetch(process.env.NOVA_POST_API_EP || '', init) +} + +async function getWarehouses(CityRef: string, Page: number = 1) { + // const branches = [] + let c = 0 + // let n = 0 + const Limit = 500 + + /*do { + const response = await fetchApi({ + method: 'POST', + body: JSON.stringify({ + apiKey: process.env.NOVA_POST_API_KEY || '', + modelName: 'AddressGeneral', + calledMethod: 'getWarehouses', + methodProperties: { + CityRef, + Page: ++c, + Limit, + FindByString: 'відд' + } + }) + }) + list = await response.json() + n = Math.ceil(list.info.totalCount / Limit) + + for (const i in list.data) { + if (list.data[i].Description.trim().match(/^відді/iu)) { + branches.push(list.data[i]) + } + } + } while (c < n) + + list.data = branches*/ + + const response = await fetchApi({ + method: 'POST', + body: JSON.stringify({ + apiKey: process.env.NOVA_POST_API_KEY || '', + modelName: 'AddressGeneral', + calledMethod: 'getWarehouses', + methodProperties: { + CityRef, + Page: ++c, + Limit, + FindByString: 'відд' + } + }) + }) + const list = await response.json() + + list.data = list.data.filter((item: Warehouse) => + item.Description.trim().match(/^відді/iu) + ) + //console.log(Math.ceil(list.info.totalCount / Limit), list.data.length) + + return list +} + +async function getWarehouse(Ref: string) { + const response = await fetch(process.env.NOVA_POST_API_EP || '', { + method: 'POST', + body: JSON.stringify({ + apiKey: process.env.NOVA_POST_API_KEY || '', + modelName: 'AddressGeneral', + calledMethod: 'getWarehouses', + methodProperties: { + Ref, + Limit: 1 + } + }) + }) + + return await response.json() +} + +async function getCities(searchString: string) { + const response = await fetch(process.env.NOVA_POST_API_EP || '', { + method: 'POST', + body: JSON.stringify({ + apiKey: process.env.NOVA_POST_API_KEY || '', + modelName: 'AddressGeneral', + calledMethod: 'getCities', + methodProperties: { + FindByString: searchString, + Limit: 500 + } + }) + }) + //console.log(searchString) + return await response.json() +} + +async function getStreet(StreetName: string) { + const response = await fetch(process.env.NOVA_POST_API_EP || '', { + method: 'POST', + body: JSON.stringify({ + apiKey: process.env.NOVA_POST_API_KEY || '', + modelName: 'AddressGeneral', + calledMethod: 'searchSettlementStreets', + methodProperties: { + SettlementRef: 'e718a680-4b33-11e4-ab6d-005056801329', + Page: 1, + Limit: 50, + StreetName + } + }) + }) + + return await response.json() +} diff --git a/app/globals.css b/app/globals.css index ac5ef2c..3f5bde6 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,10 +4,8 @@ @tailwind components; @tailwind utilities; -body { - margin: 0; - padding: 0; - overflow-y: scroll; +html, body { + @apply h-full m-0 p-0; } @layer base { @@ -115,11 +113,11 @@ body { } .bw-layout-col-left { - @apply flex-1 sm:w-7/12 md:w-5/12 xl:w-4/12 lg:flex-col + @apply flex-1 sm:w-7/12 md:w-5/12 xl:w-5/12 lg:flex-col } .bw-layout-col-right { - @apply sm:w-5/12 md:w-7/12 xl:w-8/12 flex-1 sm:flex-auto sm:pl-4 md:pl-7 xl:pl-9 + @apply sm:w-5/12 md:w-7/12 xl:w-7/12 flex-1 sm:flex-auto sm:pl-4 md:pl-7 xl:pl-9 } .bw-product-col-left{ @@ -134,7 +132,7 @@ body { } .bw-header-col-right { - @apply flex-grow-0 flex-shrink-0 md:basis-[272px] + @apply flex-grow-0 flex-shrink-0 md:basis-[142px] } .bw-border-color { @@ -146,6 +144,76 @@ body { } } +.bw-terms-section { + + .bw-accordion-item { + + @apply w-[100%] flex-none overflow-hidden border-b-0 md:w-[46.75%] lg:w-[29.25%]; /*shadow-md*/ + + /*&:hover { + @apply bg-brand-violet-800/25; + }*/ + + &[data-state="open"]{ + @apply border-brand-violet-800/25 border-[2px] rounded-lg; /*shadow-md shadow-brand-violet-900/30*/ + + .bw-accordion-content { + @apply rounded-br-lg rounded-bl-lg bg-brand-violet-50/15; /*border-r-[2px] border-b-[2px] border-l-[2px]*/ + }/*bg-brand-violet-50/50*/ + } + + + button { + &[data-state="closed"] { + @apply border-[2px] rounded-lg; /*shadow-lg border-brand-violet-900/10*/ + } + + &[data-state="open"] { + @apply border-[2px] rounded-none py-2; + } + } + } + .bw-accordion-trigger { + @apply text-center text-base text-brand-violet antialiased font-semibold font-heading; + } + + + button { + padding: 10px 16px; + text-align: center; + font-size: 1rem; + + &:hover, &[data-state="open"] { + @apply bg-brand-violet-800/25 text-white border-brand-violet-800/5; /*border-t-2*/ + } + } + svg { + @apply w-6 h-6; + } + + h3[data-state="open"], h3:hover { + & > button > svg { + @apply stroke-white; + } + } + + .bw-accordion-content { + @apply px-4 text-[15px] leading-relaxed tracking-wide text-brand-violet-900; + + p { + @apply block my-[1em] mx-0; + } + + a { + @apply font-semibold text-brand-violet-600 hover:underline; + } + + ul { + @apply block list-disc my-[1em] m-0 pl-10; + } + } +} + .bw-dd-menu { /* since nested groupes are not supported we have to use regular css for the nested dropdowns @@ -165,6 +233,40 @@ body { .group:hover .group-hover\:-rotate-180 { transform: rotate(180deg) } } +.bw-page { + @apply leading-relaxed tracking-tight mt-10; + + h1 { + @apply font-heading my-[0.67em] mx-0 text-3xl font-bold; + } + + h2 { + @apply font-heading mt-[0.83em] mb-[0.25em] mx-0 text-xl font-semibold; + } + + p { + @apply block mb-[1em] mx-0; + } + + /*a { + @apply font-semibold text-brand-violet-600 hover:underline; + }*/ + + ul { + @apply block list-disc my-[1em] m-0 pl-10; + } + + article { + @apply leading-relaxed; + } + /*display: block; + font-size: 2em; + margin-top: 0.67em; + margin-bottom: 0.67em; + margin-left: 0; + margin-right: 0; + font-weight: bold;*/ +} .jodit-wysiwyg > * { all: revert; @@ -180,6 +282,7 @@ body { }*/ } + #admin-bw-panel form { input { @apply bg-white outline-0 text-[16px] leading-none; @@ -265,3 +368,12 @@ body { flex: 1; } } + + + +/* +https://www.w3schools.com/cssref/css_default_values.php*/ + +.bw-yt-video { + @apply aspect-video w-full self-stretch md:min-h-96; +} diff --git a/app/layout.tsx b/app/layout.tsx index 0493b93..9c02e3d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,4 +1,5 @@ import type {Metadata} from 'next' +import localFont from 'next/font/local' import {headers} from 'next/headers' import {ReactNode} from 'react' import {Toaster} from 'react-hot-toast' @@ -16,6 +17,42 @@ export const metadata: Metadata = { description: APP_DESCRIPTION } +const Myriad = localFont({ + variable: '--font-myriad', + src: [ + /*{ + path: '../public/fonts/myriad-light.woff2', + weight: '300', + style: 'normal' + },*/ + { + path: '../public/fonts/myriad-regular.woff2', + weight: '400', + style: 'normal' + }, + /*{ + path: '../public/fonts/myriad-it.woff2', + weight: '400', + style: 'italic' + },*/ + { + path: '../public/fonts/myriad-semibold.woff2', + weight: '600', + style: 'normal' + }, + { + path: '../public/fonts/myriad-bold.woff2', + weight: '700', + style: 'normal' + } + /*{ + path: '../public/fonts/myriad-boldit.woff2', + weight: '700', + style: 'italic' + }*/ + ] +}) + export default async function RootLayout({ children }: Readonly<{children: ReactNode}>) { @@ -23,8 +60,8 @@ export default async function RootLayout({ const locale = headersList.get('x-site-locale') ?? routing.defaultLocale return ( - - + + {children} {/**/} diff --git a/components/(protected)/admin/entity/crud-form.tsx b/components/(protected)/admin/entity/crud-form.tsx new file mode 100644 index 0000000..693192b --- /dev/null +++ b/components/(protected)/admin/entity/crud-form.tsx @@ -0,0 +1,347 @@ +'use client' + +import {zodResolver} from '@hookform/resolvers/zod' +import {EntityType} from '@prisma/client' +import dynamic from 'next/dynamic' +import React, {Suspense, useEffect, useMemo, useRef, useState} from 'react' +import {useFieldArray, useForm} from 'react-hook-form' +import toast from 'react-hot-toast' +import {z} from 'zod' + +import {onEntityCreateEditAction} from '@/actions/admin/entity' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select' +import {i18nDefaultLocale, i18nLocales} from '@/i18n-config' +import {BaseEditorConfig} from '@/lib/config/editor' +import { + EntityTypeDescription, + createEntityFormSchema +} from '@/lib/schemas/admin/entity' +import {toEmptyParams} from '@/lib/utils' +import {Button} from '@/ui/button' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/ui/form' +import {Input} from '@/ui/input' +import {Switch} from '@/ui/switch' +import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/ui/tabs' + +const JoditEditor = dynamic(() => import('jodit-react'), {ssr: false}) + +let localesValues = { + type: '', + media: '', + title: '', + annotation: '', + body: '' +} + +let metaValues = { + title: '', + description: '', + keywords: '', + author: '' +} + +export const EntityCrudForm = ({data}: {data?: any}) => { + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + const [success, setSuccess] = useState('') + const [annotation0, setAnnotation0] = useState( + data?.locales[0].annotation || '' + ) + const [annotation1, setAnnotation1] = useState( + data?.locales[1].annotation || '' + ) + const [body0, setBody0] = useState(data?.locales[0].body || '') + const [body1, setBody1] = useState(data?.locales[1].body || '') + + const editor = useRef(null) //declared a null value + const config = useMemo(() => BaseEditorConfig, []) + config.maxWidth = '100%' + + const form = useForm>({ + resolver: zodResolver(createEntityFormSchema), + mode: 'onBlur', + defaultValues: data + ? (data => { + const {locales, meta} = data + + return { + published: data.published, + image: data.image, + locales: toEmptyParams(locales) as any, + meta: meta + ? (toEmptyParams(meta) as any) + : [{...metaValues}, {...metaValues}] + } + })(data) + : { + scopes: '', + published: false, + media: '', + slug: '', + locales: [ + {lang: 'uk', ...localesValues}, + {lang: 'ru', ...localesValues} + ], + meta: [{...metaValues}, {...metaValues}] + } + }) + + const {register, setValue} = form + + useEffect(() => { + register('locales.0.annotation') + register('locales.1.annotation') + register('locales.0.body') + register('locales.1.body') + }, [register]) + + const {fields: localeFields} = useFieldArray({ + name: 'locales', + control: form.control + }) + + const {fields: metaFields} = useFieldArray({ + name: 'meta', + control: form.control + }) + + console.log(form.formState.errors) + + const onSubmit = async (values: z.infer) => { + setLoading(true) + onEntityCreateEditAction(values).then((res: any) => { + if (res?.error) { + setError(res?.error) + setSuccess('') + setLoading(false) + toast.error(res?.error) + } else { + setSuccess(res?.success as string) + setError('') + setLoading(false) + toast.success(res?.success) + } + }) + } + + return ( +
+ +
+

+ Створити блок / статтю / сторінку +

+
+ ( + +
+ Опублікувати + + Відразу після збереження буде розміщено на сайті + +
+ + + +
+ )} + /> +
+
+
+ ( + + Тип сутності + + + + )} + /> +
+
+ ( + + Аліас / Slug + + + + + + )} + /> +
+
+ ( + + Область виведення + + + + + + )} + /> +
+
+
+
+ ( + + + Медіа (файл на диску чи URL посилання на ресурс) + + + + + + + )} + /> +
+ + + {i18nLocales.map(locale => ( + + {locale.nameUkr} + + ))} + + {localeFields.map((_, index) => ( + + ( + + + + + + + )} + /> +
+ ( + + Назва сутності + + + + + + )} + /> +
+
+ Анотація / Коротка назва + { + index === 0 ? setAnnotation0(value) : setAnnotation1(value) + setValue(`locales.${index}.annotation`, value) + }} + /> +
+
+ Текст + { + index === 0 ? setBody0(value) : setBody1(value) + setValue(`locales.${index}.body`, value) + }} + /> +
+
+ ))} +
+ +
+ + ) +} diff --git a/components/(protected)/admin/sidebar.tsx b/components/(protected)/admin/sidebar.tsx index 1aa860d..4fc5f12 100644 --- a/components/(protected)/admin/sidebar.tsx +++ b/components/(protected)/admin/sidebar.tsx @@ -3,6 +3,8 @@ import { Home, Inbox, LayoutList, + List, + Newspaper, Plus, ScanBarcode, Search, @@ -44,6 +46,16 @@ const items = [ title: 'Товари', url: `${ADMIN_DASHBOARD_PATH}/product`, icon: ScanBarcode + }, + { + title: 'Сутність', + url: `${ADMIN_DASHBOARD_PATH}/entity`, + icon: Newspaper + }, + { + title: 'Замовлення', + url: `${ADMIN_DASHBOARD_PATH}/order`, + icon: List } // { // title: 'Search', @@ -70,7 +82,7 @@ export function AdminSidebar() { SidebarGroupAction - Application + {/*Application*/} {items.map(item => ( @@ -86,7 +98,7 @@ export function AdminSidebar() { - +
diff --git a/components/pages/cart/cart.module.scss b/components/pages/cart/cart.module.scss index ba24743..b4e3761 100644 --- a/components/pages/cart/cart.module.scss +++ b/components/pages/cart/cart.module.scss @@ -1,4 +1,48 @@ -input.bw-cart-item-counter{ - font-size: 36px; - background: chocolate; +.bwOrderForm { + & > h2, & > h3 { + @apply text-center text-brand-violet text-2xl; + } + + [role="tablist"] { + @apply bg-transparent rounded-none shadow-none m-0 pb-5 border-b-2 border-brand-violet h-[unset]; + + button { + @apply justify-start rounded-none font-normal text-xl pl-0; + } + + [data-state=active] { + @apply text-brand-violet shadow-none; + } + [data-state=inactive] { + @apply text-gray-600 ; + } + } +} + +.registeredForm{ + /*@apply bg-brand-yellow-100;*/ + + & > h2, & > h3 { + @apply text-brand-violet text-2xl mt-9 mb-2; + } + + fieldset { + @apply md:flex md:items-start md:justify-between md:gap-8 + } + + label { + @apply text-lg font-normal block mt-8 leading-none; + } + + input { + @apply border-t-0 border-r-0 border-l-0 border-stone-400 rounded-none text-foreground text-lg p-0; + } + + textarea { + @apply min-h-[72px] w-full border-b border-stone-400 p-2 ; + } + + [role="combobox"] { + @apply w-full text-lg pl-0 text-foreground border-t-0 h-[unset] pb-2 border-r-0 border-l-0 rounded-none shadow-none border-stone-400; + } } diff --git a/components/pages/cart/items.tsx b/components/pages/cart/items.tsx index cc16f2a..953fcf4 100644 --- a/components/pages/cart/items.tsx +++ b/components/pages/cart/items.tsx @@ -1,16 +1,14 @@ // import styles from '@/components/pages/cart/cart.module.scss' -import {Minus, Plus} from 'lucide-react' +import {Minus, Plus, X} from 'lucide-react' import Image from 'next/image' -import {Link} from '@/i18n/routing' import useCartStore, {CartItem} from '@/store/cart-store' import {Button} from '@/ui/button' -export default function CartItems() { - const {cartItems} = useCartStore() - +export default function CartItems({cartItems}: {cartItems: CartItem[]}) { const {increaseQuantity, decreaseQuantity, removeItemFromCart} = useCartStore() + const onIncreaseQuantity = (productId: number) => { increaseQuantity(productId) } @@ -23,22 +21,33 @@ export default function CartItems() { removeItemFromCart(productId) } - if (cartItems && cartItems.length > 0) { - return ( - <> - {cartItems?.map((item: CartItem, i: number) => ( -
+ return ( + <> + {cartItems?.map((item: CartItem, i: number) => ( +
+

+
{item.title}
+
+ +
+

+
- {item.title} @@ -47,17 +56,17 @@ export default function CartItems() {
-
+
{item.quantity}
- ))} - - ) - } - return ( -
-

Cart is Empty

- - Продовжити покупки - -
+
+ ))} + ) } diff --git a/components/pages/cart/registered-order-form.tsx b/components/pages/cart/registered-order-form.tsx new file mode 100644 index 0000000..2bfcc7c --- /dev/null +++ b/components/pages/cart/registered-order-form.tsx @@ -0,0 +1,290 @@ +'use client' + +import {zodResolver} from '@hookform/resolvers/zod' +import {DeliveryOption} from '@prisma/client' +import {useLocale} from 'next-intl' +import React, {useEffect, useState} from 'react' +import {useForm} from 'react-hook-form' +import toast from 'react-hot-toast' +import {z} from 'zod' + +import {onPlacingOrder} from '@/actions/admin/place-order' +import NovaPost from '@/app/[locale]/(root)/(shop)/cart/nova-post' +import SearchAddress from '@/components/pages/cart/search-address' +import { + DeliveryOptionTypeDescription, + createOrderFormSchema +} from '@/lib/schemas/admin/order' +import {dump} from '@/lib/utils' +import useCartStore from '@/store/cart-store' +import {SessionUser} from '@/types/auth' +import {Button} from '@/ui/button' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/ui/form' +import {Input} from '@/ui/input' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/ui/select' + +export default function RegisteredOrderForm({ + styles, + user, + onSubmitHandler +}: { + styles: string + user?: SessionUser | null + onSubmitHandler: any +}) { + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + const [success, setSuccess] = useState('') + const [deliveryOption, setDeliveryOption] = useState('') + + const locale = useLocale() + const [warehouseRef, setWarehouseRef] = useState(JSON.stringify({})) + + const warehouseSubmit = (warehouse: any) => { + setWarehouseRef( + Object.keys(warehouse).length > 0 + ? JSON.stringify(warehouse) + : JSON.stringify({}) + ) + + setValue( + 'address', + Object.keys(warehouse).length > 0 + ? JSON.stringify(warehouse) + : JSON.stringify({}) + ) + } + const {cartItems, clearCart} = useCartStore() + const form = useForm>({ + resolver: zodResolver(createOrderFormSchema), + mode: 'onBlur', + defaultValues: { + user_id: user ? user.id.toString() : '', + is_quick: false, + lang: locale, + first_name: '', + surname: '', + delivery_option: '', + phone: '', + email: '', + address: warehouseRef, + notes: '', + details: JSON.stringify(cartItems) + } + }) + + const {register, setValue} = form + + useEffect(() => { + register('delivery_option') + register('address') + register('details') + }, [register]) + + const deliveryOptionHandler = (value: string) => { + setDeliveryOption(value) + setValue('delivery_option', value) + } + + const onSubmit = async (values: z.infer) => { + setLoading(true) + setValue('details', JSON.stringify(cartItems)) + onPlacingOrder(values).then((res: any) => { + if (res?.error) { + setError(res?.error) + setSuccess('') + setLoading(false) + toast.error(res?.error) + } else { + setSuccess(res?.success as string) + setError('') + setLoading(false) + clearCart() + toast.success(res?.success) + } + + onSubmitHandler(res) + }) + } + + return ( +
+ + {/*
{dump(user)}
*/} +

1. {locale !== 'ru' ? 'Особисті дані' : 'Личные данные'}

+
+
+ ( + + {locale !== 'ru' ? "Ім'я" : 'Имя'}* + + + + + + )} + /> +
+
+ ( + + + {locale !== 'ru' ? 'Прізвище' : 'Фамилия'}* + + + + + + + )} + /> +
+
+
+
+ ( + + Телефон* + + + + + + )} + /> +
+
+ ( + + E-mail* + + + + + + )} + /> +
+
+ +

+ 2.{' '} + {locale !== 'ru' + ? 'Інформація про доставку' + : 'Информация о доставке'} +

+
+ ( + + + {locale !== 'ru' + ? 'Варіанти доставки' + : 'Варианты доставки'}{' '} + + + + + )} + /> +
+ + {deliveryOption === 'NP' && ( + + )} + + {deliveryOption === 'COURIER' && ( + /**/ + + )} + + {deliveryOption === 'PICKUP' && ( +
Дані де і коли можна забрати
+ )} + +
+ ( + + + {locale !== 'ru' + ? 'Додаткова інформація' + : 'Дополнительная информация'} + + +