Dev #1

Merged
redrockjs merged 66 commits from dev into main 2025-12-11 08:37:11 +00:00
879 changed files with 33521 additions and 613 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto

137
.github/workflows/main.yaml vendored Normal file
View File

@@ -0,0 +1,137 @@
name: Build and deploy
on:
push:
branches:
- 'main'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Notify Telegram start building
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
message: |
🚀 Запуск сборки!
Сервер: ${{ secrets.SERVER_NAME }}
Репозиторий: ${{ github.repository }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
push: true
tags: smallbuster/ocenka-web:latest
build-args: |
NEXT_PUBLIC_MAIL_USER=${{ secrets.NEXT_PUBLIC_MAIL_USER }}
NEXT_PUBLIC_MAIL_PASS=${{ secrets.NEXT_PUBLIC_MAIL_PASS }}
NEXT_PUBLIC_MAIL_FROM=${{ secrets.NEXT_PUBLIC_MAIL_FROM }}
NEXT_PUBLIC_MAIL_TO=${{ secrets.NEXT_PUBLIC_MAIL_TO }}
NEXT_PUBLIC_MAIL_SECURE_KEY=${{ secrets.NEXT_PUBLIC_MAIL_SECURE_KEY }}
- name: Notify Telegram about success
if: success()
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
message: |
✅ Сборка успешно завершена!
Сервер: ${{ secrets.SERVER_NAME }}
Репозиторий: ${{ github.repository }}
Ветка: ${{ github.ref_name }}
Коммит: ${{ github.sha }}
- name: Notify Telegram about failure
if: failure()
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
message: |
❌ Сборка проекта неудачна!
Сервер: ${{ secrets.SERVER_NAME }}
Репозиторий: ${{ github.repository }}
Ветка: ${{ github.ref_name }}
Коммит: ${{ github.sha }}
Подробности: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
deploy:
runs-on: self-hosted
needs: build
steps:
- name: Notify Telegram start deploy
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
message: |
🐳 Запуск деплоя!
Сервер: ${{ secrets.SERVER_NAME }}
Репозиторий: ${{ github.repository }}
- name: Checkout repository
uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Compose
run: |
docker compose version
- name: Stop and remove existing containers
run: |
docker compose -f docker-compose.yaml down || true
- name: Pull Docker images
run: |
docker compose -f docker-compose.yaml pull
- name: Start containers
run: |
docker compose -f docker-compose.yaml up -d
- name: Notify Telegram about success
if: success()
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
message: |
✅ Деплой успешно завершен!
Сервер: ${{ secrets.SERVER_NAME }}
Репозиторий: ${{ github.repository }}
Ветка: ${{ github.ref_name }}
Коммит: ${{ github.sha }}
- name: Notify Telegram about failure
if: failure()
uses: appleboy/telegram-action@master
with:
to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
message: |
❌ Деплой не удался!
Сервер: ${{ secrets.SERVER_NAME }}
Репозиторий: ${{ github.repository }}
Ветка: ${{ github.ref_name }}
Коммит: ${{ github.sha }}
Подробности: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}

5
.husky/pre-commit Normal file
View File

@@ -0,0 +1,5 @@
npx lint-staged
echo ""
echo "🚀 Running test build..."
npm run build

6
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="TypeScriptCompiler">
<option name="useServicePoweredTypesWasEnabledByExperiment" value="true" />
</component>
</project>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/ocenka-web.iml" filepath="$PROJECT_DIR$/.idea/ocenka-web.iml" />
</modules>
</component>
</project>

12
.idea/ocenka-web.iml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/prettier.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

128
.idea/workspace.xml generated Normal file
View File

