This commit is contained in:
itqop 2025-12-07 19:44:08 +03:00
commit d07b9bacff
62 changed files with 2870 additions and 0 deletions

35
.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# Dependencies
/node_modules
/.pnp
.pnp.js
# 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
.env
# Vercel
.vercel
# TypeScript
*.tsbuildinfo
next-env.d.ts

7
.nextignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
.next
out
.git
.env*.local
*.log

123
README.md Normal file
View File

@ -0,0 +1,123 @@
# Romanovna Photo - Сайт-портфолио фотографа
Минималистичный, эстетичный, тёмно-винтажный сайт-витрина для фотографа Ангелины Чёрной.
## 🚀 Быстрый старт
### Установка зависимостей
```bash
npm install
```
### Запуск в режиме разработки
```bash
npm run dev
```
Откройте [http://localhost:3000](http://localhost:3000) в браузере.
### Сборка для продакшена
```bash
npm run build
npm start
```
## 📁 Структура проекта
```
romanovna-photo/
├── public/
│ ├── logo/
│ │ └── romanovna-logo.png # Логотип (уже добавлен)
│ └── images/
│ ├── carousel/ # Изображения для карусели (6 шт.)
│ └── services/ # Обложки и примеры для услуг
├── src/
│ ├── app/ # Страницы Next.js
│ ├── components/ # React компоненты
│ ├── lib/ # Конфигурационные данные
│ └── styles/ # CSS модули
```
## 🖼️ Добавление изображений
### Карусель работ
Добавьте 6 изображений в `public/images/carousel/`:
- `work-1.jpg`
- `work-2.jpg`
- `work-3.jpg`
- `work-4.jpg`
- `work-5.jpg`
- `work-6.jpg`
### Услуги
Для каждой услуги добавьте изображения в `public/images/services/`:
**Фотосессия на улице (street):**
- `street-cover.jpg` (обложка)
- `street-example-1.jpg`
- `street-example-2.jpg`
- `street-example-3.jpg`
**Фотосессия в студии (studio):**
- `studio-cover.jpg` (обложка)
- `studio-example-1.jpg`
- `studio-example-2.jpg`
**Ретушь (retouch):**
- `retouch-cover.jpg` (обложка)
- `retouch-example-1.jpg`
- `retouch-example-2.jpg`
**Сертификат (certificate):**
- `certificate-cover.jpg` (обложка)
- `certificate-example-1.jpg`
## 🗺️ Настройка Яндекс.Карты
✅ Яндекс.Карта уже настроена и готова к использованию!
Если нужно изменить карту:
1. Создайте новую карту на [Яндекс.Конструкторе карт](https://yandex.ru/map-constructor/)
2. Скопируйте код скрипта
3. Замените URL скрипта в `src/components/contacts/YandexMap.tsx`
## 🎨 Цветовая палитра
- **Акцентный**: `#A64456` (винтажный красно-розовый)
- **Песочный**: `#BF9B7A`
- **Светло-винтажный**: `#D9B79A`
- **Коричневый винтаж**: `#8C654F`
- **Тёмный**: `#594336`
- **Фон**: `#1a1a1a`
## 📝 Настройка данных
Все данные о компании, услугах и галерее находятся в `src/lib/`:
- `company.ts` - информация о компании
- `services.ts` - список услуг
- `gallery.ts` - изображения для карусели
## 🛠️ Технологии
- **Next.js 14** - React фреймворк
- **TypeScript** - типизация
- **CSS Modules** - стилизация
- **Google Fonts** - типографика (Playfair Display, Inter)
## 📱 Адаптивность
Сайт полностью адаптирован для:
- Desktop (1200px+)
- Tablet (768px - 1199px)
- Mobile (до 767px)
## 📄 Лицензия
Проект создан для фотографа Ангелины Чёрной (@romanovnaph_ch).

10
next.config.js Normal file
View File

@ -0,0 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
formats: ['image/avif', 'image/webp'],
remotePatterns: [],
},
};
module.exports = nextConfig;

10
next.config.mjs Normal file
View File

@ -0,0 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
formats: ['image/webp', 'image/avif'],
},
};
export default nextConfig;

498
package-lock.json generated Normal file
View File

