From ad35d4e992647d43fe6182a5fbd820cdf1b0fd4e Mon Sep 17 00:00:00 2001 From: german Date: Wed, 5 Aug 2026 19:42:13 +0300 Subject: [PATCH] =?UTF-8?q?feat(#3):=20frontend=20scaffold=20(Next.js=2014?= =?UTF-8?q?=20App=20Router,=20TS,=20Tailwind,=20=D0=B3=D0=B0=D0=BB=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D1=8F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Каркас витрины дизайнера Ники: главная (hero + сетка галереи), страница работы (детали + место под аукцион), список аукционов, подписка-заглушка. Клиент к backend /api с graceful-фолбэком на демо-работы при недоступном API. Standalone-сборка + Dockerfile. Сборка npm run build и tsc --noEmit проходят. Co-Authored-By: Claude Opus 4.8 --- frontend/.dockerignore | 7 ++ frontend/.gitignore | 9 ++ frontend/Dockerfile | 30 +++++ frontend/app/auctions/page.tsx | 104 +++++++++++++++++ frontend/app/globals.css | 49 ++++++++ frontend/app/layout.tsx | 52 +++++++++ frontend/app/not-found.tsx | 21 ++++ frontend/app/page.tsx | 55 +++++++++ frontend/app/subscribe/page.tsx | 36 ++++++ frontend/app/works/[id]/page.tsx | 173 ++++++++++++++++++++++++++++ frontend/components/Footer.tsx | 37 ++++++ frontend/components/GalleryGrid.tsx | 28 +++++ frontend/components/Nav.tsx | 42 +++++++ frontend/components/WorkCard.tsx | 55 +++++++++ frontend/lib/api.ts | 103 +++++++++++++++++ frontend/lib/demo.ts | 42 +++++++ frontend/lib/format.ts | 43 +++++++ frontend/lib/types.ts | 79 +++++++++++++ frontend/next.config.mjs | 16 +++ frontend/package.json | 27 +++++ frontend/postcss.config.mjs | 9 ++ frontend/public/.gitkeep | 0 frontend/tailwind.config.ts | 39 +++++++ frontend/tsconfig.json | 23 ++++ 24 files changed, 1079 insertions(+) create mode 100644 frontend/.dockerignore create mode 100644 frontend/.gitignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/app/auctions/page.tsx create mode 100644 frontend/app/globals.css create mode 100644 frontend/app/layout.tsx create mode 100644 frontend/app/not-found.tsx create mode 100644 frontend/app/page.tsx create mode 100644 frontend/app/subscribe/page.tsx create mode 100644 frontend/app/works/[id]/page.tsx create mode 100644 frontend/components/Footer.tsx create mode 100644 frontend/components/GalleryGrid.tsx create mode 100644 frontend/components/Nav.tsx create mode 100644 frontend/components/WorkCard.tsx create mode 100644 frontend/lib/api.ts create mode 100644 frontend/lib/demo.ts create mode 100644 frontend/lib/format.ts create mode 100644 frontend/lib/types.ts create mode 100644 frontend/next.config.mjs create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.mjs create mode 100644 frontend/public/.gitkeep create mode 100644 frontend/tailwind.config.ts create mode 100644 frontend/tsconfig.json diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..2529f18 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,7 @@ +node_modules +.next +npm-debug.log +Dockerfile +.dockerignore +.git +.env* diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..eb8da1c --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,9 @@ +# Next.js / Node +node_modules/ +.next/ +out/ +next-env.d.ts +*.tsbuildinfo +.env +.env.local +npm-debug.log* diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..e6a1477 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,30 @@ +# Next.js 14 standalone — многоступенчатая сборка (см. output:'standalone'). +FROM node:20-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm ci || npm install + +FROM node:20-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +# API-база вшивается в клиентский бандл на этапе сборки. +ARG NEXT_PUBLIC_API_URL +ARG NEXT_PUBLIC_WS_URL +ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL +ENV NEXT_PUBLIC_WS_URL=$NEXT_PUBLIC_WS_URL +ENV NEXT_TELEMETRY_DISABLED=1 +RUN npm run build + +FROM node:20-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001 +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +USER nextjs +EXPOSE 3000 +ENV PORT=3000 HOSTNAME=0.0.0.0 +CMD ["node", "server.js"] diff --git a/frontend/app/auctions/page.tsx b/frontend/app/auctions/page.tsx new file mode 100644 index 0000000..84c24e6 --- /dev/null +++ b/frontend/app/auctions/page.tsx @@ -0,0 +1,104 @@ +import Link from 'next/link'; +import Image from 'next/image'; +import type { Metadata } from 'next'; +import { api, safe } from '@/lib/api'; +import type { Auction } from '@/lib/types'; +import { formatPrice, timeLeft, placeholderImage } from '@/lib/format'; + +export const revalidate = 30; + +export const metadata: Metadata = { + title: 'Аукционы', + description: 'Открытые аукционы на работы Ники: первичные и вторичные торги.', +}; + +const statusLabel: Record = { + pending: 'Скоро', + active: 'Идёт', + ended: 'Завершён', + sold: 'Продано', + cancelled: 'Отменён', +}; + +export default async function AuctionsPage() { + const { data: auctions, ok } = await safe( + () => api.listAuctions({ status: 'active' }), + [] as Auction[], + ); + + return ( +
+
+

Торги

+

+ Аукционы +

+

+ Первичные торги на новые работы и вторичные — на работы из частных + собраний. Ставки в реальном времени. +

+
+ + {ok && auctions.length > 0 ? ( +
    + {auctions.map((a) => ( + + ))} +
+ ) : ( + + )} +
+ ); +} + +function AuctionCard({ auction }: { auction: Auction }) { + const title = auction.work?.title || 'Работа'; + const cover = + auction.work?.cover_url || placeholderImage(auction.work_id, 800, 1000); + return ( +
  • + +
    + {title} + + {statusLabel[auction.status] ?? auction.status} + +
    +
    +

    {title}

    + {timeLeft(auction.end_at)} +
    +

    + Текущая ставка · {formatPrice(auction.current_price)} +

    + +
  • + ); +} + +function EmptyState() { + return ( +
    +

    + Сейчас активных аукционов нет +

    +

    + Следите за анонсами новых торгов. Подпишитесь, чтобы узнать первыми, — + приём ставок в реальном времени появится в задаче #5. +

    + + Подписаться на анонсы + +
    + ); +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 0000000..b1b717f --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,49 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --bg: #f7f5f1; + --fg: #1a1a1a; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #121110; + --fg: #ece8e1; + } +} + +html { + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +body { + background: var(--bg); + color: var(--fg); +} + +/* Тонкая линия-разделитель в галерейном стиле */ +.hairline { + border-color: var(--line, #e4e0d8); +} + +/* Плавное появление изображений */ +@keyframes fade-in { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} +.animate-fade-in { + animation: fade-in 0.6s cubic-bezier(0.22, 1, 0.36, 1) both; +} + +/* Скелетон-пульсация для загрузки */ +@keyframes shimmer { + 0% { opacity: 0.55; } + 50% { opacity: 1; } + 100% { opacity: 0.55; } +} +.animate-shimmer { + animation: shimmer 1.6s ease-in-out infinite; +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000..d5a095f --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,52 @@ +import type { Metadata } from 'next'; +import { Cormorant_Garamond, Inter } from 'next/font/google'; +import './globals.css'; +import Nav from '@/components/Nav'; +import Footer from '@/components/Footer'; + +// Заголовочный серифный шрифт — воздушный, «галерейный». +const display = Cormorant_Garamond({ + subsets: ['latin', 'cyrillic'], + weight: ['400', '500', '600'], + variable: '--font-display', + display: 'swap', +}); + +// Текстовый гротеск — нейтральный, для интерфейса и подписей. +const body = Inter({ + subsets: ['latin', 'cyrillic'], + variable: '--font-body', + display: 'swap', +}); + +export const metadata: Metadata = { + title: { + default: 'Ника — галерея и аукцион', + template: '%s · Ника', + }, + description: + 'Галерея работ дизайнера Ники: живопись и графика, аукцион, подписка на новые работы.', + openGraph: { + title: 'Ника — галерея и аукцион', + description: + 'Галерея работ дизайнера Ники: живопись и графика, аукцион, подписка.', + type: 'website', + locale: 'ru_RU', + }, +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + +