Update project 4 frontend start
This commit is contained in:
3
frontend-client/.eslintrc.json
Normal file
3
frontend-client/.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
36
frontend-client/.gitignore
vendored
Normal file
36
frontend-client/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
7
frontend-client/Dockerfile
Normal file
7
frontend-client/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "run", "dev"]
|
||||
36
frontend-client/README.md
Normal file
36
frontend-client/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
13
frontend-client/next.config.mjs
Normal file
13
frontend-client/next.config.mjs
Normal file
@@ -0,0 +1,13 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "images.unsplash.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
6167
frontend-client/package-lock.json
generated
Normal file
6167
frontend-client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
frontend-client/package.json
Normal file
29
frontend-client/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "frontend-client",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.6",
|
||||
"lucide-react": "^0.577.0",
|
||||
"next": "14.2.35",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"zustand": "^5.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.35",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
8
frontend-client/postcss.config.mjs
Normal file
8
frontend-client/postcss.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
18
frontend-client/src/api/client.ts
Normal file
18
frontend-client/src/api/client.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import axios from "axios";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8081/api",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
apiClient.interceptors.request.use((config) => {
|
||||
// getState() — безопасен вне React-дерева (server actions, route handlers тоже работают)
|
||||
const token = useAuthStore.getState().token;
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
export default apiClient;
|
||||
BIN
frontend-client/src/app/favicon.ico
Normal file
BIN
frontend-client/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
frontend-client/src/app/fonts/GeistMonoVF.woff
Normal file
BIN
frontend-client/src/app/fonts/GeistMonoVF.woff
Normal file
Binary file not shown.
BIN
frontend-client/src/app/fonts/GeistVF.woff
Normal file
BIN
frontend-client/src/app/fonts/GeistVF.woff
Normal file
Binary file not shown.
13
frontend-client/src/app/globals.css
Normal file
13
frontend-client/src/app/globals.css
Normal file
@@ -0,0 +1,13 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--bg: #121212;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #121212;
|
||||
color: #ffffff;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
24
frontend-client/src/app/layout.tsx
Normal file
24
frontend-client/src/app/layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin", "cyrillic"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Fight Tickets",
|
||||
description: "Бронирование билетов на спортивные события",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="ru">
|
||||
<body className={`${inter.className} bg-[#121212] text-white`}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
120
frontend-client/src/app/page.tsx
Normal file
120
frontend-client/src/app/page.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Search, CalendarDays, Ticket, User } from "lucide-react";
|
||||
import EventCard, { type EventItem } from "@/components/EventCard";
|
||||
|
||||
const MOCK_EVENTS: EventItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Чемпионат по ММА",
|
||||
date: "15 Апреля",
|
||||
time: "19:00",
|
||||
venue: "ВТБ Арена",
|
||||
city: "Москва",
|
||||
priceFrom: 2500,
|
||||
imageSrc:
|
||||
"https://images.unsplash.com/photo-1549719386-74dfcbf7dbed?w=400&q=80",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Турнир по Боксу: "Удар года"',
|
||||
date: "",
|
||||
time: "",
|
||||
venue: "ВТБ Арена",
|
||||
city: "Москва",
|
||||
priceFrom: 2500,
|
||||
imageSrc:
|
||||
"https://images.unsplash.com/photo-1517649763962-0c623066013b?w=400&q=80",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Турнир по Бои",
|
||||
date: "15 Апреля",
|
||||
time: "19:00",
|
||||
venue: "ВТБ Арена",
|
||||
city: "Москва",
|
||||
priceFrom: 3000,
|
||||
imageSrc:
|
||||
"https://images.unsplash.com/photo-1484810934922-571fd7d1b41d?w=400&q=80",
|
||||
},
|
||||
];
|
||||
|
||||
const FILTERS = ["Все", "Ближайшие", "Популярные", "Скоро"] as const;
|
||||
type Filter = (typeof FILTERS)[number];
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ label: "События", icon: CalendarDays },
|
||||
{ label: "Мои билеты", icon: Ticket },
|
||||
{ label: "Профиль", icon: User },
|
||||
] as const;
|
||||
|
||||
export default function HomePage() {
|
||||
const [activeFilter, setActiveFilter] = useState<Filter>("Все");
|
||||
const [activeNav, setActiveNav] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="flex justify-center min-h-screen bg-[#121212]">
|
||||
{/* Phone-width wrapper */}
|
||||
<div className="relative w-full max-w-[390px] flex flex-col min-h-screen bg-[#121212]">
|
||||
{/* ── Header ── */}
|
||||
<div className="flex items-center justify-between px-5 pt-12 pb-4">
|
||||
<h1 className="text-[32px] font-bold leading-tight tracking-tight text-white">
|
||||
События
|
||||
</h1>
|
||||
<button
|
||||
aria-label="Поиск"
|
||||
className="p-2 rounded-full text-white hover:bg-[#2C2C2E] transition-colors"
|
||||
>
|
||||
<Search size={24} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ── Filter tabs ── */}
|
||||
<div className="flex gap-2 px-5 pb-4 overflow-x-auto scrollbar-hide">
|
||||
{FILTERS.map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setActiveFilter(f)}
|
||||
className={`flex-shrink-0 px-4 py-2 rounded-full text-sm font-medium transition-colors ${
|
||||
activeFilter === f
|
||||
? "bg-white text-[#121212]"
|
||||
: "bg-[#2C2C2E] text-[#8E8E93] hover:bg-[#3A3A3C]"
|
||||
}`}
|
||||
>
|
||||
{f}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ── Event list ── */}
|
||||
<div className="flex flex-col gap-3 px-4 flex-1 pb-24">
|
||||
{MOCK_EVENTS.map((event) => (
|
||||
<EventCard key={event.id} event={event} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ── Bottom navigation ── */}
|
||||
<nav className="fixed bottom-0 left-1/2 -translate-x-1/2 w-full max-w-[390px] bg-[#1C1C1E] border-t border-[#2C2C2E]">
|
||||
<div className="flex">
|
||||
{NAV_ITEMS.map(({ label, icon: Icon }, idx) => (
|
||||
<button
|
||||
key={label}
|
||||
onClick={() => setActiveNav(idx)}
|
||||
className={`flex-1 flex flex-col items-center gap-1 py-3 text-[10px] font-medium transition-colors ${
|
||||
activeNav === idx ? "text-white" : "text-[#8E8E93]"
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
size={22}
|
||||
strokeWidth={activeNav === idx ? 2.5 : 1.8}
|
||||
/>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
frontend-client/src/components/EventCard.tsx
Normal file
71
frontend-client/src/components/EventCard.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { MapPin, Calendar } from "lucide-react";
|
||||
|
||||
export interface EventItem {
|
||||
id: number;
|
||||
title: string;
|
||||
date: string;
|
||||
time: string;
|
||||
venue: string;
|
||||
city: string;
|
||||
priceFrom: number;
|
||||
imageSrc: string;
|
||||
}
|
||||
|
||||
interface EventCardProps {
|
||||
event: EventItem;
|
||||
}
|
||||
|
||||
export default function EventCard({ event }: EventCardProps) {
|
||||
return (
|
||||
<div className="flex gap-4 bg-[#1C1C1E] rounded-[16px] overflow-hidden p-3">
|
||||
{/* Poster thumbnail */}
|
||||
<div className="relative w-[110px] h-[110px] flex-shrink-0 rounded-xl overflow-hidden">
|
||||
<Image
|
||||
src={event.imageSrc}
|
||||
alt={event.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="110px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex flex-col justify-between flex-1 min-w-0 py-0.5">
|
||||
<p className="font-bold text-[15px] leading-tight text-white line-clamp-2">
|
||||
{event.title}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-1 mt-1">
|
||||
{event.date && (
|
||||
<div className="flex items-center gap-1.5 text-[#8E8E93] text-xs">
|
||||
<Calendar size={12} strokeWidth={2} />
|
||||
<span>
|
||||
{event.date}, {event.time}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5 text-[#8E8E93] text-xs">
|
||||
<MapPin size={12} strokeWidth={2} />
|
||||
<span className="truncate">
|
||||
{event.venue}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[#8E8E93] text-xs">{event.city}</span>
|
||||
</div>
|
||||
|
||||
{/* Price + CTA */}
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="text-white text-sm font-medium">
|
||||
от {event.priceFrom.toLocaleString("ru-RU")} ₽
|
||||
</span>
|
||||
<button className="bg-accent hover:bg-accent-hover transition-colors text-white text-xs font-semibold px-4 py-2 rounded-full whitespace-nowrap">
|
||||
Выбрать билет
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
frontend-client/src/store/authStore.ts
Normal file
26
frontend-client/src/store/authStore.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
interface AuthUser {
|
||||
id: number;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
token: string | null;
|
||||
user: AuthUser | null;
|
||||
setAuth: (token: string, user: AuthUser) => void;
|
||||
clearAuth: () => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
token: null,
|
||||
user: null,
|
||||
setAuth: (token, user) => set({ token, user }),
|
||||
clearAuth: () => set({ token: null, user: null }),
|
||||
}),
|
||||
{ name: "auth-storage" }
|
||||
)
|
||||
);
|
||||
31
frontend-client/src/store/cartStore.ts
Normal file
31
frontend-client/src/store/cartStore.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
interface CartSeat {
|
||||
seatId: number;
|
||||
sector: string;
|
||||
row: number;
|
||||
number: number;
|
||||
price: number;
|
||||
}
|
||||
|
||||
interface CartState {
|
||||
seats: CartSeat[];
|
||||
addSeat: (seat: CartSeat) => void;
|
||||
removeSeat: (seatId: number) => void;
|
||||
clearCart: () => void;
|
||||
totalPrice: () => number;
|
||||
}
|
||||
|
||||
export const useCartStore = create<CartState>()((set, get) => ({
|
||||
seats: [],
|
||||
addSeat: (seat) =>
|
||||
set((state) => ({
|
||||
seats: state.seats.some((s) => s.seatId === seat.seatId)
|
||||
? state.seats
|
||||
: [...state.seats, seat],
|
||||
})),
|
||||
removeSeat: (seatId) =>
|
||||
set((state) => ({ seats: state.seats.filter((s) => s.seatId !== seatId) })),
|
||||
clearCart: () => set({ seats: [] }),
|
||||
totalPrice: () => get().seats.reduce((sum, s) => sum + s.price, 0),
|
||||
}));
|
||||
25
frontend-client/tailwind.config.ts
Normal file
25
frontend-client/tailwind.config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
surface: "#1C1C1E",
|
||||
accent: "#E32636",
|
||||
"accent-hover": "#C41E2A",
|
||||
muted: "#8E8E93",
|
||||
border: "#2C2C2E",
|
||||
},
|
||||
borderRadius: {
|
||||
card: "16px",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
export default config;
|
||||
26
frontend-client/tsconfig.json
Normal file
26
frontend-client/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user