diff --git a/.gitignore b/.gitignore index 87b5daa..7e6ed1a 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,4 @@ dist .env*.local /messages/*.d.json.ts /public/uploads/ +/public/main-fallback.jpg diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index fdf3b0b..9120d8f 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -2,6 +2,7 @@ + diff --git a/actions/admin/product.ts b/actions/admin/product.ts index ac545b8..0633d0c 100644 --- a/actions/admin/product.ts +++ b/actions/admin/product.ts @@ -1,7 +1,6 @@ 'use server' -import {Meta, ProductLocale, ProductToStore} from '@prisma/client' -import sanitizeHtml from 'sanitize-html' +import {Meta, ProductLocale} from '@prisma/client' import {z} from 'zod' import {i18nLocalesCodes} from '@/i18n-config' @@ -9,12 +8,7 @@ import {STORE_ID} from '@/lib/config/constants' import {getProductBySlug} from '@/lib/data/models/product' import {db} from '@/lib/db/prisma/client' import {createProductFormSchema} from '@/lib/schemas/admin/product' -import { - cleanEmptyParams, - dbErrorHandling, - isEmptyObj, - slug as slugger -} from '@/lib/utils' +import {cleanEmptyParams, dbErrorHandling, slug as slugger} from '@/lib/utils' export const onProductCreateAction = async ( formData: z.infer @@ -42,7 +36,6 @@ export const onProductCreateAction = async ( for (const i in validatedData.meta) { const normalizedMeta: any = cleanEmptyParams(validatedData.meta[i]) - //if (!isEmptyObj(normalizedMeta)) {} meta.push(normalizedMeta) } @@ -58,7 +51,6 @@ export const onProductCreateAction = async ( if (!result) { const normalized: any = cleanEmptyParams({slug, ...locale}) - //locales.push({...normalized, meta: {create: meta[i]}}) locales.push(normalized) } else { return {error: `Продукт з такою назвою ${title} вже існує`} diff --git a/actions/model/category.ts b/actions/model/category.ts index 3d79e22..4925ed4 100644 --- a/actions/model/category.ts +++ b/actions/model/category.ts @@ -15,3 +15,9 @@ export const getCategoryBySlug = async (data: { } }) } + +export const getCategoryLocalesById = async (id: number) => { + return db.categoryLocale.findMany({ + where: {categoryId: id} + }) +} diff --git a/app/[locale]/(root)/(shop)/about-us/page.tsx b/app/[locale]/(root)/(shop)/about-us/page.tsx new file mode 100644 index 0000000..b37bd99 --- /dev/null +++ b/app/[locale]/(root)/(shop)/about-us/page.tsx @@ -0,0 +1,100 @@ +import React from 'react' + +export default function AboutUsPage() { + return ( +
+
+

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

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

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

+

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

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

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

+

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

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

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

+

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

+

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

+

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

+

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

+
+
+ ) +} diff --git a/app/[locale]/(root)/(shop)/cart/page.tsx b/app/[locale]/(root)/(shop)/cart/page.tsx index 1366c53..7dbdb2b 100644 --- a/app/[locale]/(root)/(shop)/cart/page.tsx +++ b/app/[locale]/(root)/(shop)/cart/page.tsx @@ -16,13 +16,15 @@ export default function Cart() { return (
-
-

{t('basket')}

-
-
Назва
-
Кількість
-
Вартість
-
+
+

+ {t('basket')} +

+
+
{t('title')}
+
{t('quantity')}
+
{t('amount')}
+
diff --git a/app/[locale]/(root)/(shop)/catalog/page.tsx b/app/[locale]/(root)/(shop)/catalog/page.tsx new file mode 100644 index 0000000..0531686 --- /dev/null +++ b/app/[locale]/(root)/(shop)/catalog/page.tsx @@ -0,0 +1,22 @@ +import {getCatalogIndexData} from '@prisma/client/sql' +import {getLocale} from 'next-intl/server' +import {notFound} from 'next/navigation' + +import CategoryPageIndex from '@/components/pages/category/page' +import {CategoryPageSqlSchema} from '@/lib/data/models/sqlSchemas' +import {db} from '@/lib/db/prisma/client' + +export default async function CatalogPage({ + params +}: { + params: Promise<{slug?: string}> +}) { + const loc = await getLocale() + const catalog: CategoryPageSqlSchema[] = await db.$queryRawTyped( + getCatalogIndexData(loc) + ) + + if (catalog.length < 1) notFound() + + return +} diff --git a/app/[locale]/(root)/(shop)/category/[[...slug]]/page.tsx b/app/[locale]/(root)/(shop)/category/[[...slug]]/page.tsx new file mode 100644 index 0000000..5a211eb --- /dev/null +++ b/app/[locale]/(root)/(shop)/category/[[...slug]]/page.tsx @@ -0,0 +1,22 @@ +import {getCategoryBySlugWitData} from '@prisma/client/sql' +import {notFound} from 'next/navigation' + +import CategoryPageIndex from '@/components/pages/category/page' +import {CategoryPageSqlSchema} from '@/lib/data/models/sqlSchemas' +import {db} from '@/lib/db/prisma/client' + +export default async function Categories({ + params +}: { + params: Promise<{slug?: string}> +}) { + const {slug} = await params + const [uri] = slug || [] + const category: CategoryPageSqlSchema[] = await db.$queryRawTyped( + getCategoryBySlugWitData(uri) + ) + + if (category.length < 2) notFound() + + return +} diff --git a/app/[locale]/(root)/page.tsx b/app/[locale]/(root)/page.tsx index d295958..dccc29a 100644 --- a/app/[locale]/(root)/page.tsx +++ b/app/[locale]/(root)/page.tsx @@ -1,11 +1,16 @@ +import {getCatalogIndexData} from '@prisma/client/sql' +import {getLocale} from 'next-intl/server' import Image from 'next/image' 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 {carousels} from '@/lib/data' +import {CategoryPageSqlSchema} from '@/lib/data/models/sqlSchemas' import {db} from '@/lib/db/prisma/client' import {dump} from '@/lib/utils' +import mainFallback from '@/public/main-fallback.jpg' import image from '@/public/uploads/products/IMG_6572.jpg' // const storeModel = async (id: any) => { @@ -26,6 +31,11 @@ import image from '@/public/uploads/products/IMG_6572.jpg' // } export default async function HomePage() { + const loc = await getLocale() + const catalog: CategoryPageSqlSchema[] = await db.$queryRawTyped( + getCatalogIndexData(loc) + ) + return ( <>
@@ -34,18 +44,27 @@ export default async function HomePage() {
- {/*
{dump(await storeModel(1))}
*/}
- {/*
{JSON.stringify(session)}
*/} -
-
- +
+ +
+ {''}
diff --git a/app/globals.css b/app/globals.css index fc948d5..ac5ef2c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,3 +1,5 @@ +@import "@/components/pages/product/embla.css"; + @tailwind base; @tailwind components; @tailwind utilities; @@ -120,6 +122,13 @@ body { @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 } + .bw-product-col-left{ + @apply flex-1 sm:w-5/12 sm:flex-auto sm:pl-4 md:w-7/12 md:pl-7 xl:w-auto xl:pl-9 + } + .bw-product-col-right{ + @apply flex-1 sm:w-7/12 md:w-5/12 lg:flex-col xl:w-[360px] xl:flex-none + } + .bw-header-col-left { @apply w-[9/12] flex-auto } @@ -221,8 +230,10 @@ body { font-size: 1rem !important; line-height: 1.45 !important; color: rgb(40, 26, 76) !important; - + background: transparent !important; + text-align: left !important; } + .bw-product__text { h2 * { font-weight: 700 !important; @@ -234,5 +245,23 @@ body { input { @apply text-xl leading-none border-0 text-brand-violet font-bold; } - +} + +.bw-cart-btn.lucide { + @apply text-brand-violet; + width: 32px; + height: 32px; + &:hover { + @apply drop-shadow-lg shadow-brand-violet-400; + } + } + +.bw-card-footer { + background: #f2f5fa !important; +} + +.bw-cart-wrapper { + .col{ + flex: 1; + } } diff --git a/components/(protected)/admin/product/create-edit-form.tsx b/components/(protected)/admin/product/create-edit-form.tsx index c9b58ba..e319b9c 100644 --- a/components/(protected)/admin/product/create-edit-form.tsx +++ b/components/(protected)/admin/product/create-edit-form.tsx @@ -4,15 +4,14 @@ import {zodResolver} from '@hookform/resolvers/zod' import dynamic from 'next/dynamic' import React, {useEffect, useMemo, useRef, useState} from 'react' import {useFieldArray, useForm} from 'react-hook-form' +import toast from 'react-hot-toast' import {z} from 'zod' import {onProductCreateAction} from '@/actions/admin/product' -import {useToast} from '@/hooks/use-toast' import {i18nDefaultLocale, i18nLocales} from '@/i18n-config' import {BaseEditorConfig} from '@/lib/config/editor' import {createProductFormSchema} from '@/lib/schemas/admin/product' import {toEmptyParams} from '@/lib/utils' -import useCountStore from '@/store/cart-store' import {Button} from '@/ui/button' import { Form, @@ -64,7 +63,6 @@ export default function ProductCreateEditForm({data}: {data?: any}) { data?.locales[1].instruction || '' ) const editor = useRef(null) //declared a null value - const {toast} = useToast() const config = useMemo(() => BaseEditorConfig, []) @@ -129,20 +127,12 @@ export default function ProductCreateEditForm({data}: {data?: any}) { setError(res?.error) setSuccess('') setLoading(false) - toast({ - variant: 'destructive', - title: res?.error, - description: res?.message - }) + toast.error(res?.error) } else { setSuccess(res?.success as string) setError('') setLoading(false) - toast({ - variant: 'success', - title: res?.success, - description: res?.message - }) + toast.success(res?.success) } }) } diff --git a/components/pages/cart/items.tsx b/components/pages/cart/items.tsx index 615a0d3..fa9fb43 100644 --- a/components/pages/cart/items.tsx +++ b/components/pages/cart/items.tsx @@ -1,9 +1,10 @@ // import styles from '@/components/pages/cart/cart.module.scss' -import Link from 'next/link' +import {Minus, Plus} from 'lucide-react' +import Image from 'next/image' -import useCartStore from '@/store/cart-store' +import {Link} from '@/i18n/routing' +import useCartStore, {CartItem} from '@/store/cart-store' import {Button} from '@/ui/button' -import {Input} from '@/ui/input' export default function CartItems() { const {cartItems} = useCartStore() @@ -27,7 +28,7 @@ export default function CartItems() {

Cart is Empty

Shop @@ -38,33 +39,45 @@ export default function CartItems() { return ( <> - {cartItems?.map((item, i) => ( -
-
{item.title}
-
- -
- {item.quantity} -
- + {cartItems?.map((item: CartItem, i: number) => ( +
+
+ {item.title} +
- -
+
+
+ +
+ {item.quantity} +
+ +
+
+
{(item.price * item.quantity).toFixed(2)} грн
diff --git a/components/pages/category/page.tsx b/components/pages/category/page.tsx new file mode 100644 index 0000000..b6e31ec --- /dev/null +++ b/components/pages/category/page.tsx @@ -0,0 +1,29 @@ +import AppCatalog from '@/components/shared/sidebar/app-catalog' +import FeatureCard from '@/components/shared/store/feature-card' +import {CategoryPageSqlSchema} from '@/lib/data/models/sqlSchemas' +import {thisLocales} from '@/lib/utils' + +export default async function CategoryPageIndex({ + data +}: { + data: CategoryPageSqlSchema[] +}) { + const locales = await thisLocales(data) + + return ( +
+
+
+ +
+
+
+ {locales.map((card: CategoryPageSqlSchema, i: number) => ( + + ))} +
+
+
+
+ ) +} diff --git a/components/pages/product/carousel.tsx b/components/pages/product/carousel.tsx new file mode 100644 index 0000000..513df40 --- /dev/null +++ b/components/pages/product/carousel.tsx @@ -0,0 +1,116 @@ +'use client' + +import {ProductResource} from '@prisma/client' +import {EmblaOptionsType} from 'embla-carousel' +import useEmblaCarousel from 'embla-carousel-react' +import Image from 'next/image' +import React, {useCallback, useEffect, useState} from 'react' + +import {Thumb} from './thumb' +import {Dialog, DialogContent, DialogTitle} from '@/ui/dialog' + +type PropType = { + slides: number[] + options?: EmblaOptionsType +} + +export default function ProductCarousel({ + images, + title +}: { + images: ProductResource[] | null + title: string +}) { + //const {slides, options} = props + const [selectedIndex, setSelectedIndex] = useState(0) + const [emblaMainRef, emblaMainApi] = useEmblaCarousel({}) //options + const [emblaThumbsRef, emblaThumbsApi] = useEmblaCarousel({ + containScroll: 'keepSnaps', + dragFree: true + }) + + const onThumbClick = useCallback( + (index: number) => { + if (!emblaMainApi || !emblaThumbsApi) return + emblaMainApi.scrollTo(index) + }, + [emblaMainApi, emblaThumbsApi] + ) + + const onSelect = useCallback(() => { + if (!emblaMainApi || !emblaThumbsApi) return + setSelectedIndex(emblaMainApi.selectedScrollSnap()) + emblaThumbsApi.scrollTo(emblaMainApi.selectedScrollSnap()) + }, [emblaMainApi, emblaThumbsApi, setSelectedIndex]) + + useEffect(() => { + if (!emblaMainApi) return + onSelect() + + emblaMainApi.on('select', onSelect).on('reInit', onSelect) + }, [emblaMainApi, onSelect]) + //let featuredImage: ProductResource | null | undefined + /*if ((resources || []).length > 0) { + featuredImage = resources?.find(resource => resource.isFeature) + }*/ + + return ( +
+
+
+ {images?.map((image: ProductResource, index: number) => ( +
+
+ +
+
+ ))} +
+
+ +
+
+
+ {images?.map((image: ProductResource, index: number) => ( + + {title} + onThumbClick(index)} + selected={index === selectedIndex} + index={index} + /> + + + + + ))} +
+
+
+
+ ) +} diff --git a/components/pages/product/embla.css b/components/pages/product/embla.css new file mode 100644 index 0000000..10a299b --- /dev/null +++ b/components/pages/product/embla.css @@ -0,0 +1,81 @@ +.embla { + max-width: 48rem; + margin: auto; + --slide-height: 19rem; + --slide-spacing: 1rem; + --slide-size: 100%; +} +.embla__viewport { + overflow: hidden; +} +.embla__container { + display: flex; + touch-action: pan-y pinch-zoom; + margin-left: calc(var(--slide-spacing) * -1); +} +.embla__slide { + transform: translate3d(0, 0, 0); + flex: 0 0 var(--slide-size); + min-width: 0; + padding-left: var(--slide-spacing); +} +.embla__slide__number { + box-shadow: inset 0 0 0 0.2rem var(--detail-medium-contrast); + border-radius: 1.8rem; + font-size: 4rem; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + height: var(--slide-height); + user-select: none; +} +.embla-thumbs { + --thumbs-slide-spacing: 0.8rem; + --thumbs-slide-height: 6rem; + margin-top: var(--thumbs-slide-spacing); +} +.embla-thumbs__viewport { + overflow: hidden; +} +.embla-thumbs__container { + display: flex; + flex-direction: row; + margin-left: calc(var(--thumbs-slide-spacing) * -1); +} +.embla-thumbs__slide { + flex: 0 0 22%; + min-width: 0; + padding-left: var(--thumbs-slide-spacing); +} +@media (min-width: 576px) { + .embla-thumbs__slide { + flex: 0 0 15%; + } +} +.embla-thumbs__slide__number { + border-radius: 1.8rem; + -webkit-tap-highlight-color: rgba(var(--text-high-contrast-rgb-value), 0.5); + -webkit-appearance: none; + appearance: none; + background-color: transparent; + touch-action: manipulation; + display: inline-flex; + text-decoration: none; + cursor: pointer; + border: 0; + padding: 0; + margin: 0; + box-shadow: inset 0 0 0 0.2rem var(--detail-medium-contrast); + font-size: 1.8rem; + font-weight: 600; + color: var(--detail-high-contrast); + display: flex; + align-items: center; + justify-content: center; + height: var(--thumbs-slide-height); + width: 100%; +} +.embla-thumbs__slide--selected .embla-thumbs__slide__number { + color: var(--text-body); +} diff --git a/components/pages/product/index.tsx b/components/pages/product/index.tsx index 4e8b1f7..7ef0d53 100644 --- a/components/pages/product/index.tsx +++ b/components/pages/product/index.tsx @@ -1,37 +1,79 @@ -import {getLocale} from 'next-intl/server' +import {ProductResource} from '@prisma/client' +import {getProductByIdWitData} from '@prisma/client/sql' +import {getTranslations} from 'next-intl/server' import {notFound} from 'next/navigation' -import {strict} from 'node:assert' -import AddCartButton from '@/components/pages/product/add-cart-button' -import {ProductProps, getProductById} from '@/lib/data/models/product' -import {dump} from '@/lib/utils' -import useCartStore from '@/store/cart-store' +import ProductCarousel from '@/components/pages/product/carousel' +import AddCartButton from '@/components/shared/store/add-cart-button' +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator +} from '@/components/ui/breadcrumb' +import {getProductResources} from '@/lib/data/models/product' +import {CategoryPageSqlSchema} from '@/lib/data/models/sqlSchemas' +import {db} from '@/lib/db/prisma/client' +import {thisLocale, toPrice} from '@/lib/utils' +import {Separator} from '@/ui/separator' import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/ui/tabs' export default async function ProductPageIndex({id}: {id: string}) { - const product = await getProductById(parseInt(id)) - if (!product) notFound() - const {locales, toStore} = product as ProductProps - const lang = await getLocale() - const locale = locales[lang === 'uk' ? 0 : 1] - const store = JSON.parse(JSON.stringify(toStore[0])) + const t = await getTranslations('Common') + + const data: CategoryPageSqlSchema[] = await db.$queryRawTyped( + getProductByIdWitData(id) + ) + + const locale = await thisLocale(data) + if (!locale) notFound() + + const resources: ProductResource[] | null = await getProductResources( + parseInt(id) + ) + + //const files = await getMetaOfFile(locale.productId) return (
-
-
- -
-
-
+
+ {/*
{dump(resources)}
*/} +
+ + + + {t('home')} + + + + + {locale.categoryTitle} + + + + + {locale.title} + + + +
+

+ {locale.title} +

+ +
+ + Опис @@ -51,6 +93,14 @@ export default async function ProductPageIndex({id}: {id: string}) {
+
+
+ {t('price')}: + + {toPrice(locale.price)} + +
+
) diff --git a/components/pages/product/thumb.tsx b/components/pages/product/thumb.tsx new file mode 100644 index 0000000..82ca458 --- /dev/null +++ b/components/pages/product/thumb.tsx @@ -0,0 +1,46 @@ +import {ProductResource} from '@prisma/client' +import Image from 'next/image' +import React from 'react' + +import {Button} from '@/ui/button' +import {DialogTrigger} from '@/ui/dialog' + +type PropType = { + selected: boolean + index: number + onClick: () => void + image: ProductResource +} + +export const Thumb: React.FC = props => { + const {selected, index, onClick, image} = props + + return ( +
+ + + +
+ ) +} diff --git a/components/shared/header/index.tsx b/components/shared/header/index.tsx index bc61f4f..37f536f 100644 --- a/components/shared/header/index.tsx +++ b/components/shared/header/index.tsx @@ -15,13 +15,6 @@ type MenuProps = { } export default async function Header() { - /*{ - searchParams - }: { - searchParams: Promise<{query?: string}> - }*/ - //const query = (await searchParams).query - return (
diff --git a/components/shared/home/feature-cards.tsx b/components/shared/home/feature-cards.tsx index abf35ab..5b2d378 100644 --- a/components/shared/home/feature-cards.tsx +++ b/components/shared/home/feature-cards.tsx @@ -1,34 +1,22 @@ -import Image from 'next/image' -import * as React from 'react' - -import {cards} from '@/lib/data' -import {Card, CardContent} from '@/ui/card' +import FeatureCardFront from '@/components/shared/store/feature-card-front' +import {CategoryPageSqlSchema} from '@/lib/data/models/sqlSchemas' import {Carousel, CarouselContent, CarouselItem} from '@/ui/carousel' -export default function FeatureCards() { +export default function FeatureCards({ + items +}: { + items: CategoryPageSqlSchema[] +}) { return ( - {cards.map((card: any) => ( + {items.map((card: any) => (
- - - - {''} - - - +
))} diff --git a/components/shared/locale-switcher.tsx b/components/shared/locale-switcher.tsx index 98c5dbe..6236fa5 100644 --- a/components/shared/locale-switcher.tsx +++ b/components/shared/locale-switcher.tsx @@ -1,39 +1,37 @@ 'use client' +import cookies from 'js-cookie' import {useLocale} from 'next-intl' -import {redirect} from 'next/navigation' -import {Link, usePathname, useRouter} from '@/i18n/routing' +import {useRouter} from '@/i18n/routing' +import {Link} from '@/i18n/routing' import {Label} from '@/ui/label' import {Switch} from '@/ui/switch' export default function LocaleSwitcher() { const router = useRouter() - const pathname = usePathname() const locale = useLocale() const initialState = locale !== 'uk' + //const [localeState, setLocaleState] = useState(initialState) - const handler = (state: boolean) => { - const newPath = `/${locale}${pathname}` - //window.history.replaceState(null, '', newPath) - const link = document.getElementById('lang-switch') - if (link) { - link.innerText = `${state ? '/ru' : ''}${pathname}` - link.setAttribute('href', `${state ? '/ru' : ''}${pathname}`) - link.click() - } + const handleLocaleChange = (state: boolean) => { + const locale = cookies.get('NEXT_LOCALE') === 'ru' ? 'uk' : 'ru' + cookies.set('NEXT_LOCALE', locale, { + expires: 7, + path: '/', + sameSite: 'Lax' + }) + router.replace(window.location.pathname.replace(/^\/ru/, ''), {locale}) } - // router.replace('/cabinet', {locale: checked ? 'ru' : 'uk'} + return (
- - LA -
diff --git a/components/shared/navbar/navbar-menu.tsx b/components/shared/navbar/navbar-menu.tsx index dc8d786..eafe551 100644 --- a/components/shared/navbar/navbar-menu.tsx +++ b/components/shared/navbar/navbar-menu.tsx @@ -1,9 +1,8 @@ 'use client' -import {Menu as MenuIcon, X} from 'lucide-react' -import Link from 'next/link' import {useState} from 'react' +import {Link} from '@/i18n/routing' import {data} from '@/lib/data' import {Button} from '@/ui/button' @@ -21,7 +20,7 @@ export default function NavbarMenu() {
{data.headerMenus.map(item => ( - + {item.name} ))} diff --git a/components/shared/sidebar/app-catalog-render.tsx b/components/shared/sidebar/app-catalog-render.tsx index f0ef31c..b3be2df 100644 --- a/components/shared/sidebar/app-catalog-render.tsx +++ b/components/shared/sidebar/app-catalog-render.tsx @@ -12,18 +12,20 @@ export default function AppCatalogRender(data: {items: Category[]}) { return (
- + + +
    {data?.items.map((item: any) => (
  • {item.locales[locale === 'uk' ? 0 : 1].title} diff --git a/components/pages/product/add-cart-button.tsx b/components/shared/store/add-cart-button.tsx similarity index 65% rename from components/pages/product/add-cart-button.tsx rename to components/shared/store/add-cart-button.tsx index 198ccbe..e28bf4a 100644 --- a/components/pages/product/add-cart-button.tsx +++ b/components/shared/store/add-cart-button.tsx @@ -5,6 +5,7 @@ import {useTranslations} from 'next-intl' import {dump} from '@/lib/utils' import useCartStore, {CartItem} from '@/store/cart-store' +import {Button} from '@/ui/button' export default function AddCartButton({product}: {product: CartItem}) { const t = useTranslations('cart') @@ -13,16 +14,20 @@ export default function AddCartButton({product}: {product: CartItem}) { return ( <> - -
    {dump(cartItems)}
    + */} + + addItemToCart(product)} + /> ) } diff --git a/components/shared/store/card-buy-button.tsx b/components/shared/store/card-buy-button.tsx new file mode 100644 index 0000000..c7ea149 --- /dev/null +++ b/components/shared/store/card-buy-button.tsx @@ -0,0 +1,31 @@ +'use client' + +import {ShoppingCartIcon} from 'lucide-react' +import {useTranslations} from 'next-intl' + +import useCartStore, {CartItem} from '@/store/cart-store' +import {Button} from '@/ui/button' + +export default function CardBuyButton({ + item, + isIcon +}: { + item: CartItem + isIcon?: boolean +}) { + const t = useTranslations('cart') + const addItemToCart = useCartStore(state => state.addItemToCart) + + return ( + + ) +} diff --git a/components/shared/store/feature-card-front.tsx b/components/shared/store/feature-card-front.tsx new file mode 100644 index 0000000..c1b55f9 --- /dev/null +++ b/components/shared/store/feature-card-front.tsx @@ -0,0 +1,71 @@ +import {ProductResource} from '@prisma/client' +import {Star, StarHalf} from 'lucide-react' +import Image from 'next/image' + +import CardBuyButton from '@/components/shared/store/card-buy-button' +import RateStars from '@/components/shared/store/stars' +import {Link} from '@/i18n/routing' +import {CategoryPageSqlSchema} from '@/lib/data/models/sqlSchemas' +import {Button} from '@/ui/button' +import {Card, CardContent, CardFooter} from '@/ui/card' +import {CarouselItem} from '@/ui/carousel' + +export default function FeatureCardFront({ + card +}: { + card: CategoryPageSqlSchema +}) { + return ( + + + + {/**/} + {''} + + {/**/} + + +
    +
    +

    + {card.title} +

    +

    + {parseFloat(card.price as string).toFixed(2)} +

    +
    + +
    +
    + ) +} + +// id: number +// quantity: number +// title: string +// price: string | any +// image?: string | null +// imageWidth?: number | null +// imageHeight?: number | null diff --git a/components/shared/store/feature-card.tsx b/components/shared/store/feature-card.tsx new file mode 100644 index 0000000..c823b43 --- /dev/null +++ b/components/shared/store/feature-card.tsx @@ -0,0 +1,65 @@ +import {Star, StarHalf} from 'lucide-react' +import Image from 'next/image' + +import CardBuyButton from '@/components/shared/store/card-buy-button' +import RateStars from '@/components/shared/store/stars' +import {Link} from '@/i18n/routing' +import {Button} from '@/ui/button' +import {Card, CardContent, CardFooter} from '@/ui/card' + +//import {CarouselItem} from '@/ui/carousel' + +export default function FeatureCard({card}: {card: any}) { + return ( + + + + {/**/} + {''} + + + +
    +
    +

    + {card.title} +

    +

    + {parseFloat(card.price).toFixed(2)} +

    +
    + +
    +
    + ) +} + +// id: number +// quantity: number +// title: string +// price: string | any +// image?: string | null +// imageWidth?: number | null +// imageHeight?: number | null diff --git a/components/shared/store/stars.tsx b/components/shared/store/stars.tsx new file mode 100644 index 0000000..b321f62 --- /dev/null +++ b/components/shared/store/stars.tsx @@ -0,0 +1,16 @@ +import {Star} from 'lucide-react' + +const startStroke = 1.5 +const color = '#ffd139' + +export default function RateStars() { + return ( +
    + + + + + +
    + ) +} diff --git a/components/ui/table.tsx b/components/ui/table.tsx new file mode 100644 index 0000000..c0df655 --- /dev/null +++ b/components/ui/table.tsx @@ -0,0 +1,120 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    + + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
    [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/components/ui/toast.tsx b/components/ui/toast.tsx deleted file mode 100644 index 60e6d82..0000000 --- a/components/ui/toast.tsx +++ /dev/null @@ -1,135 +0,0 @@ -'use client' - -import * as ToastPrimitives from '@radix-ui/react-toast' -import {type VariantProps, cva} from 'class-variance-authority' -import {X} from 'lucide-react' -import * as React from 'react' - -import {cn} from '@/lib/utils' - -const ToastProvider = ToastPrimitives.Provider - -const ToastViewport = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, ...props}, ref) => ( - -)) -ToastViewport.displayName = ToastPrimitives.Viewport.displayName - -const toastVariants = cva( - 'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full', - { - variants: { - variant: { - default: 'border bg-background text-foreground', - destructive: - 'destructive group border-destructive bg-destructive text-destructive-foreground', - success: - 'group text-emerald-950-foreground border-emerald-600 bg-emerald-200', - warning: - 'group text-amber-950-foreground border-amber-700 bg-amber-200', - brand: - 'text-brand-violet-950 border-brand-violet-200 bg-brand-violet-50' - } - }, - defaultVariants: { - variant: 'brand' - } - } -) - -const Toast = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({className, variant, ...props}, ref) => { - return ( - - ) -}) -Toast.displayName = ToastPrimitives.Root.displayName - -const ToastAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, ...props}, ref) => ( - -)) -ToastAction.displayName = ToastPrimitives.Action.displayName - -const ToastClose = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, ...props}, ref) => ( - - - -)) -ToastClose.displayName = ToastPrimitives.Close.displayName - -const ToastTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, ...props}, ref) => ( - -)) -ToastTitle.displayName = ToastPrimitives.Title.displayName - -const ToastDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({className, ...props}, ref) => ( - -)) -ToastDescription.displayName = ToastPrimitives.Description.displayName - -type ToastProps = React.ComponentPropsWithoutRef - -type ToastActionElement = React.ReactElement - -export { - type ToastProps, - type ToastActionElement, - ToastProvider, - ToastViewport, - Toast, - ToastTitle, - ToastDescription, - ToastClose, - ToastAction -} diff --git a/components/ui/toaster.tsx b/components/ui/toaster.tsx deleted file mode 100644 index 171beb4..0000000 --- a/components/ui/toaster.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client" - -import { useToast } from "@/hooks/use-toast" -import { - Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport, -} from "@/components/ui/toast" - -export function Toaster() { - const { toasts } = useToast() - - return ( - - {toasts.map(function ({ id, title, description, action, ...props }) { - return ( - -
    - {title && {title}} - {description && ( - {description} - )} -
    - {action} - -
    - ) - })} - -
    - ) -} diff --git a/data/products.ts b/data/products.ts new file mode 100644 index 0000000..adf4ff7 --- /dev/null +++ b/data/products.ts @@ -0,0 +1,9 @@ +export default [ + { + id: 0, + qnt: '14', + form: 'порошок', + vendor: 'Fine Foods & Pharmaceuticals N.T.M. SpA', + country: 'Італія' + } +] diff --git a/hooks/use-toast.ts b/hooks/use-toast.ts deleted file mode 100644 index 08776f6..0000000 --- a/hooks/use-toast.ts +++ /dev/null @@ -1,191 +0,0 @@ -'use client' - -// Inspired by react-hot-toast library -import * as React from 'react' - -import type {ToastActionElement, ToastProps} from '@/components/ui/toast' - -const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 1000000 - -type ToasterToast = ToastProps & { - id: string - title?: React.ReactNode - description?: React.ReactNode - action?: ToastActionElement -} - -const actionTypes = { - ADD_TOAST: 'ADD_TOAST', - UPDATE_TOAST: 'UPDATE_TOAST', - DISMISS_TOAST: 'DISMISS_TOAST', - REMOVE_TOAST: 'REMOVE_TOAST' -} as const - -let count = 0 - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER - return count.toString() -} - -type ActionType = typeof actionTypes - -type Action = - | { - type: ActionType['ADD_TOAST'] - toast: ToasterToast - } - | { - type: ActionType['UPDATE_TOAST'] - toast: Partial - } - | { - type: ActionType['DISMISS_TOAST'] - toastId?: ToasterToast['id'] - } - | { - type: ActionType['REMOVE_TOAST'] - toastId?: ToasterToast['id'] - } - -interface State { - toasts: ToasterToast[] -} - -const toastTimeouts = new Map>() - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId) - dispatch({ - type: 'REMOVE_TOAST', - toastId: toastId - }) - }, TOAST_REMOVE_DELAY) - - toastTimeouts.set(toastId, timeout) -} - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case 'ADD_TOAST': - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT) - } - - case 'UPDATE_TOAST': - return { - ...state, - toasts: state.toasts.map(t => - t.id === action.toast.id ? {...t, ...action.toast} : t - ) - } - - case 'DISMISS_TOAST': { - const {toastId} = action - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId) - } else { - state.toasts.forEach(toast => { - addToRemoveQueue(toast.id) - }) - } - - return { - ...state, - toasts: state.toasts.map(t => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false - } - : t - ) - } - } - case 'REMOVE_TOAST': - if (action.toastId === undefined) { - return { - ...state, - toasts: [] - } - } - return { - ...state, - toasts: state.toasts.filter(t => t.id !== action.toastId) - } - } -} - -const listeners: Array<(state: State) => void> = [] - -let memoryState: State = {toasts: []} - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action) - listeners.forEach(listener => { - listener(memoryState) - }) -} - -type Toast = Omit - -function toast({...props}: Toast) { - const id = genId() - - const update = (props: ToasterToast) => - dispatch({ - type: 'UPDATE_TOAST', - toast: {...props, id} - }) - const dismiss = () => dispatch({type: 'DISMISS_TOAST', toastId: id}) - - dispatch({ - type: 'ADD_TOAST', - toast: { - ...props, - id, - open: true, - onOpenChange: open => { - if (!open) dismiss() - } - } - }) - - return { - id: id, - dismiss, - update - } -} - -function useToast() { - const [state, setState] = React.useState(memoryState) - - React.useEffect(() => { - listeners.push(setState) - return () => { - const index = listeners.indexOf(setState) - if (index > -1) { - listeners.splice(index, 1) - } - } - }, [state]) - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({type: 'DISMISS_TOAST', toastId}) - } -} - -export {useToast, toast} diff --git a/lib/config/resources.ts b/lib/config/resources.ts new file mode 100644 index 0000000..3992afa --- /dev/null +++ b/lib/config/resources.ts @@ -0,0 +1,67 @@ +import {ResourceType} from '@prisma/client' +import im, {Features} from 'imagemagick' +import fs from 'node:fs' +import path from 'node:path' + +import {db} from '@/lib/db/prisma/client' + +export const PUBLIC_UPLOAD_PRODUCTS_DIR = '/uploads/products' +export const UPLOAD_PRODUCTS_DIR = path.resolve( + `./public${PUBLIC_UPLOAD_PRODUCTS_DIR}` +) + +export function getFilesByProductId(id: number, fullPath: boolean = true) { + const files = fs + .readdirSync(UPLOAD_PRODUCTS_DIR) + .filter(file => file.startsWith(`${id}-`)) + + return !fullPath + ? files + : files.map(file => path.join(UPLOAD_PRODUCTS_DIR, file)) +} + +interface ProductImage { + type: string + productId: number + filesize: number + height: number + width: number + 'mime type'?: string + mimeType: string + quality: number + properties: { + signature: string + } + signature: string +} + +export async function getMetaOfFile(id: number) { + getFilesByProductId(id).forEach(file => { + im.identify(file, async (err, features) => { + if (err) throw err + // { format: 'JPEG', width: 3904, height: 2622, depth: 8 } + const data = features as ProductImage + + try { + const result = await db.productResource.create({ + data: { + type: 'IMAGE' as ResourceType, + productId: id, + uri: PUBLIC_UPLOAD_PRODUCTS_DIR + '/' + path.basename(file), + filesize: parseInt(data.filesize as unknown as string), + height: data.height, + width: data.width, + quality: data.quality, + mimeType: data['mime type'] || 'image/*', + signature: data.properties.signature + //meta: features + } + }) + + //return result + } catch (e) { + //console.log(e) + } + }) + }) +} diff --git a/lib/data.ts b/lib/data.ts index 6dd7bed..e66e366 100644 --- a/lib/data.ts +++ b/lib/data.ts @@ -5,7 +5,7 @@ export const data = { { name: 'Про нас', slug: slug('Про нас'), - href: '/search?tag=' + href: '/about-us' }, { name: "Цікаво про здоров'я", diff --git a/lib/data/models/category.ts b/lib/data/models/category.ts new file mode 100644 index 0000000..8cd7c6d --- /dev/null +++ b/lib/data/models/category.ts @@ -0,0 +1,22 @@ +'use server' + +import {CategoryLocale} from '@prisma/client' + +import {STORE_ID} from '@/lib/config/constants' +import {db} from '@/lib/db/prisma/client' + +export const getCategoryBySlug = async (slug: string) => { + return db.categoryLocale.findFirst({ + where: { + slug + }, + select: { + category: { + include: { + locales: true, + categoriesOnProducts: true + } + } + } + }) +} diff --git a/lib/data/models/product.ts b/lib/data/models/product.ts index 53bd7cd..2776984 100644 --- a/lib/data/models/product.ts +++ b/lib/data/models/product.ts @@ -1,12 +1,23 @@ 'use server' -import {Lang, Product, ProductLocale, ProductToStore} from '@prisma/client' +import { + CategoriesOnProducts, + CategoryLocale, + Lang, + Product, + ProductLocale, + ProductResource, + ProductToStore +} from '@prisma/client' +import {STORE_ID} from '@/lib/config/constants' import {db, dbQueryLog} from '@/lib/db/prisma/client' export interface ProductProps extends Product { locales: ProductLocale[] toStore: ProductToStore[] + categoriesOnProducts: CategoriesOnProducts[] + resources: ProductResource[] } export const getProductBySlug = async (data: { @@ -25,7 +36,27 @@ export const getProductById = async (id: unknown): Promise => { return db.product.findUnique({ where: {id: id as number}, include: { + resources: true, + categoriesOnProducts: { + where: { + storeId: STORE_ID + }, + include: { + category: { + include: { + locales: { + orderBy: { + lang: 'asc' + } + } + } + } + } + }, locales: { + orderBy: { + lang: 'asc' + }, include: { meta: true } @@ -39,6 +70,9 @@ export const getProducts = async (): Promise => { return db.product.findMany({ include: { locales: { + orderBy: { + lang: 'asc' + }, omit: { description: true, content: true, @@ -52,3 +86,13 @@ export const getProducts = async (): Promise => { } }) } + +export const getProductResources = async ( + productId: number | null +): Promise => { + return db.productResource.findMany({ + where: { + productId: productId + } + }) +} diff --git a/lib/data/models/sqlSchemas.ts b/lib/data/models/sqlSchemas.ts new file mode 100644 index 0000000..c32ca39 --- /dev/null +++ b/lib/data/models/sqlSchemas.ts @@ -0,0 +1,23 @@ +import Decimal from 'decimal.js' + +import {i18nLocalesCodes} from '@/i18n-config' +import {locales} from '@/i18n/routing' + +export type CategoryPageSqlSchema = { + productId: number + lang: string + slug?: string | null | undefined + image?: string | null | undefined + imageWidth?: number | null | undefined + imageHeight?: number | null | undefined + categoryId?: number + title: string + shortTitle?: string | null | undefined + description?: string | null | undefined + content?: string | null | undefined + headingTitle?: string | null | undefined + instruction?: string | null | undefined + categorySlug?: string | null | undefined + categoryTitle?: string | null | undefined + price?: string | null | Decimal +} diff --git a/lib/db/prisma/schema/category.prisma b/lib/db/prisma/schema/category.prisma index 5c33656..4175d86 100644 --- a/lib/db/prisma/schema/category.prisma +++ b/lib/db/prisma/schema/category.prisma @@ -6,7 +6,7 @@ model Category { storeId Int @map("store_id") locales CategoryLocale[] createdAt DateTime @default(now()) @map("created_at") - categoriesOnPruducts CategoriesOnProducts[] + categoriesOnProducts CategoriesOnProducts[] @@map("categories") } @@ -36,5 +36,5 @@ model CategoriesOnProducts { categoryId Int @map("category_id") @@id([storeId, productId, categoryId]) - @@map("categories_on_pruducts") + @@map("categories_on_products") } diff --git a/lib/db/prisma/schema/product.prisma b/lib/db/prisma/schema/product.prisma index 7ff0813..1c1b46f 100644 --- a/lib/db/prisma/schema/product.prisma +++ b/lib/db/prisma/schema/product.prisma @@ -25,7 +25,7 @@ model ProductLocale { shortTitle String? @map("short_title") headingTitle String? @map("heading_title") description String? @db.MediumText - content String @db.MediumText + content String? @db.MediumText instruction String? @db.MediumText product Product @relation(fields: [productId], references: [id]) productId Int @map("product_id") @@ -40,12 +40,13 @@ model ProductResource { id Int @id @default(autoincrement()) type ResourceType mimeType String? @map("mime_type") + isFeature Boolean? @default(false) filesize Int? @db.UnsignedInt width Int? @db.UnsignedSmallInt height Int? @db.UnsignedSmallInt - quality Int? @db.UnsignedTinyInt - signature String? @db.Char(64) - uri String @db.Text + quality Float? @db.Float + signature String @db.Char(64) + uri String @db.VarChar(1024) title String? @db.VarChar(255) description String? @db.Text meta Json? @db.Json @@ -54,6 +55,7 @@ model ProductResource { createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(3) updatedAt DateTime? @default(dbgenerated("NULL DEFAULT NULL ON UPDATE current_timestamp(3)")) @map("updated_at") @db.Timestamp(3) + @@unique([productId, signature]) @@map("product_resources") } @@ -85,15 +87,16 @@ model ProductAttribute { } model ProductToStore { - id Int @id @default(autoincrement()) - position Int @default(0) - published Boolean @default(false) - available Boolean @default(false) - price Decimal @default(0) @db.Decimal(7, 2) - pricePromotional Decimal @default(0) @map("price_promotional") @db.Decimal(7, 2) - productId Int @map("product_id") - storeId Int @map("store_id") - product Product @relation(fields: [productId], references: [id]) + id Int @id @default(autoincrement()) + position Int @default(0) + published Boolean @default(false) + available Boolean @default(false) + price Decimal @default(0) @db.Decimal(7, 2) + pricePromotional Decimal @default(0) @map("price_promotional") @db.Decimal(7, 2) + productId Int @map("product_id") + storeId Int @map("store_id") + product Product @relation(fields: [productId], references: [id]) + createdAt DateTime @default(now()) @map("created_at") @db.Timestamp(3) @@unique([storeId, productId]) @@map("product_to_store") diff --git a/lib/db/prisma/sql/getCatalogIndexData.sql b/lib/db/prisma/sql/getCatalogIndexData.sql new file mode 100644 index 0000000..089b391 --- /dev/null +++ b/lib/db/prisma/sql/getCatalogIndexData.sql @@ -0,0 +1,20 @@ +SELECT DISTINCT + p.id as productId, + pl.lang, + pl.slug, + pr.uri as image, + pr.width as imageWidth, + pr.height as imageHeight, + pts.price, + pts.price_promotional as pricePromotional, + pl.title, + pl.short_title as shortTitle, + pl.description + -- pl.content, pl.heading_title as headingTitle, pl.instruction +FROM products p + JOIN categories_on_products cop ON p.id = cop.product_id + JOIN product_locale pl ON pl.product_id = p.id + JOIN product_to_store pts ON pts.product_id = p.id AND pts.store_id = cop.store_id + LEFT JOIN product_resources pr ON p.id = pr.product_id AND pr.isFeature = TRUE +WHERE cop.store_id = 1 AND pl.lang = ? +ORDER BY p.id DESC; diff --git a/lib/db/prisma/sql/getCategoryBySlugWitData.sql b/lib/db/prisma/sql/getCategoryBySlugWitData.sql new file mode 100644 index 0000000..4f8acf0 --- /dev/null +++ b/lib/db/prisma/sql/getCategoryBySlugWitData.sql @@ -0,0 +1,26 @@ +SELECT + p.id as productId, + pl.lang, + pl.slug, + pr.uri as image, + pr.width as imageWidth, + pr.height as imageHeight, + pts.price, + pts.price_promotional as pricePromotional, + cl.category_id as categoryId, + pl.title, + pl.short_title as shortTitle, + pl.description, + -- pl.content, + pl.heading_title as headingTitle, + -- pl.instruction, + cl.slug as categorySlug, + cl.title as categoryTitle +FROM products p + JOIN categories_on_products cop ON p.id = cop.product_id + JOIN product_locale pl ON pl.product_id = p.id + JOIN category_locales cl ON cl.category_id = cop.category_id + JOIN product_to_store pts ON pts.product_id = p.id AND pts.store_id = cop.store_id + LEFT JOIN product_resources pr ON p.id = pr.product_id AND pr.isFeature = TRUE +WHERE cop.store_id = 1 AND cl.slug = ? +ORDER BY pl.lang; diff --git a/lib/db/prisma/sql/getProductByIdWitData.sql b/lib/db/prisma/sql/getProductByIdWitData.sql new file mode 100644 index 0000000..a2837cc --- /dev/null +++ b/lib/db/prisma/sql/getProductByIdWitData.sql @@ -0,0 +1,26 @@ +SELECT DISTINCT + p.id as productId, + pl.lang, + pl.slug, + pr.uri as image, + pr.width as imageWidth, + pr.height as imageHeight, + pts.price, + pts.price_promotional as pricePromotional, + cl.category_id as categoryId, + pl.title, + pl.short_title as shortTitle, + pl.description, + pl.content, + pl.heading_title as headingTitle, + pl.instruction, + cl.slug as categorySlug, + cl.title as categoryTitle +FROM products p + JOIN categories_on_products cop ON p.id = cop.product_id + JOIN product_locale pl ON pl.product_id = p.id + JOIN category_locales cl ON cl.category_id = cop.category_id + JOIN product_to_store pts ON pts.product_id = p.id AND pts.store_id = cop.store_id + LEFT JOIN product_resources pr ON p.id = pr.product_id AND pr.isFeature = TRUE +WHERE cop.store_id = 1 AND p.id = ? +ORDER BY pl.lang diff --git a/lib/utils.ts b/lib/utils.ts index 62c5e49..422045d 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,9 +1,12 @@ import {Prisma} from '@prisma/client' import bcrypt from 'bcryptjs' import {type ClassValue, clsx} from 'clsx' +import {getLocale} from 'next-intl/server' import slugify from 'slugify' import {twMerge} from 'tailwind-merge' +import {i18nDefaultLocale} from '@/i18n-config' + /** * Just output dump using pretty output * @@ -16,6 +19,8 @@ export function dump(variable: any): [string, string] { ] } +export const toPrice = (price: any) => parseFloat(price).toFixed(2) + /** * Create fallback avatar for showing during login process or in case if empty * @@ -118,6 +123,16 @@ export const toEmptyParams = (data: object | object[]) => { return result } +export const thisLocales = async (locales: any) => { + const loc = await getLocale() + return locales.filter((locale: any) => locale.lang === loc) +} + +export const thisLocale = async (locales: any) => { + const loc = await getLocale() + return locales.find((locale: any) => locale.lang === loc) +} + export const dbErrorHandling = (e: unknown, message?: string | null) => { if (e instanceof Prisma.PrismaClientKnownRequestError) { return {error: `${e.code}: ${e.message}`} diff --git a/messages/ru.json b/messages/ru.json index 1b67658..ae4b534 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -3,6 +3,10 @@ "title": "Hello world!", "about": "Go to the about page" }, + "Common": { + "home": "Главная", + "price": "Цена" + }, "Error": { "title": "Произошла ошибка", "try-again": "Попробуйте снова", @@ -24,8 +28,12 @@ } }, "cart": { + "buy": "Купить", "basket": "Корзина", - "favorites": "Избранное" + "favorites": "Избранное", + "empty": "Корзина пуста", + "continue": "Продолжить покупки", + "checkout": "Оформить заказ" }, "cabinet": { "personal-information": { diff --git a/messages/uk.json b/messages/uk.json index adfad7e..005dfc0 100644 --- a/messages/uk.json +++ b/messages/uk.json @@ -3,6 +3,10 @@ "title": "Привіт світ!", "about": "Go to the about page" }, + "Common": { + "home": "Головна", + "price": "Ціна" + }, "Error": { "title": "Сталася помилка", "try-again": "Спробуйте знову", @@ -27,11 +31,16 @@ "title": "Каталог" }, "cart": { + "buy": "Купити", "basket": "Кошик", "favorites": "Обрані", "empty": "Кошик порожній", "continue": "Продовжити покупки", - "checkout": "Оформити замовлення" + "checkout": "Оформити замовлення", + "title": "Назва", + "quantity": "Кількість", + "amount": "Вартість", + "total": "Всього" }, "cabinet": { diff --git a/package-lock.json b/package-lock.json index f0e97bf..426107c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", - "@radix-ui/react-toast": "^1.2.5", "@radix-ui/react-tooltip": "^1.1.6", "@tailwindcss/typography": "^0.5.16", "argon2": "^0.41.1", @@ -33,6 +32,7 @@ "embla-carousel-react": "^8.5.2", "imagemagick": "^0.1.3", "jodit-react": "^5.2.4", + "js-cookie": "^3.0.5", "lucide-react": "^0.471.1", "next": "^15.1.6", "next-auth": "^5.0.0-beta.25", @@ -57,6 +57,7 @@ "@trivago/prettier-plugin-sort-imports": "^5.2.1", "@types/bcryptjs": "^2.4.6", "@types/imagemagick": "^0.0.35", + "@types/js-cookie": "^3.0.6", "@types/node": "^22.10.7", "@types/react": "^19", "@types/react-dom": "^19", @@ -74,8 +75,7 @@ "prisma-json-types-generator": "^3.2.2", "tailwindcss": "^3.4.1", "tsx": "^4.19.2", - "typescript": "^5", - "zod-prisma-types": "^3.2.1" + "typescript": "^5" }, "engines": { "node": ">=22" @@ -1648,13 +1648,6 @@ } } }, - "node_modules/@prisma/debug": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.2.1.tgz", - "integrity": "sha512-0KItvt39CmQxWkEw6oW+RQMD6RZ43SJWgEUnzxN8VC9ixMysa7MzZCZf22LCK5DSooiLNf8vM3LHZm/I/Ni7bQ==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/@prisma/engines": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.3.1.tgz", @@ -2388,40 +2381,6 @@ } } }, - "node_modules/@radix-ui/react-toast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.5.tgz", - "integrity": "sha512-ZzUsAaOx8NdXZZKcFNDhbSlbsCUy8qQWmzTdgrlrhhZAOx2ofLtKrBDW9fkqhFvXgmtv560Uj16pkLkqML7SHA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.4", - "@radix-ui/react-portal": "1.1.3", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.1", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-tooltip": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.7.tgz", @@ -2776,6 +2735,13 @@ "@types/node": "*" } }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3667,13 +3633,6 @@ "node": ">=0.10.0" } }, - "node_modules/code-block-writer": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", - "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", - "dev": true, - "license": "MIT" - }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -5989,6 +5948,15 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8867,36 +8835,6 @@ "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/zod-prisma-types": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/zod-prisma-types/-/zod-prisma-types-3.2.1.tgz", - "integrity": "sha512-qsOD8aMVx3Yg9gHctvhTRpavaJizt8xUda6qjwOcH7suvrirXax38tjs0ilKcY7GKbyY57q2rZ+uoSDnzVXpag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@prisma/generator-helper": "^6.0.1", - "code-block-writer": "^12.0.0", - "lodash": "^4.17.21", - "zod": "^3.23.8" - }, - "bin": { - "zod-prisma-types": "dist/bin.js" - }, - "peerDependencies": { - "@prisma/client": "^4.x.x || ^5.x.x || ^6.x.x", - "prisma": "^4.x.x || ^5.x.x || ^6.x.x" - } - }, - "node_modules/zod-prisma-types/node_modules/@prisma/generator-helper": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-6.2.1.tgz", - "integrity": "sha512-7Ws8DCXGan7hhaFMERXYdmhsudvSzEsrTttJEC7ubZJidvyimS12m3xpM+dLTt+NAShJ7Op7PgF+Mal2jf6xfg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.2.1" - } - }, "node_modules/zustand": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", diff --git a/package.json b/package.json index 0682516..2cce2b1 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", - "@radix-ui/react-toast": "^1.2.5", "@radix-ui/react-tooltip": "^1.1.6", "@tailwindcss/typography": "^0.5.16", "argon2": "^0.41.1", @@ -56,6 +55,7 @@ "embla-carousel-react": "^8.5.2", "imagemagick": "^0.1.3", "jodit-react": "^5.2.4", + "js-cookie": "^3.0.5", "lucide-react": "^0.471.1", "next": "^15.1.6", "next-auth": "^5.0.0-beta.25", @@ -80,6 +80,7 @@ "@trivago/prettier-plugin-sort-imports": "^5.2.1", "@types/bcryptjs": "^2.4.6", "@types/imagemagick": "^0.0.35", + "@types/js-cookie": "^3.0.6", "@types/node": "^22.10.7", "@types/react": "^19", "@types/react-dom": "^19", @@ -97,8 +98,7 @@ "prisma-json-types-generator": "^3.2.2", "tailwindcss": "^3.4.1", "tsx": "^4.19.2", - "typescript": "^5", - "zod-prisma-types": "^3.2.1" + "typescript": "^5" }, "engines": { "node": ">=22" diff --git a/public/uploads/1256-vistacare_osteostrong_tablets_box_livo_1.png b/public/uploads/1256-vistacare_osteostrong_tablets_box_livo_1.png deleted file mode 100644 index ed8cf8c..0000000 Binary files a/public/uploads/1256-vistacare_osteostrong_tablets_box_livo_1.png and /dev/null differ diff --git a/public/uploads/189238-192172-orig-1500-1500-d76a.jpg b/public/uploads/189238-192172-orig-1500-1500-d76a.jpg deleted file mode 100644 index f40e470..0000000 Binary files a/public/uploads/189238-192172-orig-1500-1500-d76a.jpg and /dev/null differ diff --git a/public/uploads/1ca3d021-a55d-4c0a-aa7b-06ecb5f905b0-w1000-h1000-wm-frame.jpg b/public/uploads/1ca3d021-a55d-4c0a-aa7b-06ecb5f905b0-w1000-h1000-wm-frame.jpg deleted file mode 100644 index 433d18c..0000000 Binary files a/public/uploads/1ca3d021-a55d-4c0a-aa7b-06ecb5f905b0-w1000-h1000-wm-frame.jpg and /dev/null differ diff --git a/public/uploads/637393-1500x1500-ea2f.jpg b/public/uploads/637393-1500x1500-ea2f.jpg deleted file mode 100644 index f2a96e3..0000000 Binary files a/public/uploads/637393-1500x1500-ea2f.jpg and /dev/null differ diff --git a/public/uploads/79282077-e324-4248-9f5d-184242ec4dd4.webp b/public/uploads/79282077-e324-4248-9f5d-184242ec4dd4.webp deleted file mode 100644 index 24b6d25..0000000 Binary files a/public/uploads/79282077-e324-4248-9f5d-184242ec4dd4.webp and /dev/null differ diff --git a/store/cart-store.ts b/store/cart-store.ts index 8146a56..214a74e 100644 --- a/store/cart-store.ts +++ b/store/cart-store.ts @@ -8,6 +8,8 @@ export interface CartItem { title: string price: string | any image?: string | null + imageWidth?: number | null + imageHeight?: number | null } interface CartState {