@ -0,0 +1,498 @@
{
"name": "romanovna-photo",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "romanovna-photo",
"version": "1.0.0",
"dependencies": {
"next": "^14.2.0",
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"devDependencies": {
"@types/node": "^20.11.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"typescript": "^5.3.0"
}
},
"node_modules/@next/env": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.33.tgz",
"integrity": "sha512-CgVHNZ1fRIlxkLhIX22flAZI/HmpDaZ8vwyJ/B0SDPTBuLZ1PJ+DWMjCHhqnExfmSQzA/PbZi8OAc7PAq2w9IA==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz",
"integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz",
"integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz",
"integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz",
"integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz",
"integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz",
"integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz",
"integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
"integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz",
"integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
"integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3",
"tslib": "^2.4.0"
}
},
"node_modules/@types/node": {
"version": "20.19.25",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz",
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.27",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.3.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^18.0.0"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001759",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz",
"integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/next": {
"version": "14.2.33",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.33.tgz",
"integrity": "sha512-GiKHLsD00t4ACm1p00VgrI0rUFAC9cRDGReKyERlM57aeEZkOQGcZTpIbsGn0b562FTPJWmYfKwplfO9EaT6ng==",
"license": "MIT",
"dependencies": {
"@next/env": "14.2.33",
"@swc/helpers": "0.5.5",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
"graceful-fs": "^4.2.11",
"postcss": "8.4.31",
"styled-jsx": "5.1.1"
},
"bin": {
"next": "dist/bin/next"
},
"engines": {
"node": ">=18.17.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "14.2.33",
"@next/swc-darwin-x64": "14.2.33",
"@next/swc-linux-arm64-gnu": "14.2.33",
"@next/swc-linux-arm64-musl": "14.2.33",
"@next/swc-linux-x64-gnu": "14.2.33",
"@next/swc-linux-x64-musl": "14.2.33",
"@next/swc-win32-arm64-msvc": "14.2.33",
"@next/swc-win32-ia32-msvc": "14.2.33",
"@next/swc-win32-x64-msvc": "14.2.33"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.41.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.3.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"@playwright/test": {
"optional": true
},
"sass": {
"optional": true
}
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/scheduler": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/styled-jsx": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
"integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
"license": "MIT",
"dependencies": {
"client-only": "0.0.1"
},
"engines": {
"node": ">= 12.0.0"
},
"peerDependencies": {
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"babel-plugin-macros": {
"optional": true
}
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

23
package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "romanovna-photo",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^14.2.0",
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"devDependencies": {
"@types/node": "^20.11.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"typescript": "^5.3.0"
}
}

View File

@ -0,0 +1,170 @@
# AI Prompts для генерации обложек услуг
## Стилистика сайта Romanovna Photography
**Цветовая палитра:**
- Accent: #A64456 (винтажный красно-розовый)
- Sand: #BF9B7A (тёплый песочный)
- Light Vintage: #D9B79A (светло-винтажный беж)
- Brown Vintage: #8C654F (коричневый винтаж)
- Dark: #594336 (глубокий коричневый)
- Background: #1a1a1a (почти чёрный)
**Общее настроение:** Тёмная винтажная эстетика, кинематографичность, минимализм, элегантность
**Слоган:** "Эстетика в каждом кадре"
---
## 1. Фотосессия на улице (street-cover.jpg)
### Промпт для Midjourney/DALL-E:
```
Cinematic street photography scene, elegant young woman in vintage outfit walking on cobblestone European street at golden hour, warm sunset light, bokeh background with old architecture, film grain texture, muted color grading with warm tones (sand #BF9B7A, vintage brown #8C654F), atmospheric haze, shot on film camera aesthetic, vintage rose accents #A64456, professional fashion photography, dreamy and romantic mood, shallow depth of field, 16:9 aspect ratio, photorealistic --ar 16:9 --style raw --stylize 300
```
### Русский промпт:
```
Кинематографичная уличная фотография, элегантная молодая женщина в винтажном наряде идет по европейской мощеной улице на закате, теплый золотой час, боке-фон со старинной архитектурой, зернистость плёнки, приглушенная цветокоррекция в тёплых тонах (песочный, винтажный коричневый), атмосферная дымка, эстетика плёночной камеры, винтажные розовые акценты, профессиональная fashion фотография, мечтательное романтичное настроение, малая глубина резкости, соотношение 16:9, фотореалистично
```
### Ключевые элементы:
- Золотой час / закатный свет
- Европейская архитектура (старые улицы)
- Винтажная одежда
- Тёплая цветокоррекция
- Боке и малая глубина резкости
- Эффект плёнки
---
## 2. Фотосессия в студии (studio-cover.jpg)
### Промпт для Midjourney/DALL-E:
```
Professional studio portrait photography, elegant fashion model in minimalist vintage-style clothing, dramatic studio lighting setup with softbox and rim light, dark moody background (#1a1a1a), warm accent lighting (#A64456 rose tint), high-end fashion editorial style, sharp focus on face, sophisticated pose, muted warm color palette (sand #BF9B7A, vintage brown tones), cinematic atmosphere, beauty dish lighting, professional retouching aesthetic, 16:9 aspect ratio --ar 16:9 --style raw --q 2
```
### Русский промпт:
```
Профессиональная студийная портретная фотография, элегантная fashion-модель в минималистичной винтажной одежде, драматичное студийное освещение с софтбоксами и контровым светом, тёмный мрачный фон, тёплый акцентный свет с розовым оттенком, high-end модный editorial стиль, резкий фокус на лице, изысканная поза, приглушенная тёплая цветовая палитра (песочный, винтажные коричневые тона), кинематографичная атмосфера, освещение beauty dish, эстетика профессиональной ретуши, соотношение 16:9
```
### Ключевые элементы:
- Студийное освещение (софтбоксы)
- Тёмный фон
- Fashion/editorial стиль
- Драматичное освещение
- Минималистичная одежда
- Фокус на модели
---
## 3. Ретушь (retouch-cover.jpg)
### Промпт для Midjourney/DALL-E:
```
Split-screen before and after photo retouching comparison, left side: raw unedited portrait, right side: professionally retouched with perfect skin, cinematic color grading in warm vintage tones (#BF9B7A sand, #A64456 rose accents), elegant female portrait, soft studio lighting, premium beauty retouching showcase, subtle vintage film look, dark moody background, high-end magazine quality, photorealistic detail, 16:9 aspect ratio --ar 16:9 --style raw --q 2
```
### Русский промпт:
```
Сравнение до/после профессиональной ретуши фото в формате split-screen, левая сторона: необработанный портрет, правая сторона: профессионально отретушированный с идеальной кожей, кинематографичная цветокоррекция в тёплых винтажных тонах, элегантный женский портрет, мягкое студийное освещение, демонстрация премиальной beauty-ретуши, тонкий винтажный плёночный вид, тёмный мрачный фон, качество high-end журнала, фотореалистичные детали, соотношение 16:9
```
### Ключевые элементы:
- Split-screen (до/после)
- Демонстрация ретуши кожи
- Цветокоррекция
- Премиальное качество
- Тёплые тона
- Beauty-стиль
---
## 4. Сертификат в подарок (certificate-cover.jpg)
### Промпт для Midjourney/DALL-E:
```
Elegant luxury gift certificate design mockup, vintage-style paper with ornate frame, warm color scheme (#BF9B7A sand, #A64456 vintage rose, #8C654F brown), professional photography gift voucher, minimalist elegant typography (Playfair Display serif), soft shadows on dark background (#1a1a1a), ribbon and wax seal details, premium stationery aesthetic, romantic vintage atmosphere, product photography style, 16:9 aspect ratio, photorealistic rendering --ar 16:9 --style raw --q 2
```
### Русский промпт:
```
Элегантный роскошный дизайн подарочного сертификата, винтажная бумага с орнаментальной рамкой, тёплая цветовая схема (песочный, винтажный розовый, коричневый), подарочный ваучер на профессиональную фотосессию, минималистичная элегантная типографика (serif шрифт Playfair Display), мягкие тени на тёмном фоне, детали с лентой и восковой печатью, премиальная канцелярская эстетика, романтичная винтажная атмосфера, стиль предметной фотографии, соотношение 16:9, фотореалистичный рендер
```
### Ключевые элементы:
- Сертификат/ваучер в кадре
- Винтажный дизайн
- Орнаменты и рамки
- Тёплые цвета палитры
- Элементы роскоши (лента, печать)
- Тёмный фон
---
## Общие рекомендации для всех промптов:
1. **Технические параметры:**
- Размер: 1920x1080px (16:9)
- Качество: высокое, фотореалистичное
- Формат: JPG
- Стиль: raw/professional photography
2. **Цветокоррекция:**
- Обязательно использовать тёплые винтажные тона
- Приглушенная палитра, не яркие цвета
- Акценты: #A64456 (розовый), #BF9B7A (песочный)
- Тени: тёмные, насыщенные (#1a1a1a, #594336)
3. **Настроение:**
- Кинематографичность
- Элегантность и роскошь
- Винтажная эстетика
- Тёмная, мрачная атмосфера (но не депрессивная)
- Романтика и мечтательность
4. **Освещение:**
- Драматичное, но мягкое
- Золотой час для уличных
- Студийное для портретов
- Тёплые оттенки света
5. **Постобработка:**
- Эффект плёнки (grain)
- Малая глубина резкости (bokeh)
- Виньетирование
- Профессиональная цветокоррекция
---
## Альтернативные сервисы для генерации:
- **Midjourney** (рекомендуется) - лучшая фотореалистичность
- **DALL-E 3** - хорошее понимание промптов
- **Stable Diffusion** (модели: Realistic Vision, DreamShaper)
- **Leonardo.ai** - для быстрых итераций
- **Firefly Adobe** - интеграция с Adobe
## Дополнительные ключевые слова для улучшения результата:
```
cinematic, film grain, vintage aesthetic, warm tones, moody, atmospheric, professional photography, editorial style, soft focus, bokeh, golden hour, dramatic lighting, minimalist, elegant, sophisticated, luxury, romantic, dreamy, photorealistic, high-end, premium quality
```
---
**Важно:** После генерации обязательно:
1. Проверить соответствие цветовой палитре сайта
2. При необходимости скорректировать цвета в Lightroom/Photoshop
3. Добавить film grain для винтажности
4. Оптимизировать размер файла (<500KB)
5. Убедиться в едином стиле всех обложек

View File

@ -0,0 +1,33 @@
REQUIRED IMAGES FOR ROMANOVNA PHOTOGRAPHY WEBSITE
================================================================
IMPORTANT: Add the following images for the site to work correctly
1. LOGO
Path: /public/logo/romanovna-logo.png
Size: 200x200px minimum
Format: PNG with transparent background
2. CAROUSEL WORKS (6 images)
Path: /public/images/carousel/
Files: work-1.jpg, work-2.jpg, work-3.jpg, work-4.jpg, work-5.jpg, work-6.jpg
Size: 1920x1200px
3. SERVICES - STREET PHOTOSHOOT
Path: /public/images/services/
- street-cover.jpg (1600x1000px)
- street-1.jpg, street-2.jpg, street-3.jpg (800x1000px)
4. SERVICES - STUDIO PHOTOSHOOT
- studio-cover.jpg (1600x1000px)
- studio-1.jpg, studio-2.jpg, studio-3.jpg (800x1000px)
5. SERVICES - RETOUCH
- retouch-cover.jpg (1600x1000px)
- retouch-1.jpg, retouch-2.jpg (800x1000px)
6. SERVICES - GIFT CERTIFICATE
- certificate-cover.jpg (1600x1000px)
- certificate-1.jpg (800x1000px)
Without these images, the site will display errors or empty blocks.

View File

@ -0,0 +1,46 @@
=================================================================
БЫСТРЫЕ ПРОМПТЫ ДЛЯ КОПИРОВАНИЯ (Romanovna Photography)
=================================================================
Общий стиль: Тёмная винтажная эстетика, кинематографичность
Цвета: #A64456 (розовый), #BF9B7A (песочный), #8C654F (коричневый)
Размер: 1920x1080px (16:9)
-----------------------------------------------------------------
1. ФОТОСЕССИЯ НА УЛИЦЕ (street-cover.jpg)
-----------------------------------------------------------------
Cinematic street photography scene, elegant young woman in vintage outfit walking on cobblestone European street at golden hour, warm sunset light, bokeh background with old architecture, film grain texture, muted color grading with warm tones (sand #BF9B7A, vintage brown #8C654F), atmospheric haze, shot on film camera aesthetic, vintage rose accents #A64456, professional fashion photography, dreamy and romantic mood, shallow depth of field, 16:9 aspect ratio, photorealistic --ar 16:9 --style raw --stylize 300
-----------------------------------------------------------------
2. ФОТОСЕССИЯ В СТУДИИ (studio-cover.jpg)
-----------------------------------------------------------------
Professional studio portrait photography, elegant fashion model in minimalist vintage-style clothing, dramatic studio lighting setup with softbox and rim light, dark moody background (#1a1a1a), warm accent lighting (#A64456 rose tint), high-end fashion editorial style, sharp focus on face, sophisticated pose, muted warm color palette (sand #BF9B7A, vintage brown tones), cinematic atmosphere, beauty dish lighting, professional retouching aesthetic, 16:9 aspect ratio --ar 16:9 --style raw --q 2
-----------------------------------------------------------------
3. РЕТУШЬ (retouch-cover.jpg)
-----------------------------------------------------------------
Split-screen before and after photo retouching comparison, left side: raw unedited portrait, right side: professionally retouched with perfect skin, cinematic color grading in warm vintage tones (#BF9B7A sand, #A64456 rose accents), elegant female portrait, soft studio lighting, premium beauty retouching showcase, subtle vintage film look, dark moody background, high-end magazine quality, photorealistic detail, 16:9 aspect ratio --ar 16:9 --style raw --q 2
-----------------------------------------------------------------
4. СЕРТИФИКАТ В ПОДАРОК (certificate-cover.jpg)
-----------------------------------------------------------------
Elegant luxury gift certificate design mockup, vintage-style paper with ornate frame, warm color scheme (#BF9B7A sand, #A64456 vintage rose, #8C654F brown), professional photography gift voucher, minimalist elegant typography (Playfair Display serif), soft shadows on dark background (#1a1a1a), ribbon and wax seal details, premium stationery aesthetic, romantic vintage atmosphere, product photography style, 16:9 aspect ratio, photorealistic rendering --ar 16:9 --style raw --q 2
=================================================================
ВАЖНО ПОСЛЕ ГЕНЕРАЦИИ:
=================================================================
1. Проверить соответствие цветовой палитре (#A64456, #BF9B7A, #8C654F)
2. Добавить film grain для винтажности
3. Скорректировать цвета если нужно (Lightroom/Photoshop)
4. Оптимизировать до <500KB
5. Размер: 1920x1080px
СЕРВИСЫ:
- Midjourney (лучший результат)
- DALL-E 3
- Leonardo.ai
- Stable Diffusion

48
public/images/README.md Normal file
View File

@ -0,0 +1,48 @@
# Изображения для сайта
## Карусель работ
Добавьте 6 изображений в папку `carousel/`:
- `work-1.jpg`
- `work-2.jpg`
- `work-3.jpg`
- `work-4.jpg`
- `work-5.jpg`
- `work-6.jpg`
**Рекомендации:**
- Формат: JPG или WebP
- Размер: минимум 1920x1080px (16:9)
- Оптимизация: сжатие для веба
## Услуги
### Фотосессия на улице (street)
- `street-cover.jpg` - обложка (1920x1080px)
- `street-example-1.jpg` - пример работы
- `street-example-2.jpg` - пример работы
- `street-example-3.jpg` - пример работы
### Фотосессия в студии (studio)
- `studio-cover.jpg` - обложка (1920x1080px)
- `studio-example-1.jpg` - пример работы
- `studio-example-2.jpg` - пример работы
### Ретушь (retouch)
- `retouch-cover.jpg` - обложка (1920x1080px)
- `retouch-example-1.jpg` - пример работы (до/после)
- `retouch-example-2.jpg` - пример работы (до/после)
### Сертификат в подарок (certificate)
- `certificate-cover.jpg` - обложка (1920x1080px)
- `certificate-example-1.jpg` - пример сертификата
## Оптимизация изображений
Рекомендуется использовать инструменты для оптимизации:
- [Squoosh](https://squoosh.app/) - онлайн оптимизатор
- [ImageOptim](https://imageoptim.com/) - для Mac
- [TinyPNG](https://tinypng.com/) - онлайн сервис
**Целевой размер:** каждое изображение должно быть < 500KB после оптимизации.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 KiB

12
src/app/contacts/page.tsx Normal file
View File

@ -0,0 +1,12 @@
import ContactInfo from '@/components/contacts/ContactInfo';
import YandexMap from '@/components/contacts/YandexMap';
export default function ContactsPage() {
return (
<>
<ContactInfo />
<YandexMap />
</>
);
}

116
src/app/globals.css Normal file
View File

@ -0,0 +1,116 @@
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Inter:wght@300;400;500;600&display=swap');
:root {
/* Цветовая палитра */
--color-accent: #A64456;
--color-sand: #BF9B7A;
--color-light-vintage: #D9B79A;
--color-brown-vintage: #8C654F;
--color-dark: #594336;
--color-bg-dark: #1a1a1a;
--color-bg-secondary: #2a2a2a;
--color-text-primary: #f5f5f5;
--color-text-secondary: #d0d0d0;
--color-text-muted: #a0a0a0;
/* Типографика */
--font-heading: 'Playfair Display', serif;
--font-body: 'Inter', sans-serif;
/* Отступы */
--spacing-xs: 0.5rem;
--spacing-sm: 1rem;
--spacing-md: 2rem;
--spacing-lg: 3rem;
--spacing-xl: 4rem;
/* Переходы */
--transition-base: 0.3s ease;
--transition-slow: 0.5s ease;
/* Тени */
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-body);
background-color: var(--color-bg-dark);
color: var(--color-text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
/* Винтажная текстура (опционально) */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
radial-gradient(circle at 20% 50%, rgba(217, 183, 154, 0.03) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(191, 155, 122, 0.02) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
a {
color: inherit;
text-decoration: none;
transition: color var(--transition-base);
}
a:hover {
color: var(--color-accent);
}
img {
max-width: 100%;
height: auto;
display: block;
}
/* Утилиты */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 var(--spacing-md);
}
@media (max-width: 768px) {
.container {
padding: 0 var(--spacing-sm);
}
}
/* Плавные переходы для страниц */
.page-transition {
animation: fadeIn var(--transition-slow);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

46
src/app/layout.tsx Normal file
View File

@ -0,0 +1,46 @@
import type { Metadata } from 'next';
import { Inter, Playfair_Display } from 'next/font/google';
import './globals.css';
import Header from '@/components/layout/Header';
import Footer from '@/components/layout/Footer';
import { companyInfo } from '@/lib/company';
const inter = Inter({
subsets: ['latin', 'cyrillic'],
variable: '--font-inter',
display: 'swap',
});
const playfair = Playfair_Display({
subsets: ['latin', 'cyrillic'],
variable: '--font-playfair',
display: 'swap',
});
export const metadata: Metadata = {
title: `${companyInfo.name} - ${companyInfo.photographerName}`,
description: `${companyInfo.slogan}. Профессиональная фотосъёмка: фотосессии на улице и в студии, ретушь, подарочные сертификаты.`,
keywords: 'фотограф, фотосессия, портрет, ретушь, фотостудия',
icons: {
icon: companyInfo.logo,
shortcut: companyInfo.logo,
apple: companyInfo.logo,
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ru" className={`${inter.variable} ${playfair.variable}`}>
<body>
<Header />
<main className="page-transition">{children}</main>
<Footer />
</body>
</html>
);
}

View File

@ -0,0 +1,40 @@
.notFound {
min-height: 60vh;
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-xl) 0;
text-align: center;
}
.title {
font-family: var(--font-heading);
font-size: clamp(4rem, 8vw, 8rem);
font-weight: 700;
color: var(--color-accent);
margin-bottom: var(--spacing-md);
}
.text {
font-family: var(--font-body);
font-size: 1.5rem;
color: var(--color-text-secondary);
margin-bottom: var(--spacing-lg);
}
.link {
display: inline-block;
padding: 0.875rem 2rem;
background-color: var(--color-accent);
color: var(--color-text-primary);
border-radius: 4px;
font-family: var(--font-body);
font-weight: 500;
transition: all var(--transition-base);
}
.link:hover {
background-color: #b84d5f;
transform: translateY(-2px);
}

18
src/app/not-found.tsx Normal file
View File

@ -0,0 +1,18 @@
import Link from 'next/link';
import LayoutContainer from '@/components/layout/LayoutContainer';
import styles from './not-found.module.css';
export default function NotFound() {
return (
<div className={styles.notFound}>
<LayoutContainer>
<h1 className={styles.title}>404</h1>
<p className={styles.text}>Страница не найдена</p>
<Link href="/" className={styles.link}>
Вернуться на главную
</Link>
</LayoutContainer>
</div>
);
}

14
src/app/page.tsx Normal file
View File

@ -0,0 +1,14 @@
import HeroSection from '@/components/home/HeroSection';
import WorksCarousel from '@/components/home/WorksCarousel';
import ServicesGrid from '@/components/home/ServicesGrid';
export default function Home() {
return (
<>
<HeroSection />
<WorksCarousel />
<ServicesGrid />
</>
);
}

View File

@ -0,0 +1,29 @@
import { notFound } from 'next/navigation';
import { getServiceBySlug } from '@/lib/services';
import ServiceDetails from '@/components/services/ServiceDetails';
interface ServicePageProps {
params: {
slug: string;
};
}
export default function ServicePage({ params }: ServicePageProps) {
const service = getServiceBySlug(params.slug);
if (!service) {
notFound();
}
return <ServiceDetails service={service} />;
}
export async function generateStaticParams() {
return [
{ slug: 'street' },
{ slug: 'studio' },
{ slug: 'retouch' },
{ slug: 'certificate' },
];
}

View File

@ -0,0 +1,118 @@
.servicesPage {
min-height: 100vh;
background-color: var(--color-bg-dark);
padding: var(--spacing-xl) 0;
}
.title {
font-family: var(--font-heading);
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 700;
text-align: center;
margin-bottom: var(--spacing-xl);
color: var(--color-text-primary);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: var(--spacing-lg);
}
.card {
background-color: var(--color-dark);
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: transform var(--transition-base), box-shadow var(--transition-base);
border: 1px solid rgba(166, 68, 86, 0.2);
display: flex;
flex-direction: column;
}
.card:hover {
transform: translateY(-8px);
box-shadow: var(--shadow-lg);
border-color: var(--color-accent);
}
.imageWrapper {
position: relative;
width: 100%;
height: 250px;
overflow: hidden;
}
.image {
object-fit: cover;
transition: transform var(--transition-slow);
filter: brightness(0.8) contrast(1.1);
}
.card:hover .image {
transform: scale(1.1);
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
to bottom,
transparent,
rgba(26, 26, 26, 0.6)
);
z-index: 1;
}
.content {
padding: var(--spacing-md);
flex: 1;
display: flex;
flex-direction: column;
z-index: 2;
}
.cardTitle {
font-family: var(--font-heading);
font-size: 1.75rem;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: var(--spacing-sm);
}
.cardDescription {
font-family: var(--font-body);
font-size: 1rem;
line-height: 1.6;
color: var(--color-text-secondary);
margin-bottom: var(--spacing-md);
flex: 1;
}
.linkText {
font-family: var(--font-body);
font-size: 0.95rem;
color: var(--color-accent);
font-weight: 500;
transition: color var(--transition-base);
margin-top: auto;
}
.card:hover .linkText {
color: var(--color-light-vintage);
}
@media (max-width: 768px) {
.servicesPage {
padding: var(--spacing-lg) 0;
}
.grid {
grid-template-columns: 1fr;
gap: var(--spacing-md);
}
}

42
src/app/services/page.tsx Normal file
View File

@ -0,0 +1,42 @@
import Link from 'next/link';
import Image from 'next/image';
import { services } from '@/lib/services';
import LayoutContainer from '@/components/layout/LayoutContainer';
import styles from './page.module.css';
export default function ServicesPage() {
return (
<div className={styles.servicesPage}>
<LayoutContainer>
<h1 className={styles.title}>Услуги</h1>
<div className={styles.grid}>
{services.map((service) => (
<Link
key={service.slug}
href={`/services/${service.slug}`}
className={styles.card}
>
<div className={styles.imageWrapper}>
<Image
src={service.coverImage}
alt={service.title}
fill
className={styles.image}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
loading="lazy"
/>
<div className={styles.overlay} />
</div>
<div className={styles.content}>
<h2 className={styles.cardTitle}>{service.title}</h2>
<p className={styles.cardDescription}>{service.description}</p>
<span className={styles.linkText}>Подробнее </span>
</div>
</Link>
))}
</div>
</LayoutContainer>
</div>
);
}

View File

@ -0,0 +1,59 @@
.contactInfo {
padding: var(--spacing-xl) 0;
background-color: var(--color-bg-dark);
text-align: center;
}
.title {
font-family: var(--font-heading);
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 700;
color: var(--color-text-primary);
margin-bottom: var(--spacing-md);
}
.text {
font-family: var(--font-body);
font-size: 1.25rem;
color: var(--color-text-secondary);
margin-bottom: var(--spacing-lg);
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.instagramBlock {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-md);
margin-top: var(--spacing-lg);
}
.instagramText {
font-family: var(--font-body);
font-size: 1.1rem;
color: var(--color-text-secondary);
}
.instagramLink {
color: var(--color-accent);
transition: color var(--transition-base);
font-weight: 500;
}
.instagramLink:hover {
color: var(--color-light-vintage);
}
@media (max-width: 768px) {
.contactInfo {
padding: var(--spacing-lg) 0;
}
.text {
font-size: 1.1rem;
padding: 0 var(--spacing-sm);
}
}

View File

@ -0,0 +1,34 @@
import { companyInfo } from '@/lib/company';
import LayoutContainer from '@/components/layout/LayoutContainer';
import InstagramLink from '@/components/ui/InstagramLink';
import styles from './ContactInfo.module.css';
export default function ContactInfo() {
const instagramUrl = `https://instagram.com/${companyInfo.instagram}`;
return (
<section className={styles.contactInfo}>
<LayoutContainer>
<h1 className={styles.title}>Контакты</h1>
<p className={styles.text}>
Вы всегда можете написать мне в Instagram
</p>
<div className={styles.instagramBlock}>
<p className={styles.instagramText}>
Instagram:{' '}
<a
href={instagramUrl}
target="_blank"
rel="noopener noreferrer"
className={styles.instagramLink}
>
@{companyInfo.instagram}
</a>
</p>
<InstagramLink>Перейти в Instagram</InstagramLink>
</div>
</LayoutContainer>
</section>
);
}

View File

@ -0,0 +1,27 @@
.mapContainer {
width: 100%;
margin: var(--spacing-xl) 0;
}
.map {
width: 100%;
height: 500px;
border-radius: 8px;
overflow: hidden;
box-shadow: var(--shadow-md);
border: 1px solid rgba(166, 68, 86, 0.2);
position: relative;
}
.map iframe {
width: 100%;
height: 100%;
border: none;
}
@media (max-width: 768px) {
.map {
height: 400px;
}
}

View File

@ -0,0 +1,43 @@
'use client';
import { useEffect, useRef } from 'react';
import styles from './YandexMap.module.css';
export default function YandexMap() {
const mapRef = useRef<HTMLDivElement>(null);
const scriptLoadedRef = useRef(false);
useEffect(() => {
if (!mapRef.current || scriptLoadedRef.current) return;
// Очистка предыдущего содержимого
mapRef.current.innerHTML = '';
// Создание скрипта конструктора карт
const script = document.createElement('script');
script.type = 'text/javascript';
script.charset = 'utf-8';
script.async = true;
script.src =
'https://api-maps.yandex.ru/services/constructor/1.0/js/?um=constructor%3Ade579bce5d153a77c904d2d713e051548ffef8ef516c10078f5ac83e53d5872a&width=1200&height=500&lang=ru_RU&scroll=true';
// Добавляем скрипт в контейнер карты (конструктор создаст iframe в этом контейнере)
mapRef.current.appendChild(script);
scriptLoadedRef.current = true;
return () => {
// Очистка при размонтировании
if (mapRef.current) {
mapRef.current.innerHTML = '';
scriptLoadedRef.current = false;
}
};
}, []);
return (
<div className={styles.mapContainer}>
<div ref={mapRef} className={styles.map} id="yandex-map" />
</div>
);
}

View File

@ -0,0 +1,81 @@
.hero {
position: relative;
min-height: 60vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(
135deg,
var(--color-bg-dark) 0%,
var(--color-dark) 50%,
var(--color-bg-dark) 100%
);
background-image:
radial-gradient(circle at 30% 40%, rgba(166, 68, 86, 0.1) 0%, transparent 50%),
radial-gradient(circle at 70% 60%, rgba(191, 155, 122, 0.08) 0%, transparent 50%);
overflow: hidden;
padding: var(--spacing-xl) var(--spacing-md);
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
linear-gradient(to bottom, rgba(26, 26, 26, 0.7), rgba(26, 26, 26, 0.5)),
linear-gradient(to top, rgba(26, 26, 26, 0.7), rgba(26, 26, 26, 0.5));
z-index: 1;
}
.content {
position: relative;
z-index: 2;
text-align: center;
max-width: 800px;
padding: var(--spacing-lg);
}
.slogan {
font-family: var(--font-heading);
font-size: clamp(2.5rem, 5vw, 4.5rem);
font-weight: 700;
color: var(--color-text-primary);
line-height: 1.2;
margin-bottom: var(--spacing-md);
text-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
letter-spacing: 0.02em;
}
.texture {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 120%;
height: 120%;
background-image:
repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(217, 183, 154, 0.03) 2px,
rgba(217, 183, 154, 0.03) 4px
);
pointer-events: none;
z-index: 1;
opacity: 0.5;
}
@media (max-width: 768px) {
.hero {
min-height: 50vh;
padding: var(--spacing-lg) var(--spacing-sm);
}
.content {
padding: var(--spacing-md);
}
}

View File

@ -0,0 +1,15 @@
import { companyInfo } from '@/lib/company';
import styles from './HeroSection.module.css';
export default function HeroSection() {
return (
<section className={styles.hero}>
<div className={styles.overlay} />
<div className={styles.content}>
<h1 className={styles.slogan}>{companyInfo.slogan}</h1>
<div className={styles.texture} />
</div>
</section>
);
}

View File

@ -0,0 +1,118 @@
.servicesSection {
padding: var(--spacing-xl) 0;
background: linear-gradient(
to bottom,
var(--color-bg-dark),
var(--color-bg-secondary)
);
}
.title {
font-family: var(--font-heading);
font-size: clamp(2rem, 4vw, 3rem);
font-weight: 600;
text-align: center;
margin-bottom: var(--spacing-lg);
color: var(--color-text-primary);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--spacing-md);
margin-top: var(--spacing-lg);
}
.card {
position: relative;
aspect-ratio: 4 / 3;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: transform var(--transition-base), box-shadow var(--transition-base);
border: 1px solid rgba(166, 68, 86, 0.2);
background-color: var(--color-dark);
}
.card:hover {
transform: translateY(-8px);
box-shadow: var(--shadow-lg);
border-color: var(--color-accent);
}
.imageWrapper {
position: relative;
width: 100%;
height: 70%;
overflow: hidden;
}
.image {
object-fit: cover;
transition: transform var(--transition-slow);
filter: brightness(0.8) contrast(1.1);
}
.card:hover .image {
transform: scale(1.1);
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
to bottom,
transparent,
rgba(26, 26, 26, 0.7)
);
z-index: 1;
}
.content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: var(--spacing-md);
z-index: 2;
background: linear-gradient(
to top,
rgba(26, 26, 26, 0.95),
transparent
);
}
.cardTitle {
font-family: var(--font-heading);
font-size: 1.5rem;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: var(--spacing-xs);
}
.linkText {
font-family: var(--font-body);
font-size: 0.95rem;
color: var(--color-accent);
font-weight: 500;
transition: color var(--transition-base);
}
.card:hover .linkText {
color: var(--color-light-vintage);
}
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr;
gap: var(--spacing-sm);
}
.card {
aspect-ratio: 16 / 9;
}
}

View File

@ -0,0 +1,41 @@
import Link from 'next/link';
import Image from 'next/image';
import { services } from '@/lib/services';
import LayoutContainer from '@/components/layout/LayoutContainer';
import styles from './ServicesGrid.module.css';
export default function ServicesGrid() {
return (
<section className={styles.servicesSection}>
<LayoutContainer>
<h2 className={styles.title}>Услуги</h2>
<div className={styles.grid}>
{services.map((service) => (
<Link
key={service.slug}
href={`/services/${service.slug}`}
className={styles.card}
>
<div className={styles.imageWrapper}>
<Image
src={service.coverImage}
alt={service.title}
fill
className={styles.image}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
loading="lazy"
/>
<div className={styles.overlay} />
</div>
<div className={styles.content}>
<h3 className={styles.cardTitle}>{service.title}</h3>
<span className={styles.linkText}>Подробнее </span>
</div>
</Link>
))}
</div>
</LayoutContainer>
</section>
);
}

View File

@ -0,0 +1,135 @@
.carouselSection {
padding: var(--spacing-xl) 0;
background-color: var(--color-bg-dark);
}
.title {
font-family: var(--font-heading);
font-size: clamp(2rem, 4vw, 3rem);
font-weight: 600;
text-align: center;
margin-bottom: var(--spacing-lg);
color: var(--color-text-primary);
}
.carousel {
position: relative;
width: 100%;
max-width: 600px; /* Уменьшена ширина для вертикальных фото */
margin: 0 auto;
aspect-ratio: 3 / 4; /* Вертикальные фотографии */
overflow: hidden;
border-radius: 8px;
box-shadow: var(--shadow-lg);
}
.slidesContainer {
position: relative;
width: 100%;
height: 100%;
}
.slide {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity var(--transition-slow);
}
.slide.active {
opacity: 1;
z-index: 1;
}
.image {
object-fit: cover;
filter: brightness(0.95) contrast(1.05);
}
.arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
background-color: rgba(26, 26, 26, 0.7);
color: var(--color-text-primary);
border: 2px solid var(--color-accent);
width: 50px;
height: 50px;
border-radius: 50%;
font-size: 2rem;
cursor: pointer;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition-base);
backdrop-filter: blur(10px);
}
.arrow:hover {
background-color: var(--color-accent);
transform: translateY(-50%) scale(1.1);
}
.arrow:first-of-type {
left: var(--spacing-sm);
}
.arrow:last-of-type {
right: var(--spacing-sm);
}
.dots {
position: absolute;
bottom: var(--spacing-sm);
left: 50%;
transform: translateX(-50%);
display: flex;
gap: var(--spacing-xs);
z-index: 10;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
border: 2px solid var(--color-text-primary);
background-color: transparent;
cursor: pointer;
transition: all var(--transition-base);
}
.dot.active {
background-color: var(--color-accent);
border-color: var(--color-accent);
transform: scale(1.2);
}
@media (max-width: 768px) {
.carouselSection {
padding: var(--spacing-lg) 0;
}
.carousel {
aspect-ratio: 3 / 4; /* Сохраняем вертикальное соотношение */
max-width: 90%; /* Адаптивная ширина на мобильных */
}
.arrow {
width: 40px;
height: 40px;
font-size: 1.5rem;
}
.arrow:first-of-type {
left: 0.5rem;
}
.arrow:last-of-type {
right: 0.5rem;
}
}

View File

@ -0,0 +1,118 @@
'use client';
import { useState, useEffect } from 'react';
import Image from 'next/image';
import { galleryImages } from '@/lib/gallery';
import LayoutContainer from '@/components/layout/LayoutContainer';
import styles from './WorksCarousel.module.css';
export default function WorksCarousel() {
const [currentIndex, setCurrentIndex] = useState(0);
const [touchStart, setTouchStart] = useState(0);
const [touchEnd, setTouchEnd] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % galleryImages.length);
}, 5000);
return () => clearInterval(interval);
}, []);
const goToSlide = (index: number) => {
setCurrentIndex(index);
};
const goToPrevious = () => {
setCurrentIndex((prev) =>
prev === 0 ? galleryImages.length - 1 : prev - 1
);
};
const goToNext = () => {
setCurrentIndex((prev) => (prev + 1) % galleryImages.length);
};
const handleTouchStart = (e: React.TouchEvent) => {
setTouchStart(e.targetTouches[0].clientX);
};
const handleTouchMove = (e: React.TouchEvent) => {
setTouchEnd(e.targetTouches[0].clientX);
};
const handleTouchEnd = () => {
if (!touchStart || !touchEnd) return;
const distance = touchStart - touchEnd;
const isLeftSwipe = distance > 50;
const isRightSwipe = distance < -50;
if (isLeftSwipe) {
goToNext();
}
if (isRightSwipe) {
goToPrevious();
}
};
return (
<section className={styles.carouselSection}>
<LayoutContainer>
<h2 className={styles.title}>Мои работы</h2>
<div
className={styles.carousel}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
<button
className={styles.arrow}
onClick={goToPrevious}
aria-label="Предыдущее изображение"
>
</button>
<div className={styles.slidesContainer}>
{galleryImages.map((image, index) => (
<div
key={image.id}
className={`${styles.slide} ${
index === currentIndex ? styles.active : ''
}`}
>
<Image
src={image.src}
alt={image.alt}
fill
className={styles.image}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 90vw, 1200px"
loading={index === currentIndex ? 'eager' : 'lazy'}
/>
</div>
))}
</div>
<button
className={styles.arrow}
onClick={goToNext}
aria-label="Следующее изображение"
>
</button>
<div className={styles.dots}>
{galleryImages.map((_, index) => (
<button
key={index}
className={`${styles.dot} ${
index === currentIndex ? styles.active : ''
}`}
onClick={() => goToSlide(index)}
aria-label={`Перейти к слайду ${index + 1}`}
/>
))}
</div>
</div>
</LayoutContainer>
</section>
);
}

View File

@ -0,0 +1,63 @@
.footer {
background-color: var(--color-dark);
border-top: 1px solid rgba(166, 68, 86, 0.2);
padding: var(--spacing-lg) 0;
margin-top: var(--spacing-xl);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 var(--spacing-md);
}
.content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: var(--spacing-md);
}
.info {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.instagram {
font-family: var(--font-body);
font-size: 1rem;
color: var(--color-text-secondary);
}
.instagramLink {
color: var(--color-accent);
transition: color var(--transition-base);
}
.instagramLink:hover {
color: var(--color-light-vintage);
}
.copyright {
font-family: var(--font-body);
font-size: 0.9rem;
color: var(--color-text-muted);
}
@media (max-width: 768px) {
.footer {
padding: var(--spacing-md) 0;
}
.content {
flex-direction: column;
text-align: center;
}
.info {
align-items: center;
}
}

View File

@ -0,0 +1,36 @@
import Link from 'next/link';
import { companyInfo } from '@/lib/company';
import InstagramLink from '@/components/ui/InstagramLink';
import styles from './Footer.module.css';
export default function Footer() {
const currentYear = new Date().getFullYear();
const instagramUrl = `https://instagram.com/${companyInfo.instagram}`;
return (
<footer className={styles.footer}>
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.info}>
<p className={styles.instagram}>
Instagram:{' '}
<Link
href={instagramUrl}
target="_blank"
rel="noopener noreferrer"
className={styles.instagramLink}
>
@{companyInfo.instagram}
</Link>
</p>
<InstagramLink variant="outline" />
</div>
<div className={styles.copyright}>
<p>© {currentYear} {companyInfo.name}</p>
</div>
</div>
</div>
</footer>
);
}

View File

@ -0,0 +1,96 @@
.header {
background-color: var(--color-dark);
border-bottom: 1px solid rgba(166, 68, 86, 0.2);
padding: var(--spacing-sm) 0;
position: sticky;
top: 0;
z-index: 100;
backdrop-filter: blur(10px);
box-shadow: var(--shadow-sm);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 var(--spacing-md);
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
display: flex;
align-items: center;
gap: var(--spacing-sm);
transition: opacity var(--transition-base);
}
.logo:hover {
opacity: 0.8;
}
.logoText {
font-family: var(--font-heading);
font-size: 1.25rem;
color: var(--color-text-primary);
font-weight: 600;
}
.nav {
display: flex;
gap: var(--spacing-md);
align-items: center;
}
.navLink {
font-family: var(--font-body);
font-size: 1rem;
color: var(--color-text-secondary);
transition: color var(--transition-base);
position: relative;
padding: 0.5rem 0;
}
.navLink::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 2px;
background-color: var(--color-accent);
transition: width var(--transition-base);
}
.navLink:hover {
color: var(--color-text-primary);
}
.navLink:hover::after {
width: 100%;
}
@media (max-width: 768px) {
.container {
padding: 0 var(--spacing-sm);
}
.logoText {
font-size: 1rem;
}
.nav {
gap: var(--spacing-sm);
}
.navLink {
font-size: 0.9rem;
}
}
@media (max-width: 480px) {
.logoText {
display: none;
}
}

View File

@ -0,0 +1,39 @@
'use client';
import Link from 'next/link';
import Image from 'next/image';
import { companyInfo } from '@/lib/company';
import styles from './Header.module.css';
export default function Header() {
return (
<header className={styles.header}>
<div className={styles.container}>
<Link href="/" className={styles.logo}>
<Image
src={companyInfo.logo}
alt={companyInfo.name}
width={60}
height={60}
priority
/>
<span className={styles.logoText}>
{companyInfo.photographerName}
</span>
</Link>
<nav className={styles.nav}>
<Link href="/" className={styles.navLink}>
Главная
</Link>
<Link href="/services" className={styles.navLink}>
Услуги
</Link>
<Link href="/contacts" className={styles.navLink}>
Контакты
</Link>
</nav>
</div>
</header>
);
}

View File

@ -0,0 +1,12 @@
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 var(--spacing-md);
}
@media (max-width: 768px) {
.container {
padding: 0 var(--spacing-sm);
}
}

View File

@ -0,0 +1,18 @@
import styles from './LayoutContainer.module.css';
interface LayoutContainerProps {
children: React.ReactNode;
className?: string;
}
export default function LayoutContainer({
children,
className = '',
}: LayoutContainerProps) {
return (
<div className={`${styles.container} ${className}`}>
{children}
</div>
);
}

View File

@ -0,0 +1,150 @@
.servicePage {
min-height: 100vh;
background-color: var(--color-bg-dark);
}
.hero {
position: relative;
width: 100%;
height: 50vh;
min-height: 400px;
overflow: hidden;
}
.heroImage {
object-fit: cover;
filter: brightness(0.6) contrast(1.1);
}
.heroOverlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
to bottom,
rgba(26, 26, 26, 0.7),
rgba(26, 26, 26, 0.9)
);
z-index: 1;
}
.heroContent {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: var(--spacing-xl) var(--spacing-md);
z-index: 2;
}
.heroTitle {
font-family: var(--font-heading);
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 700;
color: var(--color-text-primary);
text-shadow: 0 4px 20px rgba(0, 0, 0, 0.7);
max-width: 1200px;
margin: 0 auto;
}
.content {
padding: var(--spacing-xl) 0;
}
.description {
font-family: var(--font-body);
font-size: 1.25rem;
line-height: 1.8;
color: var(--color-text-secondary);
max-width: 800px;
margin: 0 auto var(--spacing-xl);
text-align: center;
}
.examples {
margin: var(--spacing-xl) 0;
}
.examplesTitle {
font-family: var(--font-heading);
font-size: clamp(1.75rem, 3vw, 2.5rem);
font-weight: 600;
text-align: center;
margin-bottom: var(--spacing-lg);
color: var(--color-text-primary);
}
.examplesGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--spacing-lg);
margin-bottom: var(--spacing-xl);
align-items: start;
}
.exampleItem {
position: relative;
aspect-ratio: 3 / 4; /* Вертикальные фотографии */
border-radius: 8px;
overflow: hidden;
box-shadow: var(--shadow-md);
border: 1px solid rgba(166, 68, 86, 0.2);
transition: transform var(--transition-base), box-shadow var(--transition-base);
}
.exampleItem:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--color-accent);
}
.exampleImage {
object-fit: cover;
filter: brightness(0.9) contrast(1.05);
transition: transform var(--transition-slow);
}
.exampleItem:hover .exampleImage {
transform: scale(1.05);
}
.cta {
text-align: center;
margin-top: var(--spacing-xl);
padding: var(--spacing-lg) 0;
}
@media (max-width: 768px) {
.hero {
height: 40vh;
min-height: 300px;
}
.heroContent {
padding: var(--spacing-lg) var(--spacing-sm);
}
.description {
font-size: 1.1rem;
padding: 0 var(--spacing-sm);
}
.examplesGrid {
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-md);
}
.exampleItem {
aspect-ratio: 3 / 4;
}
}
@media (max-width: 480px) {
.examplesGrid {
grid-template-columns: 1fr;
gap: var(--spacing-md);
}
}