@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="2a96f817-9dc2-4f3c-893a-c4974c750774" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/package-lock.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="DarkyenusTimeTracker">
<option name="totalTimeSeconds" value="226258" />
<option name="gitIntegration" value="true" />
<option name="naggedAbout" value="1" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="SCSS File" />
<option value="TypeScript JSX File" />
<option value="TypeScript File" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProblemsViewState">
<option name="selectedTabId" value="DEPENDENCY_CHECKER_PROBLEMS_TAB" />
</component>
<component name="ProjectColorInfo">{
&quot;customColor&quot;: &quot;&quot;,
&quot;associatedIndex&quot;: 4
}</component>
<component name="ProjectId" id="35jvwBqGA9zlZwQ6LZmQZbbmwbc" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
"git-widget-placeholder": "dev",
"ignore.virus.scanning.warn.message": "true",
"js.debugger.nextJs.config.created.client": "true",
"js.debugger.nextJs.config.created.server": "true",
"junie.onboarding.icon.badge.shown": "true",
"list.type.of.created.stylesheet": "SCSS",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"npm.Next.js: server-side.executor": "Run",
"prettierjs.PrettierConfiguration.Package": "C:\\dev-personal\\ocenka-web\\node_modules\\prettier",
"settings.editor.selected.configurable": "terminal",
"to.speed.mode.migration.done": "true",
"ts.external.directory.path": "C:\\dev-personal\\ocenka-web\\node_modules\\typescript\\lib",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS">
<recent name="C:\dev-personal\ocenka-web\public\images" />
<recent name="C:\dev-personal\ocenka-web\src\widgets" />
<recent name="C:\dev-personal\ocenka-web\src\widgets\sidebar" />
<recent name="C:\dev-personal\ocenka-web\src\views\home" />
</key>
</component>
<component name="RunManager" selected="npm.Next.js: server-side">
<configuration name="Next.js: debug client-side" type="JavascriptDebugType" uri="http://localhost:3000/">
<method v="2" />
</configuration>
<configuration name="Next.js: server-side" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-c7e53b3be11b-JavaScript-WS-253.28294.332" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="2a96f817-9dc2-4f3c-893a-c4974c750774" name="Changes" comment="" />
<created>1763636886190</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1763636886190</updated>
<workItem from="1763636887488" duration="6811000" />
<workItem from="1764586916122" duration="32000" />
<workItem from="1764590282382" duration="1201000" />
<workItem from="1764591867512" duration="4332000" />
<workItem from="1764657017067" duration="21490000" />
<workItem from="1764741053553" duration="18011000" />
<workItem from="1764825390464" duration="26961000" />
<workItem from="1765196288370" duration="332000" />
<workItem from="1765196704782" duration="4455000" />
<workItem from="1765279298291" duration="3813000" />
<workItem from="1765435962765" duration="4004000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
</project>

6
.prettierignore Normal file
View File

@@ -0,0 +1,6 @@
.git
.idea
.vscode
.next
/node_modules
*.md

14
.prettierrc Normal file
View File

@@ -0,0 +1,14 @@
{
"tabWidth": 2,
"semi": true,
"printWidth": 80,
"trailingComma": "all",
"singleQuote": true,
"bracketSpacing": true,
"arrowParens": "always",
"bracketSameLine": false,
"proseWrap": "always",
"useTabs": false,
"endOfLine": "lf",
"jsxSingleQuote": true
}

67
Dockerfile Normal file
View File

@@ -0,0 +1,67 @@
# Стадия сборки (builder)
FROM node:22-alpine AS builder
# Устанавливаем рабочую директорию
WORKDIR /app
# Копируем package.json и package-lock.json (или yarn.lock)
COPY package*.json ./
# Устанавливаем зависимости
RUN npm install --frozen-lockfile
# Копируем все файлы проекта
COPY . .
# Аргумент сборки для переменной окружения
ARG NEXT_PUBLIC_MAIL_USER
ARG NEXT_PUBLIC_MAIL_PASS
ARG NEXT_PUBLIC_MAIL_FROM
ARG NEXT_PUBLIC_MAIL_TO
ARG NEXT_PUBLIC_MAIL_SECURE_KEY
ENV NEXT_PUBLIC_MAIL_USER=$NEXT_PUBLIC_MAIL_USER \
NEXT_PUBLIC_MAIL_PASS=$NEXT_PUBLIC_MAIL_PASS \
NEXT_PUBLIC_MAIL_FROM=$NEXT_PUBLIC_MAIL_FROM \
NEXT_PUBLIC_MAIL_TO=$NEXT_PUBLIC_MAIL_TO \
NEXT_PUBLIC_MAIL_SECURE_KEY=$NEXT_PUBLIC_MAIL_SECURE_KEY
# Собираем приложение
RUN npm run build
# Стадия запуска (runner)
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production \
NEXT_TELEMETRY_DISABLED=1 \
NEXT_PUBLIC_MAIL_USER=$NEXT_PUBLIC_MAIL_USER \
NEXT_PUBLIC_MAIL_PASS=$NEXT_PUBLIC_MAIL_PASS \
NEXT_PUBLIC_MAIL_FROM=$NEXT_PUBLIC_MAIL_FROM \
NEXT_PUBLIC_MAIL_TO=$NEXT_PUBLIC_MAIL_TO \
NEXT_PUBLIC_MAIL_SECURE_KEY=$NEXT_PUBLIC_MAIL_SECURE_KEY
# Включаем node пользователя
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Копируем необходимые файлы из стадии builder
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/public /app/public
# Переключаем на пользователя nextjs
USER nextjs
# Открываем порт
EXPOSE 3000
# Запускаем приложение
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

View File

@@ -1,36 +1 @@
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.

9
docker-compose.yaml Normal file
View File

@@ -0,0 +1,9 @@
version: '3.8'
services:
ocenka-web:
image: smallbuster/ocenka-web:latest
container_name: ocenka-web
restart: unless-stopped
ports:
- '5003:3000'

View File

@@ -1,6 +1,6 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
import {dirname} from "path";
import {fileURLToPath} from "url";
import {FlatCompat} from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -11,6 +11,11 @@ const compat = new FlatCompat({
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
rules: {
"@typescript-eslint/no-unused-vars": "off",
},
},
];
export default eslintConfig;

View File

@@ -1,7 +1,17 @@
import type { NextConfig } from "next";
import type { NextConfig } from 'next';
import path from 'path';
const nextConfig: NextConfig = {
/* config options here */
output: 'standalone',
sassOptions: {
includePaths: [path.resolve('./src/core/styles')],
prependData: `@import "index.scss";`,
},
compiler: {
removeConsole:
process.env.NODE_ENV === 'production' ? { exclude: ['error'] } : false,
},
};
export default nextConfig;

2340
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,20 +6,50 @@
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"prettier": "prettier --write .",
"prepare": "husky"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"next": "15.3.4"
"@hookform/resolvers": "^5.1.1",
"@maskito/core": "^3.9.1",
"@maskito/phone": "^3.9.1",
"@maskito/react": "^3.9.1",
"libphonenumber-js": "^1.12.9",
"next": "^15.5.7",
"nodemailer": "7.0.11",
"puppeteer": "^24.32.1",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-hook-form": "^7.60.0",
"react-hot-toast": "^2.5.2",
"react-yandex-metrika": "^2.6.0",
"swiper": "^11.2.10",
"zod": "^3.25.75"
},
"devDependencies": {
"typescript": "^5",
"@eslint/eslintrc": "^3",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.3.4",
"@eslint/eslintrc": "^3"
"@types/nodemailer": "^6.4.17",
"@types/react": "19.1.11",
"@types/react-dom": "19.1.8",
"clsx": "^2.1.1",
"eslint": "^9.39.1",
"eslint-config-next": "15.5.2",
"husky": "^9.1.7",
"lint-staged": "^16.1.2",
"prettier": "3.6.0",
"sass": "1.77.8",
"typescript": "^5"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --fix"
]
},
"overrides": {
"@types/react": "19.1.11",
"@types/react-dom": "19.1.8"
}
}

View File

@@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

BIN
public/images/arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 B

BIN
public/images/bg-lines.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/images/bg-main.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
public/images/ogBg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
public/images/step1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/images/step2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
public/images/step3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/images/step4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

6
public/robots.txt Normal file
View File

@@ -0,0 +1,6 @@
User-agent: *
Disallow: /api/
Disallow: /cookie/
Disallow: /privacy-policy/
Disallow: /user-agreement/
Sitemap: https://ocenka-sochi.ru/sitemap.xml

19
public/site.webmanifest Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "ocenka-sochi.ru",
"short_name": "ocenka-sochi.ru",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "transparent",
"background_color": "transparent",
"display": "standalone"
}

9
public/sitemap.xml Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://ocenka-sochi.ru/</loc>
<lastmod>2025-12-01</lastmod>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
</urlset>

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='none' overflow='visible' height='100%' viewBox='0 0 20 16' fill='none' stroke='black' stroke-width='1' stroke-linecap='square' stroke-miterlimit='10'><g transform='translate(-12.000000, 0)'><path d='M28,0L10,18'/><path d='M18,0L0,18'/><path d='M48,0L30,18'/><path d='M38,0L20,18'/></g></svg>

After

Width:  |  Height:  |  Size: 352 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

View File

@@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B

View File

@@ -0,0 +1,9 @@
export async function GET(request: Request) {
return new Response('Heartbeat is OK!', {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
},
});
}

56
src/app/api/og/route.ts Normal file
View File

@@ -0,0 +1,56 @@
import { NextRequest, NextResponse } from 'next/server';
import puppeteer from 'puppeteer';
export const dynamic = 'force-dynamic';
export async function GET(req: NextRequest) {
const title = req.nextUrl.searchParams.get('title') ?? '';
const desc = req.nextUrl.searchParams.get('description') ?? '';
const imageUrl = `${req.nextUrl.origin}/images/ogBg.png`;
const logoUrl = `${req.nextUrl.origin}/images/logo-dtr-white.png`;
const html = `
<html lang="ru-RU">
<body style="
margin: 0;
padding: 0;
display: flex;
width: 600px;
height: 315px;
background: #111;
color: white;
font-family: sans-serif;
position: relative;
">
<img src="${imageUrl}" style="object-fit: cover; width:100%; height:100%; opacity:0.5; position:absolute; left:0; top:0;" />
<img src="${logoUrl}" style="width: 288px; height: 89px; position:absolute; left:10px; top:20px;" />
<p style="position: absolute; top: 24px; right:20px; color: white; font-family: Arial, Helvetica, sans-serif; font-size: 20px; ">
☎ +7 (900) 241-34-34
</p>
<div style="position: absolute; top: 140px; left: 20px; width:100%; height:100%;">
<h2 style="color: white; font-family: Arial, Helvetica, sans-serif; font-size: 40px; line-height: 1;">${title}</h2>
<p style="color: white; font-family: Arial, Helvetica, sans-serif; font-size: 24px; line-height: 1;">${desc}</p>
</div>
</body>
</html>
`;
// Запуск браузера
const browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
await page.setViewport({ width: 600, height: 315 });
const screenshot = await page.screenshot({ type: 'png' });
await browser.close();
return new NextResponse(screenshot, {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'max-age=60',
},
});
}

View File

@@ -0,0 +1,66 @@
import nodemailer from 'nodemailer';
import { TBaseForm } from '@shared/api/api.types';
import { CORE } from '@shared/config/core';
async function sendMail(data: TBaseForm) {
const { name, phone, message, form } = data;
const formattedBody = `
<html lang="ru-RU">
<body>
<p>Сообщение с сайта "Оценка-Сочи"</p>
<p>Форма отправки: ${form}</p>
<p>Имя отправителя: ${name ?? 'не указано'}</p>
<p>Номер телефона: ${phone}</p>
<p>Сообщение: ${message ?? 'отсутствует'}</p>
</body>
</html>
`;
const transporter = nodemailer.createTransport({
service: 'Yandex',
auth: {
user: CORE.MAIL_USER,
pass: CORE.MAIL_PASS,
},
});
return await transporter.sendMail({
from: CORE.MAIL_FROM,
to: CORE.MAIL_TO,
subject: 'Заявка с сайта Ocenka-Sochi',
html: formattedBody,
});
}
export async function POST(request: Request) {
try {
const payload = await request.json();
if (payload.secure !== CORE.MAIL_SECURE_KEY) {
await Promise.reject('Request failure!');
}
const sendResult = await sendMail({ ...payload });
const data = { message: 'Form accepted' };
const headers = new Headers({
'Content-Type': 'application/json',
});
const options = {
status: 200,
statusText: 'OK',
headers: headers,
};
if (sendResult?.messageId) {
return new Response(JSON.stringify(data), options);
} else {
await Promise.reject('Sending request failure!');
}
} catch (error) {
return new Response(`Api error: ${error}`, {
status: 400,
});
}
}

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

@@ -0,0 +1,20 @@
import { Contacts } from '@/views';
import type { Metadata } from 'next';
import { metaInfo } from '@shared/lib';
const metainfo = {
title: 'Контакты',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре.',
companyName: 'Компания ДИТРАСО',
phone: '+7 (900) 241-34-34',
url: 'https://ocenka-sochi.ru',
ogImageTitle: 'Оценка и экспертиза',
ogImageDescription: 'Независимая оценка и судебная экспертиза',
};
export const metadata: Metadata = metaInfo(metainfo);
export default function Page() {
return <Contacts />;
}

20
src/app/cookies/page.tsx Normal file
View File

@@ -0,0 +1,20 @@
import { Cookie } from '@/views';
import type { Metadata } from 'next';
import { metaInfo } from '@shared/lib';
const metainfo = {
title: 'Независимая оценка и судебная экспертиза',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре.',
companyName: 'Компания ДИТРАСО',
phone: '+7 (900) 241-34-34',
url: 'https://ocenka-sochi.ru',
ogImageTitle: 'Оценка и экспертиза',
ogImageDescription: 'Независимая оценка и судебная экспертиза',
};
export const metadata: Metadata = metaInfo(metainfo);
export default function Page() {
return <Cookie />;
}

View File

@@ -0,0 +1,12 @@
import { AutoTech } from '@/views';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Автотехническая экспертиза | Компания ДИТРАСО',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре. Тел. +7 (900) 241-34-34',
};
export default function Page() {
return <AutoTech />;
}

View File

@@ -0,0 +1,12 @@
import { Buhgalter } from '@/views';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Бухгалтерская экспертиза | Компания ДИТРАСО',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре. Тел. +7 (900) 241-34-34',
};
export default function Page() {
return <Buhgalter />;
}

View File

@@ -0,0 +1,12 @@
import { Document } from '@/views';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Экспертиза документов | Компания ДИТРАСО',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре. Тел. +7 (900) 241-34-34',
};
export default function Page() {
return <Document />;
}

View File

@@ -0,0 +1,12 @@
import { Finans } from '@/views';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Финансово-экономическая экспертиза | Компания ДИТРАСО',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре. Тел. +7 (900) 241-34-34',
};
export default function Page() {
return <Finans />;
}

View File

@@ -0,0 +1,12 @@
import { Kadastr } from '@/views';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Кадастровая экспертиза | Компания ДИТРАСО',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре. Тел. +7 (900) 241-34-34',
};
export default function Page() {
return <Kadastr />;
}

View File

@@ -0,0 +1,12 @@
import { Computer } from '@/views';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Компьютерно-техническая экспертиза | Компания ДИТРАСО',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре. Тел. +7 (900) 241-34-34',
};
export default function Page() {
return <Computer />;
}

View File

@@ -0,0 +1,5 @@
// TODO Проверить что это за страница
export default function Page() {
return <div>Page</div>;
}

View File

@@ -0,0 +1,12 @@
import { ExpertiseCategory } from '@/views';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Проведение экспертиз | Компания ДИТРАСО',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре. Тел. +7 (900) 241-34-34',
};
export default function ExpertizaPage() {
return <ExpertiseCategory />;
}

View File

@@ -0,0 +1,12 @@
import { Pocherk } from '@/views';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Почерковедческая экспертиза | Компания ДИТРАСО',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре. Тел. +7 (900) 241-34-34',
};
export default function Page() {
return <Pocherk />;
}

View File

@@ -0,0 +1,12 @@
import { Pozhar } from '@/views';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Пожарно-техническая экспертиза | Компания ДИТРАСО',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре. Тел. +7 (900) 241-34-34',
};
export default function Page() {
return <Pozhar />;
}

View File

@@ -0,0 +1,12 @@
import { Recenzii } from '@/views';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Строительно-техническая экспертиза | Компания ДИТРАСО',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре. Тел. +7 (900) 241-34-34',
};
export default function Page() {
return <Recenzii />;
}

View File

@@ -0,0 +1,12 @@
import { Stroit } from '@/views';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Рецензирование экспертизы | Компания ДИТРАСО',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре. Тел. +7 (900) 241-34-34',
};
export default function Page() {
return <Stroit />;
}

View File

@@ -0,0 +1,12 @@
import { TechCrim } from '@/views';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Технико-криминалистическая экспертиза | Компания ДИТРАСО',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре. Тел. +7 (900) 241-34-34',
};
export default function Page() {
return <TechCrim />;
}

View File

@@ -0,0 +1,12 @@
import { Tovar } from '@/views';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Товароведческая экспертиза | Компания ДИТРАСО',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре. Тел. +7 (900) 241-34-34',
};
export default function Page() {
return <Tovar />;
}

View File

@@ -0,0 +1,12 @@
import { Trasologia } from '@/views';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Трасологическая экспертиза | Компания ДИТРАСО',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре. Тел. +7 (900) 241-34-34',
};
export default function Page() {
return <Trasologia />;
}

View File

@@ -0,0 +1,12 @@
import { ZemStroy } from '@/views';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Земле-устроительная экспертиза | Компания ДИТРАСО',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре. Тел. +7 (900) 241-34-34',
};
export default function Page() {
return <ZemStroy />;
}

View File

