diff --git a/frontend/NodeJS/.gitignore b/frontend/style/NodeJS/.gitignore similarity index 100% rename from frontend/NodeJS/.gitignore rename to frontend/style/NodeJS/.gitignore diff --git a/frontend/NodeJS/Product b/frontend/style/NodeJS/Product similarity index 100% rename from frontend/NodeJS/Product rename to frontend/style/NodeJS/Product diff --git a/frontend/NodeJS/README.md b/frontend/style/NodeJS/README.md similarity index 100% rename from frontend/NodeJS/README.md rename to frontend/style/NodeJS/README.md diff --git a/frontend/NodeJS/eslint.config.js b/frontend/style/NodeJS/eslint.config.js similarity index 100% rename from frontend/NodeJS/eslint.config.js rename to frontend/style/NodeJS/eslint.config.js diff --git a/frontend/NodeJS/index.html b/frontend/style/NodeJS/index.html similarity index 100% rename from frontend/NodeJS/index.html rename to frontend/style/NodeJS/index.html diff --git a/frontend/NodeJS/package-lock.json b/frontend/style/NodeJS/package-lock.json similarity index 100% rename from frontend/NodeJS/package-lock.json rename to frontend/style/NodeJS/package-lock.json diff --git a/frontend/NodeJS/package.json b/frontend/style/NodeJS/package.json similarity index 100% rename from frontend/NodeJS/package.json rename to frontend/style/NodeJS/package.json diff --git a/frontend/NodeJS/public/vite.svg b/frontend/style/NodeJS/public/vite.svg similarity index 100% rename from frontend/NodeJS/public/vite.svg rename to frontend/style/NodeJS/public/vite.svg diff --git a/frontend/NodeJS/src/App.css b/frontend/style/NodeJS/src/App.css similarity index 100% rename from frontend/NodeJS/src/App.css rename to frontend/style/NodeJS/src/App.css diff --git a/frontend/NodeJS/src/App.jsx b/frontend/style/NodeJS/src/App.jsx similarity index 100% rename from frontend/NodeJS/src/App.jsx rename to frontend/style/NodeJS/src/App.jsx diff --git a/frontend/NodeJS/src/assets/react.svg b/frontend/style/NodeJS/src/assets/react.svg similarity index 100% rename from frontend/NodeJS/src/assets/react.svg rename to frontend/style/NodeJS/src/assets/react.svg diff --git a/frontend/NodeJS/src/index.css b/frontend/style/NodeJS/src/index.css similarity index 100% rename from frontend/NodeJS/src/index.css rename to frontend/style/NodeJS/src/index.css diff --git a/frontend/NodeJS/src/main.jsx b/frontend/style/NodeJS/src/main.jsx similarity index 100% rename from frontend/NodeJS/src/main.jsx rename to frontend/style/NodeJS/src/main.jsx diff --git a/frontend/NodeJS/styles.css b/frontend/style/NodeJS/styles.css similarity index 100% rename from frontend/NodeJS/styles.css rename to frontend/style/NodeJS/styles.css diff --git a/frontend/NodeJS/vite.config.js b/frontend/style/NodeJS/vite.config.js similarity index 100% rename from frontend/NodeJS/vite.config.js rename to frontend/style/NodeJS/vite.config.js diff --git a/frontend/style/app/api/cart/route.ts b/frontend/style/app/api/cart/route.ts new file mode 100644 index 00000000..6ded10c3 --- /dev/null +++ b/frontend/style/app/api/cart/route.ts @@ -0,0 +1,19 @@ +import { NextRequest, NextResponse } from 'next/server' +import { getCart, saveCart, clearCart, CartItem } from '@/lib/cartStorage' + +export async function GET() { + const cart = getCart() + return NextResponse.json(cart) +} + +export async function POST(request: NextRequest) { + const cartItems: CartItem[] = await request.json() + saveCart(cartItems) + return NextResponse.json({ message: 'Cart updated successfully' }) +} + +export async function DELETE() { + clearCart() + return NextResponse.json({ message: 'Cart cleared successfully' }) +} + diff --git a/frontend/style/app/api/disable-preview/route.ts b/frontend/style/app/api/disable-preview/route.ts new file mode 100644 index 00000000..0b042a49 --- /dev/null +++ b/frontend/style/app/api/disable-preview/route.ts @@ -0,0 +1,12 @@ +import { NextRequest, NextResponse } from 'next/server' +import { draftMode } from 'next/headers' + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url) + const id = searchParams.get('id') + + draftMode().disable() + + return NextResponse.redirect(new URL(`/product/${id}`, request.url)) +} + diff --git a/frontend/style/app/api/preview/route.ts b/frontend/style/app/api/preview/route.ts new file mode 100644 index 00000000..e78c4360 --- /dev/null +++ b/frontend/style/app/api/preview/route.ts @@ -0,0 +1,23 @@ +import { NextRequest, NextResponse } from 'next/server' + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url) + const secret = searchParams.get('secret') + const id = searchParams.get('id') + + // Check the secret and next parameters + // This secret should be only known to this API route and the CMS + if (secret !== process.env.PREVIEW_SECRET || !id) { + return NextResponse.json({ message: 'Invalid token' }, { status: 401 }) + } + + // Enable Preview Mode by setting the cookies + const res = NextResponse.next() + res.cookies.set('__prerender_bypass', process.env.PRERENDER_BYPASS_TOKEN || '') + res.cookies.set('__next_preview_data', process.env.PREVIEW_DATA_TOKEN || '') + + // Redirect to the path from the fetched post + // We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities + return NextResponse.redirect(new URL(`/product/${id}`, request.url)) +} + diff --git a/frontend/style/app/cart/page.tsx b/frontend/style/app/cart/page.tsx index fdcd2174..b860b1d8 100644 --- a/frontend/style/app/cart/page.tsx +++ b/frontend/style/app/cart/page.tsx @@ -4,17 +4,12 @@ import { CartSummary } from "@/components/cart-summary" export default function CartPage() { return (
-
+