View File

@ -0,0 +1,61 @@
import Image from 'next/image';
import { Service } from '@/lib/types';
import LayoutContainer from '@/components/layout/LayoutContainer';
import InstagramLink from '@/components/ui/InstagramLink';
import styles from './ServiceDetails.module.css';
interface ServiceDetailsProps {
service: Service;
}
export default function ServiceDetails({ service }: ServiceDetailsProps) {
return (
<div className={styles.servicePage}>
<div className={styles.hero}>
<Image
src={service.coverImage}
alt={service.title}
fill
className={styles.heroImage}
priority
sizes="100vw"
/>
<div className={styles.heroOverlay} />
<div className={styles.heroContent}>
<h1 className={styles.heroTitle}>{service.title}</h1>
</div>
</div>
<LayoutContainer>
<div className={styles.content}>
<p className={styles.description}>{service.description}</p>
{service.examples.length > 0 && (
<section className={styles.examples}>
<h2 className={styles.examplesTitle}>Примеры работ</h2>
<div className={styles.examplesGrid}>
{service.examples.map((example, index) => (
<div key={index} className={styles.exampleItem}>
<Image
src={example}
alt={`Пример работы ${index + 1}`}
fill
className={styles.exampleImage}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
loading="lazy"
/>
</div>
))}
</div>
</section>
)}
<div className={styles.cta}>
<InstagramLink>Связаться в Instagram</InstagramLink>
</div>
</div>
</LayoutContainer>
</div>
);
}