@@ -0,0 +1,20 @@
import { ExpertBorodin } from '@/views';
import type { Metadata } from 'next';
import { metaInfo } from '@shared/lib';
const metainfo = {
title: 'Эксперт - Бородин Виталий Петрович',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре.',
companyName: 'Компания ДИТРАСО',
phone: '+7 (900) 241-34-34',
url: 'https://ocenka-sochi.ru',
ogImageTitle: 'Оценка и экспертиза',
ogImageDescription: 'Независимая оценка и судебная экспертиза',
};
export const metadata: Metadata = metaInfo(metainfo);
export default function Page() {
return <ExpertBorodin />;
}

View File

@@ -0,0 +1,20 @@
import { ExpertGulmamedov } from '@/views';
import type { Metadata } from 'next';
import { metaInfo } from '@shared/lib';
const metainfo = {
title: 'Эксперт - Гюльмамедов Явар Фирмамед-оглы',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре.',
companyName: 'Компания ДИТРАСО',
phone: '+7 (900) 241-34-34',
url: 'https://ocenka-sochi.ru',
ogImageTitle: 'Оценка и экспертиза',
ogImageDescription: 'Независимая оценка и судебная экспертиза',
};
export const metadata: Metadata = metaInfo(metainfo);
export default function Page() {
return <ExpertGulmamedov />;
}

View File

@@ -0,0 +1,20 @@
import { ExpertYancen } from '@/views';
import type { Metadata } from 'next';
import { metaInfo } from '@shared/lib';
const metainfo = {
title: 'Эксперт - Янцен Яна Николаевна',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре.',
companyName: 'Компания ДИТРАСО',
phone: '+7 (900) 241-34-34',
url: 'https://ocenka-sochi.ru',
ogImageTitle: 'Оценка и экспертиза',
ogImageDescription: 'Независимая оценка и судебная экспертиза',
};
export const metadata: Metadata = metaInfo(metainfo);
export default function Page() {
return <ExpertYancen />;
}

View File

@@ -0,0 +1,20 @@
import { ExpertKaminskiy } from '@/views';
import type { Metadata } from 'next';
import { metaInfo } from '@shared/lib';
const metainfo = {
title: 'Эксперт - Каминский Дмитрий Олегович',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре.',
companyName: 'Компания ДИТРАСО',
phone: '+7 (900) 241-34-34',
url: 'https://ocenka-sochi.ru',
ogImageTitle: 'Оценка и экспертиза',
ogImageDescription: 'Независимая оценка и судебная экспертиза',
};
export const metadata: Metadata = metaInfo(metainfo);
export default function Page() {
return <ExpertKaminskiy />;
}

View File

@@ -0,0 +1,20 @@
import { ExpertKolodiy } from '@/views';
import type { Metadata } from 'next';
import { metaInfo } from '@shared/lib';
const metainfo = {
title: 'Эксперт - Колодий Александр Сергеевич',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре.',
companyName: 'Компания ДИТРАСО',
phone: '+7 (900) 241-34-34',
url: 'https://ocenka-sochi.ru',
ogImageTitle: 'Оценка и экспертиза',
ogImageDescription: 'Независимая оценка и судебная экспертиза',
};
export const metadata: Metadata = metaInfo(metainfo);
export default function Page() {
return <ExpertKolodiy />;
}

View File

@@ -0,0 +1,20 @@
import { ExpertMikova } from '@/views';
import type { Metadata } from 'next';
import { metaInfo } from '@shared/lib';
const metainfo = {
title: 'Эксперт - Микова Инна Георгиевна',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре.',
companyName: 'Компания ДИТРАСО',
phone: '+7 (900) 241-34-34',
url: 'https://ocenka-sochi.ru',
ogImageTitle: 'Оценка и экспертиза',
ogImageDescription: 'Независимая оценка и судебная экспертиза',
};
export const metadata: Metadata = metaInfo(metainfo);
export default function Page() {
return <ExpertMikova />;
}

View File

@@ -0,0 +1,20 @@
import { ExpertMityaev } from '@/views';
import type { Metadata } from 'next';
import { metaInfo } from '@shared/lib';
const metainfo = {
title: 'Эксперт - Митяев Алексей Александрович',
description:
'Услуги независимой оценки и судебной экспертизы в Сочи и Краснодаре.',
companyName: 'Компания ДИТРАСО',
phone: '+7 (900) 241-34-34',
url: 'https://ocenka-sochi.ru',
ogImageTitle: 'Оценка и экспертиза',
ogImageDescription: 'Независимая оценка и судебная экспертиза',
};
export const metadata: Metadata = metaInfo(metainfo);
export default function Page() {
return <ExpertMityaev />;
}

Some files were not shown because too many files have changed in this diff Show More