Корзина

+
-
-

Корзина

- -
-
+
diff --git a/frontend/style/app/favicon.ico b/frontend/style/app/favicon.ico deleted file mode 100644 index 718d6fea..00000000 Binary files a/frontend/style/app/favicon.ico and /dev/null differ diff --git a/frontend/style/app/favorites/page.tsx b/frontend/style/app/favorites/page.tsx new file mode 100644 index 00000000..55382187 --- /dev/null +++ b/frontend/style/app/favorites/page.tsx @@ -0,0 +1,10 @@ +import { FavoriteItems } from "@/components/favorite-items" + +export default function FavoritesPage() { + return ( +
+ +
+ ) +} + diff --git a/frontend/style/app/fonts/GeistMonoVF.woff b/frontend/style/app/fonts/GeistMonoVF.woff deleted file mode 100644 index f2ae185c..00000000 Binary files a/frontend/style/app/fonts/GeistMonoVF.woff and /dev/null differ diff --git a/frontend/style/app/fonts/GeistVF.woff b/frontend/style/app/fonts/GeistVF.woff deleted file mode 100644 index 1b62daac..00000000 Binary files a/frontend/style/app/fonts/GeistVF.woff and /dev/null differ diff --git a/frontend/style/app/globals.css b/frontend/style/app/globals.css deleted file mode 100644 index 1dcb0fc6..00000000 --- a/frontend/style/app/globals.css +++ /dev/null @@ -1,78 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -body { - font-family: Arial, Helvetica, sans-serif; -} - -@layer utilities { - .text-balance { - text-wrap: balance; - } -} - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 0 0% 3.9%; - --card: 0 0% 100%; - --card-foreground: 0 0% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 0 0% 3.9%; - --primary: 0 0% 9%; - --primary-foreground: 0 0% 98%; - --secondary: 0 0% 96.1%; - --secondary-foreground: 0 0% 9%; - --muted: 0 0% 96.1%; - --muted-foreground: 0 0% 45.1%; - --accent: 0 0% 96.1%; - --accent-foreground: 0 0% 9%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 89.8%; - --input: 0 0% 89.8%; - --ring: 0 0% 3.9%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 0.5rem; - } - .dark { - --background: 0 0% 3.9%; - --foreground: 0 0% 98%; - --card: 0 0% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 0 0% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 0 0% 9%; - --secondary: 0 0% 14.9%; - --secondary-foreground: 0 0% 98%; - --muted: 0 0% 14.9%; - --muted-foreground: 0 0% 63.9%; - --accent: 0 0% 14.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 0 0% 14.9%; - --input: 0 0% 14.9%; - --ring: 0 0% 83.1%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } -} - -@layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/frontend/style/app/layout.tsx b/frontend/style/app/layout.tsx index a36cde01..7f3415c1 100644 --- a/frontend/style/app/layout.tsx +++ b/frontend/style/app/layout.tsx @@ -1,35 +1,35 @@ -import type { Metadata } from "next"; -import localFont from "next/font/local"; -import "./globals.css"; +import type { Metadata } from "next" +import { Inter } from 'next/font/google' +import { Header } from "@/components/header" +import { CartProvider } from "@/contexts/cart-context" +import { FavoritesProvider } from "@/contexts/favorites-context" +import { AuthProvider } from "@/contexts/auth-context" -const geistSans = localFont({ - src: "./fonts/GeistVF.woff", - variable: "--font-geist-sans", - weight: "100 900", -}); -const geistMono = localFont({ - src: "./fonts/GeistMonoVF.woff", - variable: "--font-geist-mono", - weight: "100 900", -}); +const inter = Inter({ subsets: ["latin"] }) export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; + title: "Online Store", + description: "E-commerce platform", +} export default function RootLayout({ children, -}: Readonly<{ - children: React.ReactNode; -}>) { +}: { + children: React.ReactNode +}) { return ( - - {children} + + + + +
+
{children}
+ + + - ); + ) } + diff --git a/frontend/style/app/login/page.tsx b/frontend/style/app/login/page.tsx new file mode 100644 index 00000000..9d5f954e --- /dev/null +++ b/frontend/style/app/login/page.tsx @@ -0,0 +1,52 @@ +"use client" + +import { useState } from "react" +import { useRouter } from "next/navigation" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { useAuth } from "@/contexts/auth-context" + +export default function LoginPage() { + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const router = useRouter() + const { login } = useAuth() + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + // In a real application, you would validate credentials here + login() + router.push("/cart") + } + + return ( +
+

Войти

+
+
+ + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+ +
+
+ ) +} + diff --git a/frontend/style/app/page.tsx b/frontend/style/app/page.tsx index 835789de..58c84e52 100644 --- a/frontend/style/app/page.tsx +++ b/frontend/style/app/page.tsx @@ -1,23 +1,21 @@ import { ProductGrid } from "@/components/product-grid" -import { Header } from "@/components/header" - import { Banner } from "@/components/banner" import { ProductFilters } from "@/components/product-filters" import { Footer } from "@/components/footer" +import { SAMPLE_PRODUCTS } from "@/lib/sample-products" export default function HomePage() { return ( <> - -
-
- +
-
- +
+
+ +

Специальные предложения!

- +
diff --git a/frontend/style/app/product/[id]/page.tsx b/frontend/style/app/product/[id]/page.tsx new file mode 100644 index 00000000..67e58729 --- /dev/null +++ b/frontend/style/app/product/[id]/page.tsx @@ -0,0 +1,50 @@ +import { notFound } from 'next/navigation' +import { ProductDetail } from "@/components/product-detail" +import { SAMPLE_PRODUCTS } from "@/lib/sample-products" +import { draftMode } from 'next/headers' + +async function getProduct(id: string, isDraft: boolean) { + // In a real application, you would fetch the product from an API or database + // For this example, we'll use the SAMPLE_PRODUCTS + const product = SAMPLE_PRODUCTS.find(p => p.id === parseInt(id)) + + if (!product) { + return null + } + + if (isDraft) { + // Simulate draft data + return { + ...product, + title: `[Draft] ${product.title}`, + price: product.price * 0.9, // 10% discount in draft mode + } + } + + return product +} + +export default async function ProductPage({ params }: { params: { id: string } }) { + const draftModeData = draftMode() + const isDraftMode = draftModeData ? draftModeData.isEnabled : false + const product = await getProduct(params.id, isDraftMode) + + if (!product) { + notFound() + } + + return ( +
+ + {isDraftMode && ( +
+ Preview Mode Enabled - {' '} + + Exit Preview Mode + +
+ )} +
+ ) +} + diff --git a/frontend/style/app/profile/page.tsx b/frontend/style/app/profile/page.tsx new file mode 100644 index 00000000..2939175f --- /dev/null +++ b/frontend/style/app/profile/page.tsx @@ -0,0 +1,11 @@ +import { UserProfile } from "@/components/user-profile" + +export default function ProfilePage() { + return ( +
+

Личный кабинет

+ +
+ ) +} + diff --git a/frontend/style/app/register/page.tsx b/frontend/style/app/register/page.tsx new file mode 100644 index 00000000..1adbf179 --- /dev/null +++ b/frontend/style/app/register/page.tsx @@ -0,0 +1,11 @@ +import { RegisterForm } from "@/components/register-form" + +export default function RegisterPage() { + return ( +
+

Регистрация

+ +
+ ) +} + diff --git a/frontend/style/app/search/page.tsx b/frontend/style/app/search/page.tsx new file mode 100644 index 00000000..8ff7d639 --- /dev/null +++ b/frontend/style/app/search/page.tsx @@ -0,0 +1,25 @@ +import { ProductGrid } from "@/components/product-grid" +import { SAMPLE_PRODUCTS } from "@/lib/sample-products" + +export default function SearchPage({ + searchParams +}: { + searchParams: { q: string } +}) { + const searchTerm = searchParams.q || '' + const filteredProducts = SAMPLE_PRODUCTS.filter(product => + product.title.toLowerCase().includes(searchTerm.toLowerCase()) + ) + + return ( +
+

Результаты поиска для "{searchTerm}"

+ {filteredProducts.length > 0 ? ( + + ) : ( +

По вашему запросу ничего не найдено

+ )} +
+ ) +} + diff --git a/frontend/style/components/banner.tsx b/frontend/style/components/banner.tsx index 14ac1ce6..a65b6ab1 100644 --- a/frontend/style/components/banner.tsx +++ b/frontend/style/components/banner.tsx @@ -15,7 +15,7 @@ export function Banner() {

САМОЕ ЗАВЕТНОЕ!

- Исполняйте мечты с Eternos + Исполняйте мечты с Ozon Рассрочкой

)}
- {SAMPLE_ITEMS.map((item) => ( + {items.map((item) => (
toggleItem(item.id)} /> {item.name}
-

{item.name}

+

{item.title}

- - 1 -
- -
-
{item.price} ₽
-
- {item.oldPrice} ₽ +
{item.price * item.quantity} ₽
+
+ {item.price} ₽ за шт.
diff --git a/frontend/style/components/cart-summary.tsx b/frontend/style/components/cart-summary.tsx index 8cf8c46d..443d34fa 100644 --- a/frontend/style/components/cart-summary.tsx +++ b/frontend/style/components/cart-summary.tsx @@ -1,25 +1,49 @@ +"use client" + +import { useCart } from "@/contexts/cart-context" +import { useAuth } from "@/contexts/auth-context" import { Button } from "./ui/button" +import { useRouter } from "next/navigation" export function CartSummary() { + const { items, getTotalItems } = useCart() + const { isLoggedIn } = useAuth() + const router = useRouter() + + const totalPrice = items.reduce((sum, item) => sum + item.price * item.quantity, 0) + + const handleCheckout = () => { + if (!isLoggedIn) { + router.push('/login') + } else { + router.push('/checkout') + } + } + + if (getTotalItems() === 0) { + return ( +
+

Ваша корзина

+

В вашей корзине пока нет товаров

+
+ ) + } + return (

Ваша корзина

- Товары (2) - 10 236 ₽ -
-
- Скидка - - 4 387 ₽ + Товары ({getTotalItems()}) + {totalPrice} ₽
- С Ozon Картой - 5 467 ₽ + Итого + {totalPrice} ₽
-
) diff --git a/frontend/style/components/favorite-items.tsx b/frontend/style/components/favorite-items.tsx new file mode 100644 index 00000000..e024a195 --- /dev/null +++ b/frontend/style/components/favorite-items.tsx @@ -0,0 +1,50 @@ +"use client" + +import { useFavorites } from "@/contexts/favorites-context" +import { useCart } from "@/contexts/cart-context" +import { Button } from "./ui/button" +import { ShoppingCart, Trash } from 'lucide-react' +import Image from "next/image" + +export function FavoriteItems() { + const { items, removeFromFavorites } = useFavorites() + const { addToCart } = useCart() + + if (items.length === 0) { + return ( +
+

Избранное

+

У вас пока нет избранных товаров

+
+ ) + } + + return ( +
+ {items.map((item) => ( +
+ {item.title} +
+

{item.title}

+
{item.price} ₽
+
+ + +
+
+
+ ))} +
+ ) +} + diff --git a/frontend/style/components/favorites-context.tsx b/frontend/style/components/favorites-context.tsx new file mode 100644 index 00000000..3a3dceee --- /dev/null +++ b/frontend/style/components/favorites-context.tsx @@ -0,0 +1,59 @@ +"use client" + +import React, { createContext, useContext, useState, useCallback } from 'react' + +type FavoriteItem = { + id: number + title: string + price: number +} + +type FavoritesContextType = { + items: FavoriteItem[] + addToFavorites: (item: FavoriteItem) => void + removeFromFavorites: (id: number) => void + isFavorite: (id: number) => boolean + getTotalFavorites: () => number +} + +const FavoritesContext = createContext(undefined) + +export const useFavorites = () => { + const context = useContext(FavoritesContext) + if (!context) { + throw new Error('useFavorites must be used within a FavoritesProvider') + } + return context +} + +export const FavoritesProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [items, setItems] = useState([]) + + const addToFavorites = useCallback((newItem: FavoriteItem) => { + setItems(currentItems => { + if (!currentItems.some(item => item.id === newItem.id)) { + return [...currentItems, newItem] + } + return currentItems + }) + }, []) + + const removeFromFavorites = useCallback((id: number) => { + setItems(currentItems => currentItems.filter(item => item.id !== id)) + }, []) + + const isFavorite = useCallback((id: number) => { + return items.some(item => item.id === id) + }, [items]) + + const getTotalFavorites = useCallback(() => { + return items.length + }, [items]) + + return ( + + {children} + + ) +} + diff --git a/frontend/style/components/footer.tsx b/frontend/style/components/footer.tsx index a2d2571b..92a76ba4 100644 --- a/frontend/style/components/footer.tsx +++ b/frontend/style/components/footer.tsx @@ -5,7 +5,7 @@ export function Footer() { return (