View File

@ -0,0 +1,76 @@
.button {
display: inline-block;
padding: 0.875rem 2rem;
font-family: var(--font-body);
font-size: 1rem;
font-weight: 500;
text-align: center;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all var(--transition-base);
text-decoration: none;
position: relative;
overflow: hidden;
}
.button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.1),
transparent
);
transition: left 0.5s;
}
.button:hover::before {
left: 100%;
}
.primary {
background-color: var(--color-accent);
color: var(--color-text-primary);
box-shadow: var(--shadow-md);
}
.primary:hover {
background-color: #b84d5f;
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.secondary {
background-color: var(--color-sand);
color: var(--color-bg-dark);
}
.secondary:hover {
background-color: var(--color-light-vintage);
transform: translateY(-2px);
}
.outline {
background-color: transparent;
color: var(--color-text-primary);
border: 2px solid var(--color-accent);
}
.outline:hover {
background-color: var(--color-accent);
color: var(--color-text-primary);
}
@media (max-width: 768px) {
.button {
padding: 0.75rem 1.5rem;
font-size: 0.9rem;
}
}

View File

@ -0,0 +1,35 @@
import Link from 'next/link';
import styles from './Button.module.css';
interface ButtonProps {
children: React.ReactNode;
href?: string;
onClick?: () => void;
variant?: 'primary' | 'secondary' | 'outline';
className?: string;
}
export default function Button({
children,
href,
onClick,
variant = 'primary',
className = '',
}: ButtonProps) {
const classes = `${styles.button} ${styles[variant]} ${className}`;
if (href) {
return (
<Link href={href} className={classes}>
{children}
</Link>
);
}
return (
<button className={classes} onClick={onClick}>
{children}
</button>
);
}

View File

@ -0,0 +1,26 @@
import { companyInfo } from '@/lib/company';
import Button from './Button';
interface InstagramLinkProps {
variant?: 'primary' | 'secondary' | 'outline';
children?: React.ReactNode;
}
export default function InstagramLink({
variant = 'primary',
children,
}: InstagramLinkProps) {
const instagramUrl = `https://instagram.com/${companyInfo.instagram}`;
const text = children || `Написать в Instagram`;
return (
<Button
href={instagramUrl}
variant={variant}
className="instagram-link"
>
{text}
</Button>
);
}

10
src/lib/company.ts Normal file
View File

@ -0,0 +1,10 @@
import { CompanyInfo } from './types';
export const companyInfo: CompanyInfo = {
name: 'Romanovna Photo',
photographerName: 'Ангелина Чёрная',
logo: '/logo/romanovna-logo.png',
instagram: 'romanovnaph_ch',
slogan: 'Эстетика в каждом кадре',
};

36
src/lib/gallery.ts Normal file
View File

@ -0,0 +1,36 @@
import { GalleryImage } from './types';
// Карусель использует фото из примеров услуг для оптимизации
export const galleryImages: GalleryImage[] = [
{
id: '1',
src: '/images/services/street-example-1.jpg',
alt: 'Уличная фотосессия',
},
{
id: '2',
src: '/images/services/studio-example-1.jpg',
alt: 'Студийная фотосессия',
},
{
id: '3',
src: '/images/services/street-example-2.jpg',
alt: 'Уличная фотосессия',
},
{
id: '4',
src: '/images/services/studio-example-2.jpg',
alt: 'Студийная фотосессия',
},
{
id: '5',
src: '/images/services/street-example-3.jpg',
alt: 'Уличная фотосессия',
},
{
id: '6',
src: '/images/services/studio-example-3.jpg',
alt: 'Студийная фотосессия',
},
];

51
src/lib/services.ts Normal file
View File

@ -0,0 +1,51 @@
import { Service } from './types';
export const services: Service[] = [
{
slug: 'street',
title: 'Фотосессия на улице',
description: 'Живые эмоции и естественные кадры на фоне городских пейзажей, парков и архитектуры. Идеально для портретов, семейных фотосессий и романтических съёмок.',
coverImage: '/images/services/street-cover.jpg',
examples: [
'/images/services/street-example-1.jpg',
'/images/services/street-example-2.jpg',
'/images/services/street-example-3.jpg',
'/images/services/street-example-4.jpg',
],
},
{
slug: 'studio',
title: 'Фотосессия в студии',
description: 'Профессиональная студийная съёмка с качественным светом и оборудованием. Подходит для портретов, fashion-съёмок и коммерческих проектов.',
coverImage: '/images/services/studio-cover.jpg',
examples: [
'/images/services/studio-example-1.jpg',
'/images/services/studio-example-2.jpg',
'/images/services/studio-example-3.jpg',
],
},
{
slug: 'retouch',
title: 'Ретушь',
description: 'Профессиональная обработка фотографий: цветокоррекция, ретушь кожи, художественная обработка. Придание вашим снимкам премиального вида.',
coverImage: '/images/services/retouch-cover.jpg',
examples: [
'/images/services/retouch-example-1.jpg',
'/images/services/retouch-example-2.jpg',
],
},
{
slug: 'certificate',
title: 'Сертификат в подарок',
description: 'Подарочный сертификат на фотосессию — идеальный подарок для близких. Выберите сумму и тип съёмки, а получатель сам решит, когда использовать сертификат.',
coverImage: '/images/services/certificate-cover.jpg',
examples: [
'/images/services/certificate-example-1.jpg',
],
},
];
export function getServiceBySlug(slug: string): Service | undefined {
return services.find((service) => service.slug === slug);
}

24
src/lib/types.ts Normal file
View File

@ -0,0 +1,24 @@
export interface Service {
slug: string;
title: string;
description: string;
coverImage: string;
examples: string[];
}
export interface GalleryImage {
id: string;
src: string;
alt: string;
}
export interface CompanyInfo {
name: string;
photographerName: string;
logo: string;
instagram: string;
slogan: string;
address?: string;
phone?: string;
}

28
tsconfig.json Normal file
View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2020",
"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", "old", "src_old"]
}