30 Commits

Author SHA1 Message Date
root
3a8bc2f893 change ports in start 2026-03-23 21:31:26 +00:00
root
d29de100c6 add loccal changes 2026-03-23 21:20:11 +00:00
sdarbinyan
97214c3a90 Merge branch 'back-office-integration'
# Conflicts:
#	src/app/pages/cart/cart.component.ts
#	src/app/pages/category/category.component.html
#	src/app/pages/category/category.component.ts
#	src/app/pages/item-detail/item-detail.component.html
#	src/app/pages/item-detail/item-detail.component.ts
#	src/app/pages/legal/company-details/en/company-details-en.component.html
#	src/app/pages/legal/company-details/hy/company-details-hy.component.html
#	src/app/pages/legal/company-details/ru/company-details-ru.component.html
#	src/app/pages/legal/public-offer/en/public-offer-en.component.html
#	src/app/pages/legal/public-offer/ru/public-offer-ru.component.html
#	src/app/pages/search/search.component.ts
#	src/app/services/api.service.ts
2026-03-24 00:18:13 +04:00
sdarbinyan
56f4c56b9e integration new apis 2026-03-24 00:09:11 +04:00
sdarbinyan
0b3b2ee463 changes 2026-03-06 18:40:58 +04:00
sdarbinyan
c3e4e695eb changes and optimisations 2026-03-06 17:45:34 +04:00
sdarbinyan
c112aded47 added sceleton for loading 2026-03-06 17:22:35 +04:00
sdarbinyan
75f029b872 added condition 2026-03-06 16:59:01 +04:00
root
f823df7e15 Merge branch 'main' of https://sources.vitanova.network/sdarbinyan/marketplaces 2026-03-05 16:49:39 +00:00
sdarbinyan
af78c053ba fixed design 2026-03-05 20:45:15 +04:00
root
4ef4223367 Merge branch 'main' of https://sources.vitanova.network/sdarbinyan/marketplaces 2026-03-05 16:27:13 +00:00
sdarbinyan
7b18376d28 added info for legal 2026-03-05 20:23:42 +04:00
root
c64b9cfee8 Merge branch 'main' of https://sources.vitanova.network/sdarbinyan/marketplaces 2026-03-04 14:20:07 +00:00
sdarbinyan
712281d2e8 closed en/am 2026-03-04 16:45:01 +04:00
sdarbinyan
0626dcbe46 changes in legal 2026-03-04 16:40:25 +04:00
root
d288a5fb3c Merge branch 'main' of https://sources.vitanova.network/sdarbinyan/marketplaces 2026-03-02 08:57:24 +00:00
sdarbinyan
3445f55758 updates 2026-03-01 02:43:14 +04:00
sdarbinyan
350581cbe9 changes for md 2026-02-28 17:42:36 +04:00
sdarbinyan
377da22761 Merge branch 'auth-system' into back-office-integration 2026-02-28 17:37:14 +04:00
sdarbinyan
421346d957 Merge remote-tracking branch 'origin' into back-office-integration 2026-02-28 16:13:14 +04:00
sdarbinyan
d6097e2b5d style fixes 2026-02-20 10:58:06 +04:00
sdarbinyan
369af40f20 bo integration 2026-02-20 10:44:03 +04:00
root
75b45abe4f Merge branch 'main' of https://sources.vitanova.network/sdarbinyan/marketplaces 2026-02-19 21:32:07 +00:00
root
2bd98b29eb Merge branch 'main' of https://sources.vitanova.network/sdarbinyan/marketplaces 2026-02-18 14:07:44 +00:00
root
82cbf07120 okMerge branch 'main' of https://sources.vitanova.network/sdarbinyan/marketplaces 2026-02-14 15:28:51 +00:00
root
e07356a700 add new server 2026-02-14 09:52:29 +00:00
root
5068a3a114 Merge branch 'main' of https://sources.vitanova.network/sdarbinyan/marketplaces 2026-02-14 09:51:37 +00:00
root
333ea45c38 Merge branch 'main' of https://sources.vitanova.network/sdarbinyan/marketplaces 2026-01-22 20:35:13 +00:00
root
b22390f3eb Merge branch 'main' of https://sources.vitanova.network/sdarbinyan/marketplaces 2026-01-22 20:27:30 +00:00
root
3f285ca15f local build 2026-01-22 11:58:50 +00:00
86 changed files with 6121 additions and 1878 deletions

2
.gitignore vendored
View File

@@ -38,7 +38,7 @@ yarn-error.log
/libpeerconnection.log
testem.log
/typings
/public/images/
# System files
.DS_Store
Thumbs.db

View File

@@ -154,7 +154,8 @@
},
"serve": {
"options": {
"allowedHosts": ["novo.market", "dexarmarket.ru", "localhost"]
"allowedHosts": ["novo.market", "dexarmarket.ru", "localhost"],
"proxyConfig": "proxy.conf.json"
},
"builder": "@angular/build:dev-server",
"configurations": {
@@ -175,28 +176,9 @@
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.scss"
]
}
}
}
}
}
}

View File

@@ -1,24 +1,471 @@
# API Changes Required for Backend
# Complete Backend API Documentation
## Overview
Frontend has been updated with two new features:
1. **Region/Location system** — catalog filtering by region
2. **Auth/Login system** — Telegram-based authentication required before payment
Base URLs:
- Dexar: `https://api.dexarmarket.ru:445`
- Novo: `https://api.novo.market:444`
> **Last updated:** February 2026
> **Frontend:** Angular 21 · Dual-brand (Dexar + Novo)
> **Covers:** Catalog, Cart, Payments, Reviews, Regions, Auth, i18n, BackOffice
---
## 1. Region / Location Endpoints
## Base URLs
### 1.1 `GET /regions` — List available regions
| Brand | Dev | Production |
|--------|----------------------------------|----------------------------------|
| Dexar | `https://api.dexarmarket.ru:445` | `https://api.dexarmarket.ru:445` |
| Novo | `https://api.novo.market:444` | `https://api.novo.market:444` |
Returns the list of regions where the marketplace operates.
---
**Response** `200 OK`
## Global HTTP Headers
The frontend **automatically attaches** two custom headers to **every API request** via an interceptor. The backend should read these headers and use them to filter/translate responses accordingly.
| Header | Example Value | Description |
|---------------|---------------|------------------------------------------------------------|
| `X-Region` | `moscow` | Region ID selected by the user. **Absent** = global (all). |
| `X-Language` | `ru` | Active UI language: `ru`, `en`, or `hy`. |
### Backend behavior
- **`X-Region`**: If present, filter items/categories to only those available in that region. If absent, return everything (global catalog).
- **`X-Language`**: If present, return translated `name`, `description`, etc. for categories/items when translations exist. If absent or `ru`, use russians defaults.
### CORS requirements for these headers
```
Access-Control-Allow-Headers: Content-Type, X-Region, X-Language
```
---
## 1. Health Check
### `GET /ping`
Simple health check.
**Response `200`:**
```json
{ "message": "pong" }
```
---
## 2. Catalog — Categories
### `GET /category`
Returns all top-level categories. Respects `X-Region` and `X-Language` headers.
**Response `200`:**
```json
[
{
"categoryID": 1,
"name": "Электроника",
"parentID": 0,
"icon": "https://...",
"wideBanner": "https://...",
"itemCount": 42,
"priority": 10,
"id": "cat_abc123",
"visible": true,
"img": "https://...",
"projectId": "proj_xyz",
"subcategories": [
{
"id": "sub_001",
"name": "Смартфоны",
"visible": true,
"priority": 5,
"img": "https://...",
"categoryId": "cat_abc123",
"parentId": "cat_abc123",
"itemCount": 20,
"hasItems": true,
"subcategories": []
}
]
}
]
```
**Category object:**
| Field | Type | Required | Description |
|------------------|---------------|----------|----------------------------------------------------|
| `categoryID` | number | yes | Legacy numeric ID |
| `name` | string | yes | Category display name (translated if `X-Language`) |
| `parentID` | number | yes | Parent category ID (`0` = top-level) |
| `icon` | string | no | Category icon URL |
| `wideBanner` | string | no | Wide banner image URL |
| `itemCount` | number | no | Number of items in category |
| `priority` | number | no | Sort priority (higher = first) |
| `id` | string | no | BackOffice string ID |
| `visible` | boolean | no | Whether category is shown (`true` default) |
| `img` | string | no | BackOffice image URL (maps to `icon`) |
| `projectId` | string | no | BackOffice project reference |
| `subcategories` | Subcategory[] | no | Nested subcategories |
**Subcategory object:**
| Field | Type | Required | Description |
|------------------|---------------|----------|------------------------------------|
| `id` | string | yes | Subcategory ID |
| `name` | string | yes | Display name |
| `visible` | boolean | no | Whether visible |
| `priority` | number | no | Sort priority |
| `img` | string | no | Image URL |
| `categoryId` | string | yes | Parent category ID |
| `parentId` | string | yes | Direct parent ID |
| `itemCount` | number | no | Number of items |
| `hasItems` | boolean | no | Whether has any items |
| `subcategories` | Subcategory[] | no | Nested children |
---
### `GET /category/:categoryID`
Returns items in a specific category. Respects `X-Region` and `X-Language` headers.
**Query params:**
| Param | Type | Default | Description |
|----------|--------|---------|--------------------|
| `count` | number | `50` | Items per page |
| `skip` | number | `0` | Offset for paging |
**Response `200`:** Array of [Item](#item-object) objects.
---
## 3. Items
### `GET /item/:itemID`
Returns a single item. Respects `X-Region` and `X-Language` headers.
**Response `200`:** A single [Item](#item-object) object.
---
### `GET /searchitems`
Full-text search across items. Respects `X-Region` and `X-Language` headers.
**Query params:**
| Param | Type | Default | Description |
|----------|--------|---------|----------------------|
| `search` | string | — | Search query (required) |
| `count` | number | `50` | Items per page |
| `skip` | number | `0` | Offset for paging |
**Response `200`:**
```json
{
"items": [ /* Item objects */ ],
"total": 128,
"count": 50,
"skip": 0
}
```
---
### `GET /randomitems`
Returns random items for carousel/recommendations. Respects `X-Region` and `X-Language` headers.
**Query params:**
| Param | Type | Default | Description |
|------------|--------|---------|------------------------------------|
| `count` | number | `5` | Number of items to return |
| `category` | number | — | Optional: limit to this category |
**Response `200`:** Array of [Item](#item-object) objects.
---
### Item Object
The backend can return items in **either** legacy format or BackOffice format. The frontend normalizes both.
```json
{
"categoryID": 1,
"itemID": 123,
"name": "iPhone 15 Pro",
"photos": [{ "url": "https://..." }],
"description": "Описание товара",
"currency": "RUB",
"price": 89990,
"discount": 10,
"remainings": "high",
"rating": 4.5,
"callbacks": [
{
"rating": 5,
"content": "Отличный товар!",
"userID": "user_123",
"timestamp": "2026-02-01T12:00:00Z"
}
],
"questions": [
{
"question": "Есть ли гарантия?",
"answer": "Да, 12 месяцев",
"upvotes": 5,
"downvotes": 0
}
],
"id": "item_abc123",
"visible": true,
"priority": 10,
"imgs": ["https://img1.jpg", "https://img2.jpg"],
"tags": ["new", "popular"],
"badges": ["bestseller", "sale"],
"simpleDescription": "Краткое описание",
"descriptionFields": [
{ "key": "Процессор", "value": "A17 Pro" },
{ "key": "Память", "value": "256 GB" }
],
"subcategoryId": "sub_001",
"translations": {
"en": {
"name": "iPhone 15 Pro",
"simpleDescription": "Short description",
"description": [
{ "key": "Processor", "value": "A17 Pro" }
]
},
"hy": {
"name": "iPhone 15 Pro",
"simpleDescription": "Կարcheck check check"
}
},
"comments": [
{
"id": "cmt_001",
"text": "Отличный товар!",
"author": "user_123",
"stars": 5,
"createdAt": "2026-02-01T12:00:00Z"
}
],
"quantity": 50
}
```
**Full Item fields:**
| Field | Type | Required | Description |
|---------------------|-------------------|----------|------------------------------------------------------------|
| `categoryID` | number | yes | Category this item belongs to |
| `itemID` | number | yes | Legacy numeric item ID |
| `name` | string | yes | Item display name |
| `photos` | Photo[] | no | Legacy photo array `[{ url }]` |
| `description` | string | yes | Text description |
| `currency` | string | yes | Currency code (default: `RUB`) |
| `price` | number | yes | Price in the currency's smallest display unit |
| `discount` | number | yes | Discount percentage (`0``100`) |
| `remainings` | string | no | Stock level: `high`, `medium`, `low`, `out` |
| `rating` | number | yes | Average rating (`0``5`) |
| `callbacks` | Review[] | no | Legacy reviews (alias for reviews) |
| `questions` | Question[] | no | Q&A entries |
| `id` | string | no | BackOffice string ID |
| `visible` | boolean | no | Whether item is visible (`true` default) |
| `priority` | number | no | Sort priority (higher = first) |
| `imgs` | string[] | no | BackOffice image URLs (maps to `photos`) |
| `tags` | string[] | no | Item tags for filtering |
| `badges` | string[] | no | Display badges (`bestseller`, `sale`, etc.) |
| `simpleDescription` | string | no | Short plain-text description |
| `descriptionFields` | DescriptionField[]| no | Structured `[{ key, value }]` descriptions |
| `subcategoryId` | string | no | BackOffice subcategory reference |
| `translations` | Record | no | Translations keyed by lang code (see below) |
| `comments` | Comment[] | no | BackOffice comments format |
| `quantity` | number | no | Numeric stock count (maps to `remainings` on frontend) |
**Nested types:**
| Type | Fields |
|--------------------|-----------------------------------------------------------------|
| `Photo` | `url: string`, `photo?: string`, `video?: string`, `type?: string` |
| `DescriptionField` | `key: string`, `value: string` |
| `Comment` | `id?: string`, `text: string`, `author?: string`, `stars?: number`, `createdAt?: string` |
| `Review` | `rating?: number`, `content?: string`, `userID?: string`, `answer?: string`, `timestamp?: string` |
| `Question` | `question: string`, `answer: string`, `upvotes: number`, `downvotes: number` |
| `ItemTranslation` | `name?: string`, `simpleDescription?: string`, `description?: DescriptionField[]` |
---
## 4. Cart
### `POST /cart` — Add item to cart
**Request body:**
```json
{ "itemID": 123, "quantity": 1 }
```
**Response `200`:**
```json
{ "message": "Added to cart" }
```
---
### `PATCH /cart` — Update item quantity
**Request body:**
```json
{ "itemID": 123, "quantity": 3 }
```
**Response `200`:**
```json
{ "message": "Updated" }
```
---
### `DELETE /cart` — Remove items from cart
**Request body:** Array of item IDs
```json
[123, 456]
```
**Response `200`:**
```json
{ "message": "Removed" }
```
---
### `GET /cart` — Get cart contents
**Response `200`:** Array of [Item](#item-object) objects (each with `quantity` field).
---
## 5. Payments (SBP / QR)
### `POST /cart` — Create payment (SBP QR)
> Note: Same endpoint as add-to-cart but with different body schema.
**Request body:**
```json
{
"amount": 89990,
"currency": "RUB",
"siteuserID": "tg_123456789",
"siteorderID": "order_abc123",
"redirectUrl": "",
"telegramUsername": "john_doe",
"items": [
{ "itemID": 123, "price": 89990, "name": "iPhone 15 Pro" }
]
}
```
**Response `200`:**
```json
{
"qrId": "qr_abc123",
"qrStatus": "CREATED",
"qrExpirationDate": "2026-02-28T13:00:00Z",
"payload": "https://qr.nspk.ru/...",
"qrUrl": "https://qr.nspk.ru/..."
}
```
---
### `GET /qr/payment/:qrId` — Check payment status
**Response `200`:**
```json
{
"additionalInfo": "",
"paymentPurpose": "Order #order_abc123",
"amount": 89990,
"code": "SUCCESS",
"createDate": "2026-02-28T12:00:00Z",
"currency": "RUB",
"order": "order_abc123",
"paymentStatus": "COMPLETED",
"qrId": "qr_abc123",
"transactionDate": "2026-02-28T12:01:00Z",
"transactionId": 999,
"qrExpirationDate": "2026-02-28T13:00:00Z",
"phoneNumber": "+7XXXXXXXXXX"
}
```
| `paymentStatus` values | Meaning |
|------------------------|---------------------------|
| `CREATED` | QR generated, not paid |
| `WAITING` | Payment in progress |
| `COMPLETED` | Payment successful |
| `EXPIRED` | QR code expired |
| `CANCELLED` | Payment cancelled |
---
### `POST /purchase-email` — Submit email after payment
**Request body:**
```json
{
"email": "user@example.com",
"telegramUserId": "123456789",
"items": [
{ "itemID": 123, "name": "iPhone 15 Pro", "price": 89990, "currency": "RUB" }
]
}
```
**Response `200`:**
```json
{ "message": "Email sent" }
```
---
## 6. Reviews / Comments
### `POST /comment` — Submit a review
**Request body:**
```json
{
"itemID": 123,
"rating": 5,
"comment": "Great product!",
"username": "john_doe",
"userId": 123456789,
"timestamp": "2026-02-28T12:00:00Z"
}
```
**Response `200`:**
```json
{ "message": "Review submitted" }
```
---
## 7. Regions
### `GET /regions` — List available regions
Returns regions where the marketplace operates.
**Response `200`:**
```json
[
{
@@ -46,52 +493,44 @@ Returns the list of regions where the marketplace operates.
```
**Region object:**
| Field | Type | Required | Description |
|---------------|--------|----------|------------------------------|
| `id` | string | yes | Unique region identifier |
| `city` | string | yes | City name (display) |
| `country` | string | yes | Country name (display) |
| `countryCode` | string | yes | ISO 3166-1 alpha-2 code |
| `timezone` | string | no | IANA timezone string |
> If this endpoint is unavailable, the frontend falls back to 6 hardcoded regions (Moscow, SPB, Yerevan, Minsk, Almaty, Tbilisi).
| Field | Type | Required | Description |
|---------------|--------|----------|--------------------------|
| `id` | string | yes | Unique region identifier |
| `city` | string | yes | City name (display) |
| `country` | string | yes | Country name |
| `countryCode` | string | yes | ISO 3166-1 alpha-2 |
| `timezone` | string | no | IANA timezone |
> **Fallback:** If this endpoint is down, the frontend uses 6 hardcoded defaults: Moscow, SPB, Yerevan, Minsk, Almaty, Tbilisi.
---
### 1.2 Region Query Parameter on Existing Endpoints
The following **existing** endpoints now accept an optional `?region=` query parameter:
| Endpoint | Example |
|---------------------------------|----------------------------------------------|
| `GET /category` | `GET /category?region=moscow` |
| `GET /category/:id` | `GET /category/5?count=50&skip=0&region=spb` |
| `GET /item/:id` | `GET /item/123?region=yerevan` |
| `GET /searchitems` | `GET /searchitems?search=phone&region=moscow` |
| `GET /randomitems` | `GET /randomitems?count=5&region=almaty` |
**Behavior:**
- If `region` param is **present** → return only items/categories available in that region
- If `region` param is **absent** → return all items globally (current behavior, no change)
- The `region` value matches the `id` field from the `/regions` response
---
## 2. Auth / Login Endpoints
## 8. Authentication (Telegram Login)
Authentication is **Telegram-based** with **cookie sessions** (HttpOnly, Secure, SameSite=None).
All auth endpoints must support CORS with `credentials: true`.
All auth endpoints must include `withCredentials: true` CORS support.
### 2.1 `GET /auth/session` — Check current session
### Auth flow
Called on every page load to check if the user has an active session.
```
1. User clicks "Checkout" → not authenticated → login dialog shown
2. User clicks "Log in with Telegram" → opens https://t.me/{bot}?start=auth_{callback}
3. User starts the bot in Telegram
4. Bot sends user data → backend /auth/telegram/callback
5. Backend creates session → sets Set-Cookie
6. Frontend polls GET /auth/session every 3s
7. Session detected → dialog closes → checkout proceeds
```
**Request:**
- Cookies: session cookie (set by backend)
- CORS: `withCredentials: true`
---
**Response `200 OK`** (authenticated):
### `GET /auth/session` — Check current session
**Request:** Cookies only (session cookie set by backend).
**Response `200`** (authenticated):
```json
{
"sessionId": "sess_abc123",
@@ -103,7 +542,7 @@ Called on every page load to check if the user has an active session.
}
```
**Response `200 OK`** (expired session):
**Response `200`** (expired):
```json
{
"sessionId": "sess_abc123",
@@ -115,37 +554,29 @@ Called on every page load to check if the user has an active session.
}
```
**Response `401 Unauthorized`** (no session / invalid cookie):
**Response `401`** (no session):
```json
{
"error": "No active session"
}
{ "error": "No active session" }
```
**AuthSession object:**
| Field | Type | Required | Description |
|------------------|---------|----------|------------------------------------------|
| `sessionId` | string | yes | Unique session ID |
| `telegramUserId` | number | yes | Telegram user ID |
| `username` | string? | no | Telegram @username (can be null) |
| `displayName` | string | yes | User display name (first_name + last_name) |
| `active` | boolean | yes | Whether session is currently valid |
| `expiresAt` | string | yes | ISO 8601 expiration datetime |
| Field | Type | Required | Description |
|------------------|---------|----------|--------------------------------------------|
| `sessionId` | string | yes | Unique session ID |
| `telegramUserId` | number | yes | Telegram user ID |
| `username` | string? | no | Telegram @username (can be null) |
| `displayName` | string | yes | User display name (first + last) |
| `active` | boolean | yes | Whether session is valid |
| `expiresAt` | string | yes | ISO 8601 expiration datetime |
---
### 2.2 `GET /auth/telegram/callback` — Telegram bot auth callback
### `GET /auth/telegram/callback` — Telegram bot auth callback
This is the URL the Telegram bot redirects to after the user starts the bot.
Called by the Telegram bot after user authenticates.
**Flow:**
1. Frontend generates link: `https://t.me/{botUsername}?start=auth_{encodedCallbackUrl}`
2. User clicks → opens Telegram → starts the bot
3. Bot sends user data to this callback endpoint
4. Backend creates session, sets `Set-Cookie` header
5. Frontend polls `GET /auth/session` every 3 seconds to detect when session becomes active
**Request** (from Telegram bot / webhook):
**Request body (from bot):**
```json
{
"id": 123456789,
@@ -158,7 +589,7 @@ This is the URL the Telegram bot redirects to after the user starts the bot.
}
```
**Response:** Should set a session cookie and return:
**Response:** Must set a session cookie and return:
```json
{
"sessionId": "sess_abc123",
@@ -167,93 +598,89 @@ This is the URL the Telegram bot redirects to after the user starts the bot.
```
**Cookie requirements:**
| Attribute | Value | Notes |
|------------|----------------|------------------------------------------|
| `HttpOnly` | `true` | Not accessible via JS |
| `Secure` | `true` | HTTPS only |
| Attribute | Value | Notes |
|------------|----------------|--------------------------------------------|
| `HttpOnly` | `true` | Not accessible via JS |
| `Secure` | `true` | HTTPS only |
| `SameSite` | `None` | Required for cross-origin (API ≠ frontend) |
| `Path` | `/` | |
| `Max-Age` | `86400` (24h) | Or as needed |
| `Domain` | API domain | |
> **Important:** Since the API domain differs from the frontend domain, `SameSite=None` + `Secure=true` is required for the cookie to be sent cross-origin.
| `Path` | `/` | |
| `Max-Age` | `86400` (24h) | Or as needed |
---
### 2.3 `POST /auth/logout` — End session
### `POST /auth/logout` — End session
**Request:**
- Cookies: session cookie
- CORS: `withCredentials: true`
- Body: `{}` (empty)
**Request:** Cookies only, empty body `{}`
**Response `200 OK`:**
**Response `200`:**
```json
{
"message": "Logged out"
}
{ "message": "Logged out" }
```
Should clear/invalidate the session cookie.
Must clear/invalidate the session cookie.
---
## 3. CORS Configuration
### Session refresh
For auth cookies to work cross-origin, the backend CORS config must include:
The frontend re-checks the session **60 seconds before `expiresAt`**. If the backend supports sliding expiration, it can reset the cookie's `Max-Age` on each `GET /auth/session`.
---
## 9. i18n / Translations
The frontend supports 3 languages: **Russian (ru)**, **English (en)**, **Armenian (hy)**.
The active language is sent via the `X-Language` HTTP header on every request.
### What the backend should do with `X-Language`
1. **Categories & items**: If `translations` field exists for the requested language, return the translated `name`, `description`, etc. OR the backend can apply translations server-side and return already-translated fields.
2. **The `translations` field** on items (optional approach):
```json
{
"translations": {
"en": {
"name": "iPhone 15 Pro",
"simpleDescription": "Short desc in English",
"description": [{ "key": "Processor", "value": "A17 Pro" }]
},
"hy": {
"name": "iPhone 15 Pro",
"simpleDescription": "Կarcheck check"
}
}
}
```
3. **Recommended approach**: Read `X-Language` header and return the `name`/`description` in that language directly. If no translation exists, return the Russian default.
---
## 10. CORS Configuration
For auth cookies and custom headers to work, the backend CORS config must include:
```
Access-Control-Allow-Origin: https://dexarmarket.ru (NOT *)
Access-Control-Allow-Origin: https://dexarmarket.ru (NOT wildcard *)
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Headers: Content-Type, X-Region, X-Language
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
```
> `Access-Control-Allow-Origin` **cannot** be `*` when `Allow-Credentials: true`. Must be the exact frontend origin.
> **Important:** `Access-Control-Allow-Origin` cannot be `*` when `Allow-Credentials: true`. Must be the exact frontend origin.
For Novo, also allow `https://novo.market`.
**Allowed origins:**
- `https://dexarmarket.ru`
- `https://novo.market`
- `http://localhost:4200` (dev)
- `http://localhost:4201` (dev, Novo)
---
## 4. Session Refresh Behavior
The frontend automatically re-checks the session **60 seconds before `expiresAt`**. If the backend supports session extension (sliding expiration), it can re-set the cookie with a fresh `Max-Age` on every `GET /auth/session` call.
---
## 5. Auth Gate — Checkout Flow
The checkout button (`POST /cart` payment) now requires authentication:
- If the user is **not logged in** → frontend shows a Telegram login dialog instead of proceeding
- If the user **is logged in** → checkout proceeds normally
- The session cookie is sent automatically with the payment request
No backend changes needed for the payment endpoint itself — just ensure it reads the session cookie if needed for order association.
---
## Summary of New Endpoints
| Method | Path | Purpose | Auth Required |
|--------|----------------------------|-----------------------------|---------------|
| `GET` | `/regions` | List available regions | No |
| `GET` | `/auth/session` | Check current session | Cookie |
| `GET` | `/auth/telegram/callback` | Telegram bot auth callback | No (from bot) |
| `POST` | `/auth/logout` | End session | Cookie |
## Summary of Modified Endpoints
| Method | Path | Change |
|--------|-------------------|---------------------------------------|
| `GET` | `/category` | Added optional `?region=` param |
| `GET` | `/category/:id` | Added optional `?region=` param |
| `GET` | `/item/:id` | Added optional `?region=` param |
| `GET` | `/searchitems` | Added optional `?region=` param |
| `GET` | `/randomitems` | Added optional `?region=` param |
---
## Telegram Bot Setup
## 11. Telegram Bot Setup
Each brand needs its own bot:
- **Dexar:** `@dexarmarket_bot`
@@ -262,5 +689,38 @@ Each brand needs its own bot:
The bot should:
1. Listen for `/start auth_{callbackUrl}` command
2. Extract the callback URL
3. Send the user's Telegram data (id, first_name, username, etc.) to that callback URL
3. Send the user's Telegram data (`id`, `first_name`, `username`, etc.) to that callback URL
4. The callback URL is `{apiUrl}/auth/telegram/callback`
---
## Complete Endpoint Reference
### New endpoints
| Method | Path | Description | Auth |
|--------|---------------------------|----------------------------|----------|
| `GET` | `/regions` | List available regions | No |
| `GET` | `/auth/session` | Check current session | Cookie |
| `GET` | `/auth/telegram/callback` | Telegram bot auth callback | No (bot) |
| `POST` | `/auth/logout` | End session | Cookie |
### Existing endpoints
| Method | Path | Description | Auth | Headers |
|----------|-----------------------|-------------------------|------|--------------------|
| `GET` | `/ping` | Health check | No | — |
| `GET` | `/category` | List categories | No | X-Region, X-Language |
| `GET` | `/category/:id` | Items in category | No | X-Region, X-Language |
| `GET` | `/item/:id` | Single item | No | X-Region, X-Language |
| `GET` | `/searchitems` | Search items | No | X-Region, X-Language |
| `GET` | `/randomitems` | Random items | No | X-Region, X-Language |
| `POST` | `/cart` | Add to cart / Payment | No* | — |
| `PATCH` | `/cart` | Update cart quantity | No* | — |
| `DELETE` | `/cart` | Remove from cart | No* | — |
| `GET` | `/cart` | Get cart contents | No* | — |
| `POST` | `/comment` | Submit review | No | — |
| `GET` | `/qr/payment/:qrId` | Check payment status | No | — |
| `POST` | `/purchase-email` | Submit email after pay | No | — |
> \* Cart/payment endpoints may use the session cookie if available for order association, but don't strictly require auth. The frontend enforces auth before checkout.

726
docs/API_DOCS_RU.md Normal file
View File

@@ -0,0 +1,726 @@
# Полная документация Backend API
> **Последнее обновление:** Февраль 2026
> **Фронтенд:** Angular 21 · Два бренда (Dexar + Novo)
> **Охватывает:** Каталог, Корзина, Оплата, Отзывы, Регионы, Авторизация, i18n, BackOffice
---
## Базовые URL
| Бренд | Dev | Production |
|--------|----------------------------------|----------------------------------|
| Dexar | `https://api.dexarmarket.ru:445` | `https://api.dexarmarket.ru:445` |
| Novo | `https://api.novo.market:444` | `https://api.novo.market:444` |
---
## Глобальные HTTP-заголовки
Фронтенд **автоматически добавляет** два кастомных заголовка к **каждому API-запросу** через interceptor. Бэкенд должен читать эти заголовки и использовать для фильтрации/перевода ответов.
| Заголовок | Пример значения | Описание |
|---------------|-----------------|-------------------------------------------------------------------|
| `X-Region` | `moscow` | ID региона, выбранного пользователем. **Отсутствует** = все регионы. |
| `X-Language` | `ru` | Активный язык интерфейса: `ru`, `en` или `hy`. |
### Поведение бэкенда
- **`X-Region`**: Если присутствует — фильтровать товары/категории только по этому региону. Если отсутствует — возвращать всё (глобальный каталог).
- **`X-Language`**: Если присутствует — возвращать переведённые `name`, `description` и т.д., если переводы существуют. Если отсутствует или `ru` — возвращать на русском (по умолчанию).
### Требования CORS для этих заголовков
```
Access-Control-Allow-Headers: Content-Type, X-Region, X-Language
```
---
## 1. Проверка состояния
### `GET /ping`
Простая проверка работоспособности.
**Ответ `200`:**
```json
{ "message": "pong" }
```
---
## 2. Каталог — Категории
### `GET /category`
Возвращает все категории верхнего уровня. Учитывает заголовки `X-Region` и `X-Language`.
**Ответ `200`:**
```json
[
{
"categoryID": 1,
"name": "Электроника",
"parentID": 0,
"icon": "https://...",
"wideBanner": "https://...",
"itemCount": 42,
"priority": 10,
"id": "cat_abc123",
"visible": true,
"img": "https://...",
"projectId": "proj_xyz",
"subcategories": [
{
"id": "sub_001",
"name": "Смартфоны",
"visible": true,
"priority": 5,
"img": "https://...",
"categoryId": "cat_abc123",
"parentId": "cat_abc123",
"itemCount": 20,
"hasItems": true,
"subcategories": []
}
]
}
]
```
**Объект Category:**
| Поле | Тип | Обязат. | Описание |
|------------------|---------------|---------|-----------------------------------------------------|
| `categoryID` | number | да | Числовой ID (legacy) |
| `name` | string | да | Название категории (переведённое если `X-Language`) |
| `parentID` | number | да | ID родительской категории (`0` = верхний уровень) |
| `icon` | string | нет | URL иконки категории |
| `wideBanner` | string | нет | URL широкого баннера |
| `itemCount` | number | нет | Количество товаров в категории |
| `priority` | number | нет | Приоритет сортировки (больше = выше) |
| `id` | string | нет | Строковый ID из BackOffice |
| `visible` | boolean | нет | Видима ли категория (по умолч. `true`) |
| `img` | string | нет | URL изображения из BackOffice (маппится на `icon`) |
| `projectId` | string | нет | Ссылка на проект в BackOffice |
| `subcategories` | Subcategory[] | нет | Вложенные подкатегории |
**Объект Subcategory:**
| Поле | Тип | Обязат. | Описание |
|------------------|---------------|---------|----------------------------------|
| `id` | string | да | ID подкатегории |
| `name` | string | да | Отображаемое название |
| `visible` | boolean | нет | Видима ли |
| `priority` | number | нет | Приоритет сортировки |
| `img` | string | нет | URL изображения |
| `categoryId` | string | да | ID родительской категории |
| `parentId` | string | да | ID прямого родителя |
| `itemCount` | number | нет | Количество товаров |
| `hasItems` | boolean | нет | Есть ли товары |
| `subcategories` | Subcategory[] | нет | Вложенные дочерние подкатегории |
---
### `GET /category/:categoryID`
Возвращает товары определённой категории. Учитывает заголовки `X-Region` и `X-Language`.
**Query-параметры:**
| Параметр | Тип | По умолч. | Описание |
|----------|--------|-----------|-----------------------|
| `count` | number | `50` | Товаров на страницу |
| `skip` | number | `0` | Смещение для пагинации |
**Ответ `200`:** Массив объектов [Item](#объект-item).
---
## 3. Товары
### `GET /item/:itemID`
Возвращает один товар. Учитывает заголовки `X-Region` и `X-Language`.
**Ответ `200`:** Один объект [Item](#объект-item).
---
### `GET /searchitems`
Полнотекстовый поиск по товарам. Учитывает заголовки `X-Region` и `X-Language`.
**Query-параметры:**
| Параметр | Тип | По умолч. | Описание |
|----------|--------|-----------|-------------------------------|
| `search` | string | — | Поисковый запрос (обязателен) |
| `count` | number | `50` | Товаров на страницу |
| `skip` | number | `0` | Смещение для пагинации |
**Ответ `200`:**
```json
{
"items": [ /* объекты Item */ ],
"total": 128,
"count": 50,
"skip": 0
}
```
---
### `GET /randomitems`
Возвращает случайные товары для карусели/рекомендаций. Учитывает заголовки `X-Region` и `X-Language`.
**Query-параметры:**
| Параметр | Тип | По умолч. | Описание |
|------------|--------|-----------|--------------------------------------|
| `count` | number | `5` | Количество товаров |
| `category` | number | — | Ограничить данной категорией (опц.) |
**Ответ `200`:** Массив объектов [Item](#объект-item).
---
### Объект Item
Бэкенд может возвращать товары в **любом** из двух форматов — legacy или BackOffice. Фронтенд нормализует оба варианта.
```json
{
"categoryID": 1,
"itemID": 123,
"name": "iPhone 15 Pro",
"photos": [{ "url": "https://..." }],
"description": "Описание товара",
"currency": "RUB",
"price": 89990,
"discount": 10,
"remainings": "high",
"rating": 4.5,
"callbacks": [
{
"rating": 5,
"content": "Отличный товар!",
"userID": "user_123",
"timestamp": "2026-02-01T12:00:00Z"
}
],
"questions": [
{
"question": "Есть ли гарантия?",
"answer": "Да, 12 месяцев",
"upvotes": 5,
"downvotes": 0
}
],
"id": "item_abc123",
"visible": true,
"priority": 10,
"imgs": ["https://img1.jpg", "https://img2.jpg"],
"tags": ["new", "popular"],
"badges": ["bestseller", "sale"],
"simpleDescription": "Краткое описание",
"descriptionFields": [
{ "key": "Процессор", "value": "A17 Pro" },
{ "key": "Память", "value": "256 GB" }
],
"subcategoryId": "sub_001",
"translations": {
"en": {
"name": "iPhone 15 Pro",
"simpleDescription": "Short description",
"description": [
{ "key": "Processor", "value": "A17 Pro" }
]
},
"hy": {
"name": "iPhone 15 Pro",
"simpleDescription": "Կարcheck check"
}
},
"comments": [
{
"id": "cmt_001",
"text": "Отличный товар!",
"author": "user_123",
"stars": 5,
"createdAt": "2026-02-01T12:00:00Z"
}
],
"quantity": 50
}
```
**Все поля Item:**
| Поле | Тип | Обязат. | Описание |
|---------------------|-------------------|---------|-----------------------------------------------------------|
| `categoryID` | number | да | Категория, к которой принадлежит товар |
| `itemID` | number | да | Числовой ID товара (legacy) |
| `name` | string | да | Название товара |
| `photos` | Photo[] | нет | Массив фотографий `[{ url }]` (legacy) |
| `description` | string | да | Текстовое описание |
| `currency` | string | да | Код валюты (по умолч. `RUB`) |
| `price` | number | да | Цена |
| `discount` | number | да | Процент скидки (`0``100`) |
| `remainings` | string | нет | Уровень остатка: `high`, `medium`, `low`, `out` |
| `rating` | number | да | Средний рейтинг (`0``5`) |
| `callbacks` | Review[] | нет | Отзывы (legacy формат) |
| `questions` | Question[] | нет | Вопросы и ответы |
| `id` | string | нет | Строковый ID из BackOffice |
| `visible` | boolean | нет | Виден ли товар (по умолч. `true`) |
| `priority` | number | нет | Приоритет сортировки (больше = выше) |
| `imgs` | string[] | нет | URL картинок из BackOffice (маппится на `photos`) |
| `tags` | string[] | нет | Теги для фильтрации |
| `badges` | string[] | нет | Бейджи (`bestseller`, `sale` и т.д.) |
| `simpleDescription` | string | нет | Краткое текстовое описание |
| `descriptionFields` | DescriptionField[]| нет | Структурированное описание `[{ key, value }]` |
| `subcategoryId` | string | нет | Ссылка на подкатегорию из BackOffice |
| `translations` | Record | нет | Переводы по ключу языка (см. ниже) |
| `comments` | Comment[] | нет | Комментарии в формате BackOffice |
| `quantity` | number | нет | Числовое кол-во на складе (маппится на `remainings`) |
**Вложенные типы:**
| Тип | Поля |
|--------------------|-----------------------------------------------------------------|
| `Photo` | `url: string`, `photo?: string`, `video?: string`, `type?: string` |
| `DescriptionField` | `key: string`, `value: string` |
| `Comment` | `id?: string`, `text: string`, `author?: string`, `stars?: number`, `createdAt?: string` |
| `Review` | `rating?: number`, `content?: string`, `userID?: string`, `answer?: string`, `timestamp?: string` |
| `Question` | `question: string`, `answer: string`, `upvotes: number`, `downvotes: number` |
| `ItemTranslation` | `name?: string`, `simpleDescription?: string`, `description?: DescriptionField[]` |
---
## 4. Корзина
### `POST /cart` — Добавить товар в корзину
**Тело запроса:**
```json
{ "itemID": 123, "quantity": 1 }
```
**Ответ `200`:**
```json
{ "message": "Added to cart" }
```
---
### `PATCH /cart` — Обновить количество товара
**Тело запроса:**
```json
{ "itemID": 123, "quantity": 3 }
```
**Ответ `200`:**
```json
{ "message": "Updated" }
```
---
### `DELETE /cart` — Удалить товары из корзины
**Тело запроса:** Массив ID товаров
```json
[123, 456]
```
**Ответ `200`:**
```json
{ "message": "Removed" }
```
---
### `GET /cart` — Получить содержимое корзины
**Ответ `200`:** Массив объектов [Item](#объект-item) (каждый с полем `quantity`).
---
## 5. Оплата (СБП / QR)
### `POST /cart` — Создать платёж (СБП QR)
> Примечание: Тот же эндпоинт что и добавление в корзину, но с другой схемой тела запроса.
**Тело запроса:**
```json
{
"amount": 89990,
"currency": "RUB",
"siteuserID": "tg_123456789",
"siteorderID": "order_abc123",
"redirectUrl": "",
"telegramUsername": "john_doe",
"items": [
{ "itemID": 123, "price": 89990, "name": "iPhone 15 Pro" }
]
}
```
**Ответ `200`:**
```json
{
"qrId": "qr_abc123",
"qrStatus": "CREATED",
"qrExpirationDate": "2026-02-28T13:00:00Z",
"payload": "https://qr.nspk.ru/...",
"qrUrl": "https://qr.nspk.ru/..."
}
```
---
### `GET /qr/payment/:qrId` — Проверить статус оплаты
**Ответ `200`:**
```json
{
"additionalInfo": "",
"paymentPurpose": "Order #order_abc123",
"amount": 89990,
"code": "SUCCESS",
"createDate": "2026-02-28T12:00:00Z",
"currency": "RUB",
"order": "order_abc123",
"paymentStatus": "COMPLETED",
"qrId": "qr_abc123",
"transactionDate": "2026-02-28T12:01:00Z",
"transactionId": 999,
"qrExpirationDate": "2026-02-28T13:00:00Z",
"phoneNumber": "+7XXXXXXXXXX"
}
```
| Значение `paymentStatus` | Значение |
|--------------------------|------------------------------|
| `CREATED` | QR создан, не оплачен |
| `WAITING` | Оплата в процессе |
| `COMPLETED` | Оплата успешна |
| `EXPIRED` | QR-код истёк |
| `CANCELLED` | Оплата отменена |
---
### `POST /purchase-email` — Отправить email после оплаты
**Тело запроса:**
```json
{
"email": "user@example.com",
"telegramUserId": "123456789",
"items": [
{ "itemID": 123, "name": "iPhone 15 Pro", "price": 89990, "currency": "RUB" }
]
}
```
**Ответ `200`:**
```json
{ "message": "Email sent" }
```
---
## 6. Отзывы / Комментарии
### `POST /comment` — Оставить отзыв
**Тело запроса:**
```json
{
"itemID": 123,
"rating": 5,
"comment": "Отличный товар!",
"username": "john_doe",
"userId": 123456789,
"timestamp": "2026-02-28T12:00:00Z"
}
```
**Ответ `200`:**
```json
{ "message": "Review submitted" }
```
---
## 7. Регионы
### `GET /regions` — Список доступных регионов
Возвращает регионы, в которых работает маркетплейс.
**Ответ `200`:**
```json
[
{
"id": "moscow",
"city": "Москва",
"country": "Россия",
"countryCode": "RU",
"timezone": "Europe/Moscow"
},
{
"id": "spb",
"city": "Санкт-Петербург",
"country": "Россия",
"countryCode": "RU",
"timezone": "Europe/Moscow"
},
{
"id": "yerevan",
"city": "Ереван",
"country": "Армения",
"countryCode": "AM",
"timezone": "Asia/Yerevan"
}
]
```
**Объект Region:**
| Поле | Тип | Обязат. | Описание |
|---------------|--------|---------|----------------------------------|
| `id` | string | да | Уникальный идентификатор региона |
| `city` | string | да | Название города |
| `country` | string | да | Название страны |
| `countryCode` | string | да | Код страны ISO 3166-1 alpha-2 |
| `timezone` | string | нет | Часовой пояс IANA |
> **Фоллбэк:** Если эндпоинт недоступен, фронтенд использует 6 захардкоженных значений: Москва, СПб, Ереван, Минск, Алматы, Тбилиси.
---
## 8. Авторизация (вход через Telegram)
Авторизация **через Telegram** с **cookie-сессиями** (HttpOnly, Secure, SameSite=None).
Все auth-эндпоинты должны поддерживать CORS с `credentials: true`.
### Процесс авторизации
```
1. Пользователь нажимает «Оформить заказ» → не авторизован → показывается диалог входа
2. Нажимает «Войти через Telegram» → открывается https://t.me/{bot}?start=auth_{callback}
3. Пользователь запускает бота в Telegram
4. Бот отправляет данные пользователя → бэкенд /auth/telegram/callback
5. Бэкенд создаёт сессию → устанавливает Set-Cookie
6. Фронтенд опрашивает GET /auth/session каждые 3 секунды
7. Сессия обнаружена → диалог закрывается → оформление заказа продолжается
```
---
### `GET /auth/session` — Проверить текущую сессию
**Запрос:** Только cookie (сессионная cookie, установленная бэкендом).
**Ответ `200`** (авторизован):
```json
{
"sessionId": "sess_abc123",
"telegramUserId": 123456789,
"username": "john_doe",
"displayName": "John Doe",
"active": true,
"expiresAt": "2026-03-01T12:00:00Z"
}
```
**Ответ `200`** (сессия истекла):
```json
{
"sessionId": "sess_abc123",
"telegramUserId": 123456789,
"username": "john_doe",
"displayName": "John Doe",
"active": false,
"expiresAt": "2026-02-27T12:00:00Z"
}
```
**Ответ `401`** (нет сессии):
```json
{ "error": "No active session" }
```
**Объект AuthSession:**
| Поле | Тип | Обязат. | Описание |
|------------------|---------|---------|-------------------------------------------|
| `sessionId` | string | да | Уникальный ID сессии |
| `telegramUserId` | number | да | ID пользователя в Telegram |
| `username` | string? | нет | @username в Telegram (может быть null) |
| `displayName` | string | да | Отображаемое имя (имя + фамилия) |
| `active` | boolean | да | Действительна ли сессия |
| `expiresAt` | string | да | Дата истечения в формате ISO 8601 |
---
### `GET /auth/telegram/callback` — Callback авторизации Telegram-бота
Вызывается Telegram-ботом после авторизации пользователя.
**Тело запроса (от бота):**
```json
{
"id": 123456789,
"first_name": "John",
"last_name": "Doe",
"username": "john_doe",
"photo_url": "https://t.me/i/userpic/...",
"auth_date": 1709100000,
"hash": "abc123def456..."
}
```
**Ответ:** Должен установить cookie сессии и вернуть:
```json
{
"sessionId": "sess_abc123",
"message": "Authenticated successfully"
}
```
**Требования к cookie:**
| Атрибут | Значение | Примечание |
|------------|----------------|-----------------------------------------------------|
| `HttpOnly` | `true` | Недоступна из JavaScript |
| `Secure` | `true` | Только HTTPS |
| `SameSite` | `None` | Обязательно для cross-origin (API ≠ фронтенд) |
| `Path` | `/` | |
| `Max-Age` | `86400` (24ч) | Или по необходимости |
---
### `POST /auth/logout` — Завершить сессию
**Запрос:** Только cookie, пустое тело `{}`
**Ответ `200`:**
```json
{ "message": "Logged out" }
```
Должен очистить/инвалидировать cookie сессии.
---
### Обновление сессии
Фронтенд повторно проверяет сессию за **60 секунд до `expiresAt`**. Если бэкенд поддерживает скользящий срок действия (sliding expiration), можно обновлять `Max-Age` cookie при каждом вызове `GET /auth/session`.
---
## 9. i18n / Переводы
Фронтенд поддерживает 3 языка: **Русский (ru)**, **Английский (en)**, **Армянский (hy)**.
Активный язык отправляется через HTTP-заголовок `X-Language` с каждым запросом.
### Что бэкенд должен делать с `X-Language`
1. **Категории и товары**: Если для запрошенного языка есть поле `translations`, вернуть переведённые `name`, `description` и т.д. ИЛИ бэкенд может применять переводы на стороне сервера и возвращать уже переведённые поля.
2. **Поле `translations`** на товарах (опциональный подход):
```json
{
"translations": {
"en": {
"name": "iPhone 15 Pro",
"simpleDescription": "Short desc in English",
"description": [{ "key": "Processor", "value": "A17 Pro" }]
},
"hy": {
"name": "iPhone 15 Pro",
"simpleDescription": "Կarcheck check"
}
}
}
```
3. **Рекомендуемый подход**: Читать заголовок `X-Language` и возвращать `name`/`description` на этом языке напрямую. Если перевода нет — возвращать русский вариант по умолчанию.
---
## 10. Настройка CORS
Для работы auth-cookie и кастомных заголовков конфигурация CORS бэкенда должна включать:
```
Access-Control-Allow-Origin: https://dexarmarket.ru (НЕ wildcard *)
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type, X-Region, X-Language
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS
```
> **Важно:** `Access-Control-Allow-Origin` не может быть `*` при `Allow-Credentials: true`. Должен быть точный origin фронтенда.
**Разрешённые origins:**
- `https://dexarmarket.ru`
- `https://novo.market`
- `http://localhost:4200` (dev)
- `http://localhost:4201` (dev, Novo)
---
## 11. Настройка Telegram-бота
Каждому бренду нужен свой бот:
- **Dexar:** `@dexarmarket_bot`
- **Novo:** `@novomarket_bot`
Бот должен:
1. Слушать команду `/start auth_{callbackUrl}`
2. Извлечь callback URL
3. Отправить данные пользователя (`id`, `first_name`, `username` и т.д.) на этот callback URL
4. Callback URL: `{apiUrl}/auth/telegram/callback`
---
## Полный справочник эндпоинтов
### Новые эндпоинты
| Метод | Путь | Описание | Авторизация |
|--------|---------------------------|---------------------------------|-------------|
| `GET` | `/regions` | Список доступных регионов | Нет |
| `GET` | `/auth/session` | Проверка текущей сессии | Cookie |
| `GET` | `/auth/telegram/callback` | Callback авторизации через бота | Нет (бот) |
| `POST` | `/auth/logout` | Завершение сессии | Cookie |
### Существующие эндпоинты
| Метод | Путь | Описание | Авт. | Заголовки |
|----------|-----------------------|---------------------------|------|--------------------|
| `GET` | `/ping` | Проверка состояния | Нет | — |
| `GET` | `/category` | Список категорий | Нет | X-Region, X-Language |
| `GET` | `/category/:id` | Товары категории | Нет | X-Region, X-Language |
| `GET` | `/item/:id` | Один товар | Нет | X-Region, X-Language |
| `GET` | `/searchitems` | Поиск товаров | Нет | X-Region, X-Language |
| `GET` | `/randomitems` | Случайные товары | Нет | X-Region, X-Language |
| `POST` | `/cart` | Добавить в корзину / Оплата | Нет* | — |
| `PATCH` | `/cart` | Обновить кол-во | Нет* | — |
| `DELETE` | `/cart` | Удалить из корзины | Нет* | — |
| `GET` | `/cart` | Содержимое корзины | Нет* | — |
| `POST` | `/comment` | Оставить отзыв | Нет | — |
| `GET` | `/qr/payment/:qrId` | Статус оплаты | Нет | — |
| `POST` | `/purchase-email` | Отправить email после оплаты | Нет | — |
> \* Эндпоинты корзины/оплаты могут использовать cookie сессии (если есть) для привязки к заказу, но не требуют авторизации строго. Фронтенд проверяет авторизацию перед оформлением заказа.

View File

@@ -36,6 +36,9 @@ server {
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://telegram.org; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https:; frame-src https://telegram.org;" always;
# Brotli compression (if available)
# brotli on;

73
package-lock.json generated
View File

@@ -8,11 +8,15 @@
"name": "dexarmarket",
"version": "0.0.0",
"dependencies": {
"@angular/animations": "^21.1.5",
"@angular/cdk": "^21.1.5",
"@angular/common": "^21.0.6",
"@angular/compiler": "^21.0.6",
"@angular/core": "^21.0.6",
"@angular/forms": "^21.0.6",
"@angular/material": "^21.1.5",
"@angular/platform-browser": "^21.0.6",
"@angular/platform-browser-dynamic": "^21.1.5",
"@angular/router": "^21.0.6",
"@angular/service-worker": "^21.0.6",
"primeicons": "^7.0.0",
@@ -324,6 +328,21 @@
"yarn": ">= 1.13.0"
}
},
"node_modules/@angular/animations": {
"version": "21.1.5",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.1.5.tgz",
"integrity": "sha512-gsqHX8lCYV8cgVtHs0iLwrX8SVlmcjUF44l/xCc/jBC/TeKWRl2e6Jqrn1Wcd0NDlGiNsm+mYNyqMyy5/I7kjw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/core": "21.1.5"
}
},
"node_modules/@angular/build": {
"version": "21.1.0",
"resolved": "https://registry.npmjs.org/@angular/build/-/build-21.1.0.tgz",
@@ -472,6 +491,22 @@
"dev": true,
"license": "MIT"
},
"node_modules/@angular/cdk": {
"version": "21.1.5",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.1.5.tgz",
"integrity": "sha512-AlQPgqe3LLwXCyrDwYSX3m/WKnl2ppCMW7Gb+7bJpIcpMdWYEpSOSQF318jXGYIysKg43YbdJ1tWhJWY/cbn3w==",
"license": "MIT",
"dependencies": {
"parse5": "^8.0.0",
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": "^21.0.0 || ^22.0.0",
"@angular/core": "^21.0.0 || ^22.0.0",
"@angular/platform-browser": "^21.0.0 || ^22.0.0",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/cli": {
"version": "21.1.0",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.1.0.tgz",
@@ -613,6 +648,23 @@
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/material": {
"version": "21.1.5",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-21.1.5.tgz",
"integrity": "sha512-D6JvFulPvIKhPJ52prMV7DxwYMzcUpHar11ZcMb7r9WQzUfCS3FDPXfMAce5n3h+3kFccfmmGpnyBwqTlLPSig==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/cdk": "21.1.5",
"@angular/common": "^21.0.0 || ^22.0.0",
"@angular/core": "^21.0.0 || ^22.0.0",
"@angular/forms": "^21.0.0 || ^22.0.0",
"@angular/platform-browser": "^21.0.0 || ^22.0.0",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/platform-browser": {
"version": "21.0.6",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.6.tgz",
@@ -635,6 +687,24 @@
}
}
},
"node_modules/@angular/platform-browser-dynamic": {
"version": "21.1.5",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.1.5.tgz",
"integrity": "sha512-Pd8nPbJSIONnze1WS9wLBAtaFw4TYIH+ZGjKHS9G1E9l09tDWtHWyB7dY82Sc//Nc8iR4V7dcsbUmFjOJHThww==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@angular/common": "21.1.5",
"@angular/compiler": "21.1.5",
"@angular/core": "21.1.5",
"@angular/platform-browser": "21.1.5"
}
},
"node_modules/@angular/router": {
"version": "21.0.6",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-21.0.6.tgz",
@@ -7687,7 +7757,6 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
"dev": true,
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
@@ -7741,7 +7810,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
@@ -9512,3 +9580,4 @@
}
}
}

View File

@@ -16,11 +16,15 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^21.1.5",
"@angular/cdk": "^21.1.5",
"@angular/common": "^21.0.6",
"@angular/compiler": "^21.0.6",
"@angular/core": "^21.0.6",
"@angular/forms": "^21.0.6",
"@angular/material": "^21.1.5",
"@angular/platform-browser": "^21.0.6",
"@angular/platform-browser-dynamic": "^21.1.5",
"@angular/router": "^21.0.6",
"@angular/service-worker": "^21.0.6",
"primeicons": "^7.0.0",
@@ -43,3 +47,4 @@
"typescript": "~5.9.3"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,5 +1,4 @@
{
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
"name": "Novo Market - Интернет-магазин",
"short_name": "Novo",
"description": "Novo Market - ваш онлайн магазин качественных товаров с доставкой",
@@ -12,34 +11,10 @@
"categories": ["shopping", "lifestyle"],
"icons": [
{
"src": "icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "maskable any"
"src": "assets/images/novo-favicon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "icons/icon-192x192.png",
@@ -47,12 +22,6 @@
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-512x512.png",
"sizes": "512x512",

View File

@@ -11,34 +11,10 @@
"categories": ["shopping", "marketplace"],
"icons": [
{
"src": "icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "maskable any"
"src": "assets/images/dexar-favicon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "icons/icon-192x192.png",
@@ -46,12 +22,6 @@
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "icons/icon-512x512.png",
"sizes": "512x512",

View File

@@ -4,6 +4,8 @@ import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { cacheInterceptor } from './interceptors/cache.interceptor';
import { apiHeadersInterceptor } from './interceptors/api-headers.interceptor';
import { mockDataInterceptor } from './interceptors/mock-data.interceptor';
import { provideServiceWorker } from '@angular/service-worker';
export const appConfig: ApplicationConfig = {
@@ -15,7 +17,7 @@ export const appConfig: ApplicationConfig = {
withInMemoryScrolling({ scrollPositionRestoration: 'top' })
),
provideHttpClient(
withInterceptors([cacheInterceptor])
withInterceptors([mockDataInterceptor, apiHeadersInterceptor, cacheInterceptor])
),
provideServiceWorker('ngsw-worker.js', {
enabled: !isDevMode(),

View File

@@ -12,10 +12,10 @@
</div>
} @else {
<app-header></app-header>
@if (!isHomePage()) {
<app-back-button />
}
<main class="main-content">
@if (!isHomePage()) {
<app-back-button />
}
<router-outlet></router-outlet>
</main>
<app-footer></app-footer>

View File

@@ -1,18 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { App } from './app';
import { provideRouter } from '@angular/router';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
providers: [provideRouter([])]
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
});

View File

@@ -17,14 +17,16 @@ import { TranslateService } from '../../i18n/translate.service';
`,
styles: [`
.dexar-back-btn {
position: fixed;
top: 76px;
position: sticky;
top: 72px;
left: 20px;
z-index: 100;
background: none;
border: none;
cursor: pointer;
padding: 4px;
padding: 8px 4px;
margin-bottom: -40px;
width: fit-content;
transition: transform 0.2s ease;
svg path {
@@ -47,7 +49,7 @@ import { TranslateService } from '../../i18n/translate.service';
@media (max-width: 768px) {
.dexar-back-btn {
top: 68px;
top: 64px;
left: 12px;
svg {

View File

@@ -30,8 +30,8 @@
<app-region-selector />
<app-language-selector />
<a [routerLink]="'/cart' | langRoute" routerLinkActive="novo-cart-active" class="novo-cart" (click)="closeMenu()">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<a [routerLink]="'/cart' | langRoute" routerLinkActive="novo-cart-active" class="novo-cart" (click)="closeMenu()" [attr.aria-label]="'header.cart' | translate">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="9" cy="21" r="1"></circle>
<circle cx="20" cy="21" r="1"></circle>
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
@@ -41,7 +41,7 @@
}
</a>
<button class="menu-toggle" (click)="toggleMenu()" [class.active]="menuOpen">
<button class="menu-toggle" (click)="toggleMenu()" [class.active]="menuOpen" [attr.aria-label]="menuOpen ? 'Close menu' : 'Open menu'" [attr.aria-expanded]="menuOpen">
<span></span>
<span></span>
<span></span>
@@ -118,7 +118,7 @@
</div>
<!-- Mobile Menu Toggle -->
<button class="dexar-menu-toggle" (click)="toggleMenu()" [class.active]="menuOpen">
<button class="dexar-menu-toggle" (click)="toggleMenu()" [class.active]="menuOpen" [attr.aria-label]="menuOpen ? 'Close menu' : 'Open menu'" [attr.aria-expanded]="menuOpen">
<span></span>
<span></span>
<span></span>

View File

@@ -18,14 +18,21 @@
<div class="item-card">
<a [routerLink]="['/item', product.itemID] | langRoute" class="item-link">
<div class="item-image">
<img [src]="getItemImage(product)" [alt]="product.name" loading="lazy" />
<img [src]="getItemImage(product)" [alt]="itemName(product)" loading="lazy" />
@if (product.discount > 0) {
<span class="discount-badge">-{{ product.discount }}%</span>
}
@if (product.badges && product.badges.length > 0) {
<div class="item-badges-overlay">
@for (badge of product.badges; track badge) {
<span class="item-badge" [class]="getBadgeClass(badge)">{{ badge }}</span>
}
</div>
}
</div>
<div class="item-details">
<h3 class="item-name">{{ product.name }}</h3>
<h3 class="item-name">{{ itemName(product) }}</h3>
@if (product.rating) {
<div class="item-rating">

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, signal, ChangeDetectionStrategy } from '@angular/core';
import { Component, OnInit, signal, ChangeDetectionStrategy, inject } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { RouterLink } from '@angular/router';
import { CarouselModule } from 'primeng/carousel';
@@ -7,7 +7,8 @@ import { TagModule } from 'primeng/tag';
import { ApiService, CartService } from '../../services';
import { Item } from '../../models';
import { environment } from '../../../environments/environment';
import { getDiscountedPrice, getMainImage } from '../../utils/item.utils';
import { getDiscountedPrice, getMainImage, getBadgeClass, getTranslatedField } from '../../utils/item.utils';
import { LanguageService } from '../../services/language.service';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
@@ -98,6 +99,10 @@ export class ItemsCarouselComponent implements OnInit {
readonly getItemImage = getMainImage;
readonly getDiscountedPrice = getDiscountedPrice;
readonly getBadgeClass = getBadgeClass;
private langService = inject(LanguageService);
itemName(product: Item): string { return getTranslatedField(product, 'name', this.langService.currentLanguage()); }
addToCart(event: Event, item: Item): void {
event.preventDefault();

View File

@@ -1,5 +1,5 @@
<div class="language-selector">
<button class="language-button" (click)="toggleDropdown()">
<div class="language-selector" role="listbox">
<button class="language-button" (click)="toggleDropdown()" (keydown)="onKeyDown($event)" aria-haspopup="listbox" [attr.aria-expanded]="dropdownOpen">
<img [src]="languageService.getCurrentLanguage()?.flagSvg"
[alt]="languageService.getCurrentLanguage()?.name"
class="language-flag">
@@ -13,6 +13,8 @@
@for (lang of languageService.languages; track lang.code) {
<button
class="language-option"
role="option"
[attr.aria-selected]="languageService.currentLanguage() === lang.code"
[class.active]="languageService.currentLanguage() === lang.code"
[class.disabled]="!lang.enabled"
[disabled]="!lang.enabled"
@@ -22,4 +24,25 @@
</button>
}
</div>
<button class="currency-button" (click)="toggleCurrency()">
<span class="currency-symbol">{{ languageService.getCurrentCurrency()?.symbol }}</span>
<span class="currency-code">{{ languageService.currentCurrency() }}</span>
<svg class="dropdown-arrow" [class.rotated]="currencyOpen" width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2.5 4.5L6 8L9.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="currency-dropdown" [class.open]="currencyOpen">
@for (cur of languageService.currencies; track cur.code) {
<button
class="currency-option"
[class.active]="languageService.currentCurrency() === cur.code"
(click)="selectCurrency(cur)">
<span class="cur-symbol">{{ cur.symbol }}</span>
<span class="cur-name">{{ cur.name }}</span>
<span class="cur-code">{{ cur.code }}</span>
</button>
}
</div>
</div>

View File

@@ -301,3 +301,162 @@
}
}
}
// ── Currency selector ──
.language-selector {
display: inline-flex;
align-items: center;
gap: 6px;
}
.currency-button {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 10px;
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
color: #ffffff;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
font-weight: 500;
&:hover {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.3);
}
.currency-symbol {
font-size: 15px;
font-weight: 700;
}
.currency-code {
font-size: 13px;
letter-spacing: 0.5px;
}
.dropdown-arrow {
transition: transform 0.3s ease;
opacity: 0.7;
&.rotated { transform: rotate(180deg); }
}
}
.currency-dropdown {
position: absolute;
top: calc(100% + 8px);
right: 0;
min-width: 170px;
background: var(--card-bg, #1a1a1a);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s ease;
z-index: 1000;
overflow: hidden;
&.open {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
}
.currency-option {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 11px 16px;
background: transparent;
border: none;
color: #ffffff;
cursor: pointer;
transition: background 0.2s ease;
font-size: 14px;
&:hover {
background: rgba(255, 255, 255, 0.05);
}
&.active {
background: rgba(255, 255, 255, 0.1);
font-weight: 600;
}
.cur-symbol {
font-size: 16px;
font-weight: 700;
width: 20px;
text-align: center;
}
.cur-name {
flex: 1;
}
.cur-code {
font-size: 12px;
opacity: 0.6;
}
}
// Light / Novo / Dexar theme adjustments for currency
:host-context(.novo-header),
:host-context(.header) {
.currency-button {
border-color: rgba(0, 0, 0, 0.2);
color: #333333;
&:hover {
background: rgba(0, 0, 0, 0.05);
border-color: rgba(0, 0, 0, 0.3);
}
}
}
:host-context(.light-theme) {
.currency-dropdown {
background: #ffffff;
border-color: rgba(0, 0, 0, 0.1);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.currency-option {
color: #333333;
&:hover { background: rgba(0, 0, 0, 0.05); }
&.active { background: rgba(0, 0, 0, 0.1); }
}
}
:host-context(.dexar-header),
:host-context(.dexar-mobile-menu) {
.currency-button {
padding: 4px 8px;
gap: 3px;
background: rgba(255, 255, 255, 0.3);
border: 1px solid #677b78;
border-radius: 8px;
color: #1e3c38;
&:hover {
background: rgba(255, 255, 255, 0.5);
}
.currency-symbol { font-size: 14px; color: #1e3c38; }
.currency-code { font-size: 13px; color: #1e3c38; }
.dropdown-arrow path { stroke: #1e3c38; }
}
.currency-dropdown {
background: #ffffff;
border-color: #d3dad9;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.currency-option {
color: #1e3c38;
&:hover { background: rgba(161, 180, 181, 0.2); }
&.active { background: rgba(73, 118, 113, 0.1); }
}
}

View File

@@ -1,5 +1,5 @@
import { Component, HostListener, ElementRef, ChangeDetectionStrategy } from '@angular/core';
import { LanguageService, Language } from '../../services/language.service';
import { LanguageService, Language, Currency } from '../../services/language.service';
@Component({
selector: 'app-language-selector',
@@ -10,6 +10,7 @@ import { LanguageService, Language } from '../../services/language.service';
})
export class LanguageSelectorComponent {
dropdownOpen = false;
currencyOpen = false;
constructor(
public languageService: LanguageService,
@@ -18,6 +19,12 @@ export class LanguageSelectorComponent {
toggleDropdown(): void {
this.dropdownOpen = !this.dropdownOpen;
this.currencyOpen = false;
}
toggleCurrency(): void {
this.currencyOpen = !this.currencyOpen;
this.dropdownOpen = false;
}
selectLanguage(lang: Language): void {
@@ -27,14 +34,30 @@ export class LanguageSelectorComponent {
}
}
selectCurrency(currency: Currency): void {
this.languageService.setCurrency(currency.code);
this.currencyOpen = false;
}
closeDropdown(): void {
this.dropdownOpen = false;
this.currencyOpen = false;
}
onKeyDown(event: KeyboardEvent): void {
if (event.key === 'Escape') {
this.dropdownOpen = false;
} else if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this.toggleDropdown();
}
}
@HostListener('document:click', ['$event'])
onClickOutside(event: Event): void {
if (!this.elementRef.nativeElement.contains(event.target)) {
this.dropdownOpen = false;
this.currencyOpen = false;
}
}
}

View File

@@ -0,0 +1,19 @@
// Payment polling
export const PAYMENT_POLL_INTERVAL_MS = 5000;
export const PAYMENT_MAX_CHECKS = 36;
export const PAYMENT_TIMEOUT_CLOSE_MS = 3000;
export const PAYMENT_ERROR_CLOSE_MS = 4000;
export const LINK_COPIED_DURATION_MS = 2000;
// Infinite scroll
export const SCROLL_THRESHOLD_PX = 1200;
export const SCROLL_DEBOUNCE_MS = 100;
export const ITEMS_PER_PAGE = 50;
// Search
export const SEARCH_DEBOUNCE_MS = 300;
export const SEARCH_MIN_LENGTH = 3;
// Cache
export const CACHE_DURATION_MS = 5 * 60 * 1000;
export const CATEGORY_CACHE_DURATION_MS = 2 * 60 * 1000;

View File

@@ -102,6 +102,10 @@ export const en: Translations = {
emailNeedsAt: 'Email must contain @',
emailNeedsDomain: 'Email must contain a domain (.com, .ru, etc.)',
emailInvalid: 'Invalid email format',
loginRequired: 'Log in to checkout',
loginRequiredDesc: 'Please log in via Telegram to place your order',
loginWithTelegram: 'Log in with Telegram',
orScanQr: 'Or scan the QR code',
},
search: {
title: 'Product search',
@@ -134,6 +138,7 @@ export const en: Translations = {
emptyTitle: 'Oops! No subcategories yet',
emptyDesc: 'There are no subcategories in this section yet, but they will appear soon',
goHome: 'Go home',
itemsInCategory: 'Items in this category',
},
itemDetail: {
loading: 'Loading...',
@@ -148,6 +153,7 @@ export const en: Translations = {
mediumStock: 'Running low',
addToCart: 'Add to cart',
description: 'Description',
specifications: 'Specifications',
reviews: 'Reviews',
yourReview: 'Your review',
leaveReview: 'Leave a review',
@@ -169,6 +175,8 @@ export const en: Translations = {
yesterday: 'Yesterday',
daysAgo: 'd. ago',
weeksAgo: 'w. ago',
colour: 'Colour',
size: 'Size',
},
app: {
connecting: 'Connecting to server...',

View File

@@ -6,7 +6,7 @@ export const hy: Translations = {
search: 'Որոնում',
about: 'Մեր մասին',
contacts: 'Կապ',
searchPlaceholder: 'Որոնել...',
searchPlaceholder: 'Փնտրել...',
catalog: 'Կատալոգ',
},
footer: {
@@ -14,7 +14,7 @@ export const hy: Translations = {
company: 'Ընկերություն',
aboutUs: 'Մեր մասին',
contacts: 'Կապ',
requisites: ավերապայմաններ',
requisites: ճարային տվյալներ',
support: 'Աջակցություն',
faq: 'ՀՏՀ',
delivery: 'Առաքում',
@@ -35,133 +35,139 @@ export const hy: Translations = {
},
home: {
welcomeTo: 'Բարի գալուստ {{brand}}',
subtitle: 'Գտեք ամեն ինչ մեկ վայրում',
subtitle: 'Գտեք այն ամենը, ինչ պետք է՝ մեկ վայրում',
startSearch: 'Սկսել որոնումը',
loading: 'Կատեգորիաները բեռնվում են...',
errorTitle: 'Ինչ-որ բան սխալ է գնացել',
loading: 'Բեռնում ենք կատեգորիաները...',
errorTitle: 'Ինչ-որ բան սխալ գնաց',
retry: 'Փորձել կրկին',
categoriesTitle: 'Ապրանքների կատեգորիաներ',
categoriesSubtitle: 'Ընտրեք հետաքրքրող կատեգորիան',
categoriesSubtitle: 'Ընտրեք ձեզ հետաքրքիր կատեգորիան',
categoriesEmpty: 'Կատեգորիաները շուտով կհայտնվեն',
categoriesEmptyDesc: 'Մենք աշխատում ենք կատալոգի համալրման վրա',
dexarHeroTitle: 'Այստեղ դու կգտնես ամեն ինչ',
categoriesEmptyDesc: 'Մենք աշխատում ենք կատալոգի լրացման վրա',
dexarHeroTitle: 'Այստեղ կգտնես ամեն ինչ',
dexarHeroSubtitle: 'Հազարավոր ապրանքներ մեկ վայրում',
dexarHeroTagline: 'պարզ և հարմար',
goToCatalog: 'Անցնել կատալոգ',
goToCatalog: 'Գնալ կատալոգ',
findProduct: 'Գտնել ապրանք',
loadingDexar: 'Կատեգորիաները բեռնվում են...',
loadingDexar: 'Կատեգորիաների բեռնում...',
catalogTitle: 'Ապրանքների կատալոգ',
emptyCategoriesDexar: 'Կատեգորիաները դեռ չկան',
categoriesSoonDexar: 'Շուտով այստեղ կհայտնվեն ապրանքների կատեգորիաներ',
categoriesSoonDexar: 'Շուտով այստեղ կհայտնվեն կատեգորիաներ',
itemsCount: '{{count}} ապրանք',
},
cart: {
title: 'Զամբյուղ',
clear: 'Մաքրել',
empty: 'Զամբյուղը դատարկ է',
emptyDesc: 'Ավելացրեք ապրանքներ գնումները սկսելու համար',
goShopping: 'Անցնել գնումների',
emptyDesc: 'Ավելացրեք ապրանքներ՝ գնումները սկսելու համար',
goShopping: 'Գնալ գնումների',
total: 'Ընդամենը',
items: 'Ապրանքներ',
deliveryLabel: 'Առաքում',
toPay: 'Վճարման ենթակա',
agreeWith: 'Ես համաձայն եմ',
publicOffer: 'հանրային օֆերտային',
returnPolicy: 'վերադարձի քաղաքականությանը',
guaranteeTerms: 'երաշխիքային պայմաններին',
privacyPolicy: 'գաղտնիության քաղաքականությանը',
publicOffer: 'հանրային օֆերտայի',
returnPolicy: 'վերադարձի քաղաքականության',
guaranteeTerms: 'երաշխիքային պայմանների',
privacyPolicy: 'գաղտնիության քաղաքականության',
and: 'և',
checkout: 'Ձևակերպել պատվեր',
checkout: 'Ձևակերպել պատվերը',
close: 'Փակել',
creatingPayment: 'Վճարումը ստեղծվում է...',
waitFewSeconds: 'Սպասեք մի քանի վայրկյան',
scanQr: 'Սկանավորեք QR կոդը վճարման համար',
amountToPay: 'Վճարման գումարը՝',
creatingPayment: 'Վճարման ստեղծում...',
waitFewSeconds: 'Խնդրում ենք սպասել մի քանի վայրկյան',
scanQr: 'Սքանավորեք QR կոդը վճարման համար',
amountToPay: 'Վճարման գումար՝',
waitingPayment: 'Սպասում ենք վճարմանը...',
copied: '✓ Պատճենված է',
copyLink: 'Պատճենել հղումը',
openNewTab: 'Բացել նոր ներդիրում',
paymentSuccess: 'Շնորհավորում ենք։ Վճարումը հաջողությամբ կատարվել է։',
paymentSuccessDesc: 'Մուտքագրեք ձեր կոնտակտային տվյալները, և մենք կուղարկենք գնումը մի քանի րոպեի ընթացքում',
paymentSuccess: 'Շնորհավորում ենք! Վճարումը հաջող է անցել!',
paymentSuccessDesc: 'Մուտքագրեք ձեր տվյալները, և մենք կուղարկենք գնումը մի քանի րոպեի ընթացքում',
sending: 'Ուղարկվում է...',
send: 'Ուղարկել',
paymentTimeout: 'Սպասման ժամանակը սպառվել է',
paymentTimeoutDesc: 'Մենք չենք ստացել վճարման հաստատում 3 րոպեի ընթացքում։',
paymentTimeout: 'Ժամանակը սպառվեց',
paymentTimeoutDesc: 'Մենք չստացանք վճարման հաստատում 3 րոպեի ընթացքում։',
autoClose: 'Պատուհանը կփակվի ավտոմատ...',
confirmClear: 'Համոզվա՞ծ եք, որ ցանկանում եք մաքրել զամբյուղը։',
acceptTerms: 'Խնդրում ենք ընդունել օֆերտայի, վերադարձի և երաշխիքի պայմանները պատվերը հաստատելու համար։',
confirmClear: 'Վստա՞հ եք, որ ցանկանում եք մաքրել զամբյուղը',
acceptTerms: 'Խնդրում ենք ընդունել պայմանները՝ պատվերը հաստատելու համար։',
copyError: 'Պատճենման սխալ՝',
emailSuccess: 'Email-ը հաջողությամբ ուղարկվել է։ Ստուգեք ձեր փոստը։',
emailError: 'Email ուղարկելու ժամանակ տեղի ունեցավ սխալ։ Խնդրում ենք փորձել կրկին։',
emailSuccess: 'Email-ը հաջողությամբ ուղարկվեց։ Ստուգեք ձեր փոստը։',
emailError: 'Սխալ email ուղարկելիս։ Խնդրում ենք փորձել կրկին։',
phoneRequired: 'Հեռախոսահամարը պարտադիր է',
phoneMoreDigits: 'Մուտքագրեք ևս {{count}} թիվ',
phoneTooMany: 'Չափազանց շատ թվեր',
emailRequired: 'Email-ը պարտադիր է',
emailTooShort: 'Email-ը չափազանց կարճ է (նվազագույնը 5 նիշ)',
emailTooShort: 'Email-ը չափազանց կարճ է (առնվազն 5 նիշ)',
emailTooLong: 'Email-ը չափազանց երկար է (առավելագույնը 100 նիշ)',
emailNeedsAt: 'Email-ը պետք է պարունակի @ նշանը',
emailNeedsDomain: 'Email-ը պետք է պարունակի դոմեն (.com, .ru և այլն)',
emailInvalid: 'Email ձևաչափը սխալ է',
emailNeedsAt: 'Email-ը պետք է պարունակի @',
emailNeedsDomain: 'Email-ը պետք է պարունակի դոմեյն (.com, .ru և այլն)',
emailInvalid: 'Սխալ email ձևաչափ',
loginRequired: 'Մուտք գործեք ձևակերպելու համար',
loginRequiredDesc: 'Պատվեր ձևակերպելու համար մուտք գործեք Telegram-ով',
loginWithTelegram: 'Մուտք Telegram-ով',
orScanQr: 'Կամ սքանավորեք QR կոդը',
},
search: {
title: 'Ապրանքների որոնում',
placeholder: 'Մուտքագրեք ապրանքի անունը...',
placeholder: 'Մուտքագրեք ապրանքի անվանումը...',
resultsCount: 'Գտնված ապրանքներ՝',
searching: 'Որոնում...',
retry: 'Փորձել կրկին',
noResults: 'Ոչինչ չի գտնվել',
noResultsFor: '"{{query}}" հարցման համար ապրանքներ չեն գտնվել',
noResultsFor: '"{{query}}" հարցմամբ ապրանքներ չեն գտնվել',
noResultsHint: 'Փորձեք փոխել հարցումը կամ օգտագործել այլ բանալի բառեր',
addToCart: 'Ավելացնել զամբյուղ',
loadingMore: 'Բեռնվում է...',
loadingMore: 'Բեռնում...',
allLoaded: 'Բոլոր արդյունքները բեռնված են',
emptyState: 'Մուտքագրեք հարցում ապրանքներ որոնելու համար',
of: 'ից',
emptyState: 'Մուտքագրեք հարցում որոնման համար',
of: '-ից',
},
category: {
retry: 'Փորձել կրկին',
addToCart: 'Ավելացնել զամբյուղ',
loadingMore: 'Բեռնվում է...',
loadingMore: 'Բեռնում...',
allLoaded: 'Բոլոր ապրանքները բեռնված են',
emptyTitle: 'Ուպս։ Այստեղ դեռ դատարկ է',
emptyDesc: 'Այս կատեգորիայում դեռ ապրանքներ չկան, բայց շուտով կհայտնվեն',
goHome: 'Գլխավոր էջ',
loading: 'Ապրանքները բեռնվում են...',
emptyTitle: 'Վա՜յ, այստեղ դեռ դատարկ է',
emptyDesc: 'Այս կատեգորիայում դեռ ապրանքներ չկան',
goHome: 'Գլխավոր',
loading: 'Ապրանքների բեռնում...',
},
subcategories: {
loading: 'Ենթակատեգորիաները բեռնվում են...',
loading: 'Ենթակատեգորիաների բեռնում...',
retry: 'Փորձել կրկին',
emptyTitle: 'Ուպս։ Ենթակատեգորիաներ դեռ չկան',
emptyDesc: 'Այս բաժնում դեռ ենթակատեգորիաներ չկան, բայց շուտով կհայտնվեն',
goHome: 'Գլխավոր էջ',
emptyTitle: 'Ենթակատեգորիաներ չկան',
emptyDesc: 'Այս բաժնում դեռ ենթակատեգորիաներ չկան',
goHome: 'Գլխավոր',
itemsInCategory: 'Ապրանքներ այս կատեգորիայում',
},
itemDetail: {
loading: 'Բեռնվում է...',
loadingDexar: 'Ապրանքը բեռնվում է...',
loading: 'Բեռնում...',
loadingDexar: 'Ապրանքի բեռնում...',
back: 'Վերադառնալ',
backHome: 'Վերադառնալ գլխավոր էջ',
noImage: 'Պատկեր չկա',
stock: 'Առկայություն՝',
inStock: 'Առկա է',
lowStock: 'Մնացել է քիչ',
lowStock: 'Քիչ է մնացել',
lastItems: 'Վերջին հատերը',
mediumStock: 'Վերջանում է',
mediumStock: 'Ավարտվում է',
addToCart: 'Ավելացնել զամբյուղ',
description: 'Նկարագրություն',
specifications: 'Բնութագրեր',
reviews: 'Կարծիքներ',
yourReview: 'Ձեր կարծիքը',
leaveReview: 'Թողնել կարծիք',
rating: 'Գնահատական՝',
reviewPlaceholder: 'Կիսվեք ձեր տպավորություններով ապրանքի մասին...',
reviewPlaceholderDexar: 'Կիսվեք ձեր տպավորություններով...',
reviewPlaceholder: 'Կիսվեք ձեր կարծիքով...',
reviewPlaceholderDexar: 'Կիսվեք տպավորություններով...',
anonymous: 'Անանուն',
submitting: 'Ուղարկվում է...',
submit: 'Ուղարկել',
reviewSuccess: 'Շնորհակալություն ձեր կարծիքի համար։',
reviewError: 'Ուղարկման սխալ։ Փորձեք ավելի ուշ։',
reviewSuccess: 'Շնորհակալություն ձեր կարծիքի համար!',
reviewError: 'Սխալ ուղարկելիս։ Փորձեք ավելի ուշ։',
defaultUser: 'Օգտատեր',
defaultUserDexar: 'Անանուն',
noReviews: 'Դեռ կարծիքներ չկան։ Դարձեք առաջինը։',
noReviews: 'Կարծիքներ դեռ չկան',
qna: 'Հարցեր և պատասխաններ',
photo: 'Լուսանկար',
reviewsCount: 'կարծիք',
@@ -169,34 +175,35 @@ export const hy: Translations = {
yesterday: 'Երեկ',
daysAgo: 'օր առաջ',
weeksAgo: 'շաբաթ առաջ',
colour: 'Գույն',
size: 'Չափ',
},
app: {
connecting: 'Միացում սերվերին...',
serverUnavailable: 'Սերվերը հասանելի չէ',
serverError: 'Չհաջողվեց միանալ սերվերին։ Ստուգեք ինտերնետ կապը։',
retryConnection: 'Կրկնել փորձը',
connecting: 'Կապ սերվերի հետ...',
serverUnavailable: 'Սերվերը անհասանելի է',
serverError: 'Չհաջողվեց միանալ սերվերին։ Ստուգեք ինտերնետը։',
retryConnection: 'Փորձել կրկին',
pageTitle: 'Ապրանքների և ծառայությունների մարքեթփլեյս',
},
carousel: {
loading: 'Ապրանքները բեռնվում են...',
loading: 'Ապրանքների բեռնում...',
addToCart: 'Ավելացնել զամբյուղ',
},
common: {
retry: 'Փորձել կրկին',
loading: 'Բեռնվում է...',
loading: 'Բեռնում...',
},
location: {
allRegions: 'Բոլոր տարածաշրջաններ',
chooseRegion: 'Ընտրեք տարածաշրջան',
detectAuto: 'Որոշել ինքնաշխատ',
allRegions: 'Բոլոր տարածաշրջանները',
chooseRegion: 'Ընտրեք տարածաշրջանը',
detectAuto: 'Որոշել ավտոմատ',
},
auth: {
loginRequired: 'Մուտք պահանջվում է',
loginDescription: 'Պատվերի կատարման համար մուտք արեք Telegram-ի միջոցով',
checking: 'Ստուգում է...',
loginWithTelegram: 'Մուտք գործել Telegram-ով',
orScanQr: 'Կամ սկանավորեք QR կոդը',
loginNote: 'Մուտքից հետո դուք կվերադառնավեք',
loginRequired: 'Պահանջվում է մուտք',
loginDescription: 'Պատվերի համար մուտք գործեք Telegram-ով',
checking: 'Ստուգում...',
loginWithTelegram: 'Մուտք Telegram-ով',
orScanQr: 'Կամ սքանավորեք QR կոդը',
loginNote: 'Մուտքից հետո դուք կվերաուղղվեք',
},
};

View File

@@ -102,6 +102,10 @@ export const ru: Translations = {
emailNeedsAt: 'Email должен содержать @',
emailNeedsDomain: 'Email должен содержать домен (.com, .ru и т.д.)',
emailInvalid: 'Некорректный формат email',
loginRequired: 'Войдите для оформления',
loginRequiredDesc: 'Для оформления заказа войдите через Telegram',
loginWithTelegram: 'Войти через Telegram',
orScanQr: 'Или отсканируйте QR-код',
},
search: {
title: 'Поиск товаров',
@@ -134,6 +138,7 @@ export const ru: Translations = {
emptyTitle: 'Упс! Подкатегорий пока нет',
emptyDesc: 'В этом разделе ещё нет подкатегорий, но скоро они появятся',
goHome: 'На главную',
itemsInCategory: 'Товары в этой категории',
},
itemDetail: {
loading: 'Загрузка...',
@@ -148,6 +153,7 @@ export const ru: Translations = {
mediumStock: 'Заканчивается',
addToCart: 'Добавить в корзину',
description: 'Описание',
specifications: 'Характеристики',
reviews: 'Отзывы',
yourReview: 'Ваш отзыв',
leaveReview: 'Оставить отзыв',
@@ -169,6 +175,8 @@ export const ru: Translations = {
yesterday: 'Вчера',
daysAgo: 'дн. назад',
weeksAgo: 'нед. назад',
colour: 'Цвет',
size: 'Размер',
},
app: {
connecting: 'Подключение к серверу...',

View File

@@ -100,6 +100,10 @@ export interface Translations {
emailNeedsAt: string;
emailNeedsDomain: string;
emailInvalid: string;
loginRequired: string;
loginRequiredDesc: string;
loginWithTelegram: string;
orScanQr: string;
};
search: {
title: string;
@@ -132,6 +136,7 @@ export interface Translations {
emptyTitle: string;
emptyDesc: string;
goHome: string;
itemsInCategory: string;
};
itemDetail: {
loading: string;
@@ -146,6 +151,7 @@ export interface Translations {
mediumStock: string;
addToCart: string;
description: string;
specifications: string;
reviews: string;
yourReview: string;
leaveReview: string;
@@ -167,6 +173,8 @@ export interface Translations {
yesterday: string;
daysAgo: string;
weeksAgo: string;
colour: string;
size: string;
};
app: {
connecting: string;

View File

@@ -0,0 +1,50 @@
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { LocationService } from '../services/location.service';
import { LanguageService } from '../services/language.service';
import { AuthService } from '../services/auth.service';
import { environment } from '../../environments/environment';
/** Map internal language codes to API header values */
const LANG_HEADER_MAP: Record<string, string> = {
'ru': 'RU',
'en': 'EN',
'hy': 'AM',
};
/** Map region IDs to API header values */
const REGION_HEADER_MAP: Record<string, string> = {
'moscow': 'Moscow',
'spb': 'ST. Petersburg',
'yerevan': 'Yerevan',
};
export const apiHeadersInterceptor: HttpInterceptorFn = (req, next) => {
if (!req.url.startsWith(environment.apiUrl)) {
return next(req);
}
const locationService = inject(LocationService);
const languageService = inject(LanguageService);
const authService = inject(AuthService);
const regionId = locationService.regionId();
const lang = languageService.currentLanguage();
const currency = languageService.currentCurrency();
const session = authService.session();
let headers = req.headers;
if (regionId) {
headers = headers.set('X-Region', REGION_HEADER_MAP[regionId] ?? regionId);
}
if (lang) {
headers = headers.set('X-Language', LANG_HEADER_MAP[lang] ?? lang.toUpperCase());
}
headers = headers.set('Currency', currency || 'RUB');
if (session?.sessionId) {
headers = headers.set('WebSessionID', session.sessionId);
}
return next(req.clone({ headers }));
};

View File

@@ -2,8 +2,9 @@ import { HttpInterceptorFn, HttpResponse } from '@angular/common/http';
import { of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { CACHE_DURATION_MS, CATEGORY_CACHE_DURATION_MS } from '../config/constants';
const cache = new Map<string, { response: HttpResponse<unknown>, timestamp: number }>();
const CACHE_DURATION = 5 * 60 * 1000; // 5 минут
export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
// Кэшируем только GET запросы
@@ -11,12 +12,16 @@ export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
return next(req);
}
// Кэшируем только запросы списка категорий (не товары категорий)
const shouldCache = req.url.match(/\/category$/) !== null;
if (!shouldCache) {
// Кэшируем списки категорий, товары категорий и отдельные товары
const isCategoryList = /\/category$/.test(req.url);
const isCategoryItems = /\/category\/\d+/.test(req.url);
const isItem = /\/item\/\d+/.test(req.url);
if (!isCategoryList && !isCategoryItems && !isItem) {
return next(req);
}
const ttl = isCategoryList ? CACHE_DURATION_MS : CATEGORY_CACHE_DURATION_MS;
// Cleanup expired entries before checking
cleanupExpiredCache();
@@ -25,7 +30,7 @@ export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
// Проверяем наличие и актуальность кэша
if (cachedResponse) {
const age = Date.now() - cachedResponse.timestamp;
if (age < CACHE_DURATION) {
if (age < ttl) {
return of(cachedResponse.response.clone());
} else {
cache.delete(req.url);
@@ -53,7 +58,7 @@ export function clearCache(): void {
function cleanupExpiredCache(): void {
const now = Date.now();
for (const [url, data] of cache.entries()) {
if (now - data.timestamp >= CACHE_DURATION) {
if (now - data.timestamp >= CACHE_DURATION_MS) {
cache.delete(url);
}
}

View File

@@ -0,0 +1,795 @@
import { HttpInterceptorFn, HttpResponse } from '@angular/common/http';
import { of, delay } from 'rxjs';
import { environment } from '../../environments/environment';
// ─── Mock Categories (backOffice format: string IDs, img, subcategories, visible) ───
const MOCK_CATEGORIES = [
{
id: 'electronics',
categoryID: 1,
name: 'Электроника',
parentID: 0,
visible: true,
priority: 1,
img: 'https://images.unsplash.com/photo-1498049794561-7780e7231661?w=400&h=300&fit=crop',
icon: 'https://images.unsplash.com/photo-1498049794561-7780e7231661?w=400&h=300&fit=crop',
projectId: 'dexar',
itemCount: 15,
subcategories: [
{
id: 'smartphones',
name: 'Смартфоны',
visible: true,
priority: 1,
img: 'https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?w=400&h=300&fit=crop',
categoryId: 'electronics',
parentId: 'electronics',
itemCount: 8,
hasItems: true,
subcategories: []
},
{
id: 'laptops',
name: 'Ноутбуки',
visible: true,
priority: 2,
img: 'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?w=400&h=300&fit=crop',
categoryId: 'electronics',
parentId: 'electronics',
itemCount: 6,
hasItems: true,
subcategories: []
}
]
},
{
id: 'clothing',
categoryID: 2,
name: 'Одежда',
parentID: 0,
visible: true,
priority: 2,
img: 'https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=400&h=300&fit=crop',
icon: 'https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=400&h=300&fit=crop',
projectId: 'dexar',
itemCount: 25,
subcategories: [
{
id: 'mens',
name: 'Мужская',
visible: true,
priority: 1,
img: 'https://images.unsplash.com/photo-1490578474895-699cd4e2cf59?w=400&h=300&fit=crop',
categoryId: 'clothing',
parentId: 'clothing',
itemCount: 12,
hasItems: true,
subcategories: []
},
{
id: 'womens',
name: 'Женская',
visible: true,
priority: 2,
img: 'https://images.unsplash.com/photo-1487222477894-8943e31ef7b2?w=400&h=300&fit=crop',
categoryId: 'clothing',
parentId: 'clothing',
itemCount: 13,
hasItems: true,
subcategories: []
}
]
},
{
id: 'home',
categoryID: 3,
name: 'Дом и сад',
parentID: 0,
visible: true,
priority: 3,
img: 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=400&h=300&fit=crop',
icon: 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=400&h=300&fit=crop',
projectId: 'dexar',
itemCount: 8,
subcategories: []
},
// Subcategories as flat entries (for the legacy flat category list)
{
id: 'smartphones',
categoryID: 11,
name: 'Смартфоны',
parentID: 1,
visible: true,
priority: 1,
img: 'https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?w=400&h=300&fit=crop',
icon: 'https://images.unsplash.com/photo-1511707171634-5f897ff02aa9?w=400&h=300&fit=crop',
itemCount: 8
},
{
id: 'laptops',
categoryID: 12,
name: 'Ноутбуки',
parentID: 1,
visible: true,
priority: 2,
img: 'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?w=400&h=300&fit=crop',
icon: 'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?w=400&h=300&fit=crop',
itemCount: 6
},
{
id: 'mens',
categoryID: 21,
name: 'Мужская одежда',
parentID: 2,
visible: true,
priority: 1,
img: 'https://images.unsplash.com/photo-1490578474895-699cd4e2cf59?w=400&h=300&fit=crop',
icon: 'https://images.unsplash.com/photo-1490578474895-699cd4e2cf59?w=400&h=300&fit=crop',
itemCount: 12
},
{
id: 'womens',
categoryID: 22,
name: 'Женская одежда',
parentID: 2,
visible: true,
priority: 2,
img: 'https://images.unsplash.com/photo-1487222477894-8943e31ef7b2?w=400&h=300&fit=crop',
icon: 'https://images.unsplash.com/photo-1487222477894-8943e31ef7b2?w=400&h=300&fit=crop',
itemCount: 13
}
];
// ─── Mock Items (backOffice format with ALL fields) ───
const MOCK_ITEMS: any[] = [
{
id: 'iphone15',
itemID: 101,
name: 'iPhone 15 Pro Max',
visible: true,
priority: 1,
quantity: 50,
price: 149990,
discount: 0,
currency: 'RUB',
rating: 4.8,
remainings: 'high',
categoryID: 11,
imgs: [
'https://images.unsplash.com/photo-1695048133142-1a20484d2569?w=600&h=400&fit=crop',
'https://images.unsplash.com/photo-1592750475338-74b7b21085ab?w=600&h=400&fit=crop'
],
photos: [
{ url: 'https://images.unsplash.com/photo-1695048133142-1a20484d2569?w=600&h=400&fit=crop' },
{ url: 'https://images.unsplash.com/photo-1592750475338-74b7b21085ab?w=600&h=400&fit=crop' }
],
tags: ['new', 'featured', 'apple'],
badges: ['new', 'bestseller'],
colour: 'Натуральный титан',
size: '',
names: [
{ language: 'ru', value: 'iPhone 15 Pro Max' },
{ language: 'en', value: 'iPhone 15 Pro Max' },
{ language: 'hy', value: 'iPhone 15 Pro Max' }
],
descriptions: [
{ language: 'ru', value: 'Новейший iPhone с титановым корпусом и чипом A17 Pro' },
{ language: 'en', value: 'Latest iPhone with titanium body and A17 Pro chip' }
],
attributes: [
{ key: 'Цвет', value: 'Натуральный титан' },
{ key: 'Память', value: '256 ГБ' },
{ key: 'Процессор', value: 'A17 Pro' }
],
simpleDescription: 'Новейший iPhone с титановым корпусом и чипом A17 Pro',
description: [
{ key: 'Цвет', value: 'Натуральный титан' },
{ key: 'Память', value: '256 ГБ' },
{ key: 'Дисплей', value: '6.7" Super Retina XDR' },
{ key: 'Процессор', value: 'A17 Pro' },
{ key: 'Камера', value: '48 Мп основная' },
{ key: 'Аккумулятор', value: '4441 мАч' }
],
descriptionFields: [
{ key: 'Цвет', value: 'Натуральный титан' },
{ key: 'Память', value: '256 ГБ' },
{ key: 'Дисплей', value: '6.7" Super Retina XDR' },
{ key: 'Процессор', value: 'A17 Pro' },
{ key: 'Камера', value: '48 Мп основная' },
{ key: 'Аккумулятор', value: '4441 мАч' }
],
subcategoryId: 'smartphones',
translations: {
en: {
name: 'iPhone 15 Pro Max',
simpleDescription: 'Latest iPhone with titanium body and A17 Pro chip',
description: [
{ key: 'Color', value: 'Natural Titanium' },
{ key: 'Storage', value: '256GB' },
{ key: 'Display', value: '6.7" Super Retina XDR' },
{ key: 'Chip', value: 'A17 Pro' },
{ key: 'Camera', value: '48MP main' },
{ key: 'Battery', value: '4441 mAh' }
]
}
},
comments: [
{ id: 'c1', text: 'Отличный телефон! Камера просто огонь 🔥', author: 'Иван Петров', stars: 5, createdAt: '2025-12-15T10:30:00Z' },
{ id: 'c2', text: 'Батарея держит весь день, очень доволен.', author: 'Мария Козлова', stars: 4, createdAt: '2026-01-05T14:20:00Z' }
],
callbacks: [
{ rating: 5, content: 'Отличный телефон! Камера просто огонь 🔥', userID: 'Иван Петров', timestamp: '2025-12-15T10:30:00Z' },
{ rating: 4, content: 'Батарея держит весь день, очень доволен.', userID: 'Мария Козлова', timestamp: '2026-01-05T14:20:00Z' }
],
questions: []
},
{
id: 'samsung-s24',
itemID: 102,
name: 'Samsung Galaxy S24 Ultra',
visible: true,
priority: 2,
quantity: 35,
price: 129990,
discount: 10,
currency: 'RUB',
rating: 4.6,
remainings: 'high',
categoryID: 11,
imgs: [
'https://images.unsplash.com/photo-1610945415295-d9bbf067e59c?w=600&h=400&fit=crop'
],
photos: [
{ url: 'https://images.unsplash.com/photo-1610945415295-d9bbf067e59c?w=600&h=400&fit=crop' }
],
tags: ['new', 'android', 'samsung'],
badges: ['new', 'sale'],
colour: 'Титановый серый',
size: '',
names: [
{ language: 'ru', value: 'Samsung Galaxy S24 Ultra' },
{ language: 'en', value: 'Samsung Galaxy S24 Ultra' }
],
descriptions: [
{ language: 'ru', value: 'Премиальный флагман Samsung с S Pen' },
{ language: 'en', value: 'Premium Samsung flagship with S Pen' }
],
attributes: [
{ key: 'Память', value: '512 ГБ' },
{ key: 'ОЗУ', value: '12 ГБ' }
],
simpleDescription: 'Премиальный флагман Samsung с S Pen',
description: [
{ key: 'Цвет', value: 'Титановый серый' },
{ key: 'Память', value: '512 ГБ' },
{ key: 'ОЗУ', value: '12 ГБ' },
{ key: 'Дисплей', value: '6.8" Dynamic AMOLED 2X' }
],
descriptionFields: [
{ key: 'Цвет', value: 'Титановый серый' },
{ key: 'Память', value: '512 ГБ' },
{ key: 'ОЗУ', value: '12 ГБ' },
{ key: 'Дисплей', value: '6.8" Dynamic AMOLED 2X' }
],
subcategoryId: 'smartphones',
translations: {
en: {
name: 'Samsung Galaxy S24 Ultra',
simpleDescription: 'Premium Samsung flagship with S Pen',
description: [
{ key: 'Color', value: 'Titanium Gray' },
{ key: 'Storage', value: '512GB' },
{ key: 'RAM', value: '12GB' },
{ key: 'Display', value: '6.8" Dynamic AMOLED 2X' }
]
}
},
comments: [
{ id: 'c3', text: 'S Pen — топ, использую каждый день.', author: 'Алексей', stars: 5, createdAt: '2026-01-20T08:10:00Z' }
],
callbacks: [
{ rating: 5, content: 'S Pen — топ, использую каждый день.', userID: 'Алексей', timestamp: '2026-01-20T08:10:00Z' }
],
questions: []
},
{
id: 'pixel-8',
itemID: 103,
name: 'Google Pixel 8 Pro',
visible: true,
priority: 3,
quantity: 20,
price: 89990,
discount: 15,
currency: 'RUB',
rating: 4.5,
remainings: 'medium',
categoryID: 11,
imgs: [
'https://images.unsplash.com/photo-1598327105666-5b89351aff97?w=600&h=400&fit=crop'
],
photos: [
{ url: 'https://images.unsplash.com/photo-1598327105666-5b89351aff97?w=600&h=400&fit=crop' }
],
tags: ['sale', 'android', 'ai', 'google'],
badges: ['sale', 'hot'],
simpleDescription: 'Лучший смартфон для ИИ-фотографии',
description: [
{ key: 'Цвет', value: 'Bay Blue' },
{ key: 'Память', value: '256 ГБ' },
{ key: 'Процессор', value: 'Tensor G3' }
],
descriptionFields: [
{ key: 'Цвет', value: 'Bay Blue' },
{ key: 'Память', value: '256 ГБ' },
{ key: 'Процессор', value: 'Tensor G3' }
],
subcategoryId: 'smartphones',
translations: {},
comments: [],
callbacks: [],
questions: [
{ question: 'Поддерживает eSIM?', answer: 'Да, поддерживает dual eSIM.', upvotes: 12, downvotes: 0 }
]
},
{
id: 'macbook-pro',
itemID: 104,
name: 'MacBook Pro 16" M3 Max',
visible: true,
priority: 1,
quantity: 15,
price: 299990,
discount: 0,
currency: 'RUB',
rating: 4.9,
remainings: 'low',
categoryID: 12,
imgs: [
'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?w=600&h=400&fit=crop',
'https://images.unsplash.com/photo-1541807084-5c52b6b3adef?w=600&h=400&fit=crop'
],
photos: [
{ url: 'https://images.unsplash.com/photo-1517336714731-489689fd1ca8?w=600&h=400&fit=crop' },
{ url: 'https://images.unsplash.com/photo-1541807084-5c52b6b3adef?w=600&h=400&fit=crop' }
],
tags: ['featured', 'professional', 'apple'],
badges: ['exclusive', 'limited'],
simpleDescription: 'Мощный ноутбук для профессионалов',
description: [
{ key: 'Процессор', value: 'Apple M3 Max' },
{ key: 'ОЗУ', value: '36 ГБ' },
{ key: 'Память', value: '1 ТБ SSD' },
{ key: 'Дисплей', value: '16.2" Liquid Retina XDR' },
{ key: 'Батарея', value: 'До 22 ч' }
],
descriptionFields: [
{ key: 'Процессор', value: 'Apple M3 Max' },
{ key: 'ОЗУ', value: '36 ГБ' },
{ key: 'Память', value: '1 ТБ SSD' },
{ key: 'Дисплей', value: '16.2" Liquid Retina XDR' },
{ key: 'Батарея', value: 'До 22 ч' }
],
subcategoryId: 'laptops',
translations: {
en: {
name: 'MacBook Pro 16" M3 Max',
simpleDescription: 'Powerful laptop for professionals',
description: [
{ key: 'Chip', value: 'Apple M3 Max' },
{ key: 'RAM', value: '36GB' },
{ key: 'Storage', value: '1TB SSD' },
{ key: 'Display', value: '16.2" Liquid Retina XDR' },
{ key: 'Battery', value: 'Up to 22h' }
]
}
},
comments: [
{ id: 'c4', text: 'Невероятная производительность. Рендер в 3 раза быстрее.', author: 'Дизайнер Про', stars: 5, createdAt: '2025-11-15T12:00:00Z' },
{ id: 'c5', text: 'Стоит каждого рубля. Экран — сказка.', author: 'Видеоредактор', stars: 5, createdAt: '2026-02-01T09:00:00Z' }
],
callbacks: [
{ rating: 5, content: 'Невероятная производительность. Рендер в 3 раза быстрее.', userID: 'Дизайнер Про', timestamp: '2025-11-15T12:00:00Z' },
{ rating: 5, content: 'Стоит каждого рубля. Экран — сказка.', userID: 'Видеоредактор', timestamp: '2026-02-01T09:00:00Z' }
],
questions: []
},
{
id: 'dell-xps',
itemID: 105,
name: 'Dell XPS 15',
visible: true,
priority: 2,
quantity: 3,
price: 179990,
discount: 5,
currency: 'RUB',
rating: 4.3,
remainings: 'low',
categoryID: 12,
imgs: [
'https://images.unsplash.com/photo-1593642702749-b7d2a804c22e?w=600&h=400&fit=crop'
],
photos: [
{ url: 'https://images.unsplash.com/photo-1593642702749-b7d2a804c22e?w=600&h=400&fit=crop' }
],
tags: ['windows', 'professional'],
badges: ['limited'],
simpleDescription: 'Тонкий и мощный Windows ноутбук',
description: [
{ key: 'Процессор', value: 'Intel Core i9-13900H' },
{ key: 'ОЗУ', value: '32 ГБ' },
{ key: 'Дисплей', value: '15.6" OLED 3.5K' }
],
descriptionFields: [
{ key: 'Процессор', value: 'Intel Core i9-13900H' },
{ key: 'ОЗУ', value: '32 ГБ' },
{ key: 'Дисплей', value: '15.6" OLED 3.5K' }
],
subcategoryId: 'laptops',
translations: {},
comments: [],
callbacks: [],
questions: []
},
{
id: 'jacket-leather',
itemID: 201,
name: 'Кожаная куртка Premium',
visible: true,
priority: 1,
quantity: 8,
price: 34990,
discount: 20,
currency: 'RUB',
rating: 4.7,
remainings: 'medium',
categoryID: 21,
imgs: [
'https://images.unsplash.com/photo-1551028719-00167b16eac5?w=600&h=400&fit=crop'
],
photos: [
{ url: 'https://images.unsplash.com/photo-1551028719-00167b16eac5?w=600&h=400&fit=crop' }
],
tags: ['leather', 'premium', 'winter'],
badges: ['sale', 'bestseller'],
simpleDescription: 'Стильная мужская кожаная куртка из натуральной кожи',
description: [
{ key: 'Материал', value: 'Натуральная кожа' },
{ key: 'Размеры', value: 'S, M, L, XL, XXL' },
{ key: 'Цвет', value: 'Чёрный' },
{ key: 'Подкладка', value: 'Полиэстер 100%' }
],
descriptionFields: [
{ key: 'Материал', value: 'Натуральная кожа' },
{ key: 'Размеры', value: 'S, M, L, XL, XXL' },
{ key: 'Цвет', value: 'Чёрный' },
{ key: 'Подкладка', value: 'Полиэстер 100%' }
],
subcategoryId: 'mens',
translations: {
en: {
name: 'Premium Leather Jacket',
simpleDescription: 'Stylish men\'s genuine leather jacket',
description: [
{ key: 'Material', value: 'Genuine Leather' },
{ key: 'Sizes', value: 'S, M, L, XL, XXL' },
{ key: 'Color', value: 'Black' },
{ key: 'Lining', value: '100% Polyester' }
]
}
},
comments: [
{ id: 'c6', text: 'Качество кожи отличное, сидит идеально.', author: 'Антон', stars: 5, createdAt: '2026-01-10T16:30:00Z' }
],
callbacks: [
{ rating: 5, content: 'Качество кожи отличное, сидит идеально.', userID: 'Антон', timestamp: '2026-01-10T16:30:00Z' }
],
questions: []
},
{
id: 'dress-silk',
itemID: 202,
name: 'Шёлковое платье Elegance',
visible: true,
priority: 1,
quantity: 12,
price: 18990,
discount: 0,
currency: 'RUB',
rating: 4.9,
remainings: 'high',
categoryID: 22,
imgs: [
'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=600&h=400&fit=crop'
],
photos: [
{ url: 'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=600&h=400&fit=crop' }
],
tags: ['silk', 'elegant', 'new'],
badges: ['new', 'featured'],
simpleDescription: 'Элегантное шёлковое платье для особых случаев',
description: [
{ key: 'Материал', value: '100% Шёлк' },
{ key: 'Размеры', value: 'XS, S, M, L' },
{ key: 'Цвет', value: 'Бордовый' },
{ key: 'Длина', value: 'Миди' }
],
descriptionFields: [
{ key: 'Материал', value: '100% Шёлк' },
{ key: 'Размеры', value: 'XS, S, M, L' },
{ key: 'Цвет', value: 'Бордовый' },
{ key: 'Длина', value: 'Миди' }
],
subcategoryId: 'womens',
translations: {},
comments: [
{ id: 'c7', text: 'Восхитительное платье! Ткань потрясающая.', author: 'Елена', stars: 5, createdAt: '2026-02-14T20:00:00Z' },
{ id: 'c8', text: 'Идеально на вечер. Рекомендую!', author: 'Наталья', stars: 5, createdAt: '2026-02-10T11:00:00Z' }
],
callbacks: [
{ rating: 5, content: 'Восхитительное платье! Ткань потрясающая.', userID: 'Елена', timestamp: '2026-02-14T20:00:00Z' },
{ rating: 5, content: 'Идеально на вечер. Рекомендую!', userID: 'Наталья', timestamp: '2026-02-10T11:00:00Z' }
],
questions: []
},
{
id: 'hoodie-basic',
itemID: 203,
name: 'Худи Oversize Basic',
visible: true,
priority: 3,
quantity: 45,
price: 5990,
discount: 0,
currency: 'RUB',
rating: 4.2,
remainings: 'high',
categoryID: 21,
imgs: [
'https://images.unsplash.com/photo-1556821840-3a63f95609a7?w=600&h=400&fit=crop'
],
photos: [
{ url: 'https://images.unsplash.com/photo-1556821840-3a63f95609a7?w=600&h=400&fit=crop' }
],
tags: ['casual', 'basic'],
badges: [],
simpleDescription: 'Удобное худи свободного кроя на каждый день',
description: [
{ key: 'Материал', value: 'Хлопок 80%, Полиэстер 20%' },
{ key: 'Размеры', value: 'S, M, L, XL' },
{ key: 'Цвет', value: 'Серый меланж' }
],
descriptionFields: [
{ key: 'Материал', value: 'Хлопок 80%, Полиэстер 20%' },
{ key: 'Размеры', value: 'S, M, L, XL' },
{ key: 'Цвет', value: 'Серый меланж' }
],
subcategoryId: 'mens',
translations: {},
comments: [],
callbacks: [],
questions: []
},
{
id: 'sneakers-run',
itemID: 204,
name: 'Кроссовки AirPulse Run',
visible: true,
priority: 2,
quantity: 0,
price: 12990,
discount: 30,
currency: 'RUB',
rating: 4.4,
remainings: 'out',
categoryID: 21,
imgs: [
'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=600&h=400&fit=crop'
],
photos: [
{ url: 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=600&h=400&fit=crop' }
],
tags: ['sport', 'running'],
badges: ['sale', 'hot'],
simpleDescription: 'Лёгкие беговые кроссовки с пенной амортизацией',
description: [
{ key: 'Верх', value: 'Текстильная сетка' },
{ key: 'Подошва', value: 'Пена EVA' },
{ key: 'Вес', value: '260 г' }
],
descriptionFields: [
{ key: 'Верх', value: 'Текстильная сетка' },
{ key: 'Подошва', value: 'Пена EVA' },
{ key: 'Вес', value: '260 г' }
],
subcategoryId: 'mens',
translations: {},
comments: [
{ id: 'c9', text: 'Нет в наличии уже месяц... Верните!', author: егун42', stars: 3, createdAt: '2026-02-05T07:00:00Z' }
],
callbacks: [
{ rating: 3, content: 'Нет в наличии уже месяц... Верните!', userID: егун42', timestamp: '2026-02-05T07:00:00Z' }
],
questions: []
},
{
id: 'lamp-smart',
itemID: 301,
name: 'Умная лампа Homelight Pro',
visible: true,
priority: 1,
quantity: 100,
price: 3990,
discount: 0,
currency: 'RUB',
rating: 4.1,
remainings: 'high',
categoryID: 3,
imgs: [
'https://images.unsplash.com/photo-1507473885765-e6ed057ab6fe?w=600&h=400&fit=crop'
],
photos: [
{ url: 'https://images.unsplash.com/photo-1507473885765-e6ed057ab6fe?w=600&h=400&fit=crop' }
],
tags: ['smart-home', 'lighting'],
badges: ['featured'],
simpleDescription: 'Wi-Fi лампа с управлением через приложение и голосом',
description: [
{ key: 'Яркость', value: '1100 лм' },
{ key: 'Цветовая t°', value: '2700K6500K' },
{ key: 'Совместимость', value: 'Алиса, Google Home, Alexa' }
],
descriptionFields: [
{ key: 'Яркость', value: '1100 лм' },
{ key: 'Цветовая t°', value: '2700K6500K' },
{ key: 'Совместимость', value: 'Алиса, Google Home, Alexa' }
],
subcategoryId: 'home',
translations: {},
comments: [],
callbacks: [],
questions: []
}
];
// ─── Helper ───
function getAllVisibleItems(): any[] {
return MOCK_ITEMS.filter(i => i.visible !== false);
}
function getItemsByCategoryId(categoryID: number): any[] {
return getAllVisibleItems().filter(i => i.categoryID === categoryID);
}
function respond<T>(body: T, delayMs = 150) {
return of(new HttpResponse({ status: 200, body })).pipe(delay(delayMs));
}
// ─── The Interceptor ───
export const mockDataInterceptor: HttpInterceptorFn = (req, next) => {
if (!(environment as any).useMockData) {
return next(req);
}
const url = req.url;
// ── GET /ping
if (url.endsWith('/ping') && req.method === 'GET') {
return respond({ message: 'pong (mock)' });
}
// ── GET /category (all categories flat list)
if (url.endsWith('/category') && req.method === 'GET') {
return respond(MOCK_CATEGORIES);
}
// ── GET /category/:id (items for a category)
const catItemsMatch = url.match(/\/category\/(\d+)$/);
if (catItemsMatch && req.method === 'GET') {
const catId = parseInt(catItemsMatch[1], 10);
const items = getItemsByCategoryId(catId);
return respond(items);
}
// ── GET /item/:id
const itemMatch = url.match(/\/item\/(\d+)$/);
if (itemMatch && req.method === 'GET') {
const itemId = parseInt(itemMatch[1], 10);
const item = MOCK_ITEMS.find(i => i.itemID === itemId);
if (item) {
return respond(item);
}
return of(new HttpResponse({ status: 404, body: { error: 'Item not found' } })).pipe(delay(100));
}
// ── GET /searchitems?search=...
if (url.includes('/searchitems') && req.method === 'GET') {
const search = req.params.get('search')?.toLowerCase() || '';
const items = getAllVisibleItems().filter(i =>
i.name.toLowerCase().includes(search) ||
i.simpleDescription?.toLowerCase().includes(search) ||
i.tags?.some((t: string) => t.toLowerCase().includes(search))
);
return respond({
items,
total: items.length,
count: items.length,
skip: 0
});
}
// ── GET /randomitems
if (url.includes('/randomitems') && req.method === 'GET') {
const count = parseInt(req.params.get('count') || '5', 10);
const shuffled = [...getAllVisibleItems()].sort(() => Math.random() - 0.5);
return respond(shuffled.slice(0, count));
}
// ── GET /cart (return empty)
if (url.endsWith('/cart') && req.method === 'GET') {
return respond([]);
}
// ── POST /cart (add to cart / create payment)
if (url.endsWith('/cart') && req.method === 'POST') {
const body = req.body as any;
if (body?.amount) {
// Payment mock
return respond({
qrId: 'mock-qr-' + Date.now(),
qrStatus: 'CREATED',
qrExpirationDate: new Date(Date.now() + 180000).toISOString(),
payload: 'https://example.com/pay/mock',
qrUrl: 'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=mock-payment'
}, 300);
}
return respond({ message: 'Added (mock)' });
}
// ── PATCH /cart
if (url.endsWith('/cart') && req.method === 'PATCH') {
return respond({ message: 'Updated (mock)' });
}
// ── DELETE /cart
if (url.endsWith('/cart') && req.method === 'DELETE') {
return respond({ message: 'Removed (mock)' });
}
// ── POST /comment
if (url.endsWith('/comment') && req.method === 'POST') {
return respond({ message: 'Review submitted (mock)' }, 200);
}
// ── POST /purchase-email
if (url.endsWith('/purchase-email') && req.method === 'POST') {
return respond({ message: 'Email sent (mock)' }, 200);
}
// ── GET /qr/payment/:id (always return success for testing)
if (url.includes('/qr/payment/') && req.method === 'GET') {
return respond({
paymentStatus: 'SUCCESS',
code: 'SUCCESS',
amount: 0,
currency: 'RUB',
qrId: 'mock',
transactionId: 999,
transactionDate: new Date().toISOString(),
additionalInfo: '',
paymentPurpose: '',
createDate: new Date().toISOString(),
order: 'mock-order',
qrExpirationDate: new Date().toISOString(),
phoneNumber: ''
}, 500);
}
// Fallback — pass through
return next(req);
};

View File

@@ -6,4 +6,28 @@ export interface Category {
wideBanner?: string;
itemCount?: number;
priority?: number;
// BackOffice API fields
id?: string;
visible?: boolean;
img?: string;
projectId?: string;
subcategories?: Subcategory[];
}
export interface Subcategory {
id: string;
name: string;
visible?: boolean;
priority?: number;
img?: string;
categoryId: string;
parentId: string;
itemCount?: number;
hasItems?: boolean;
subcategories?: Subcategory[];
}
export interface CategoryTranslation {
name?: string;
}

View File

@@ -5,6 +5,25 @@ export interface Photo {
type?: string;
}
export interface DescriptionField {
key: string;
value: string;
}
export interface Comment {
id?: string;
text: string;
author?: string;
stars?: number;
createdAt?: string;
}
export interface ItemTranslation {
name?: string;
simpleDescription?: string;
description?: DescriptionField[];
}
export interface Review {
rating?: number;
content?: string;
@@ -23,6 +42,24 @@ export interface Question {
downvotes: number;
}
/** Localized name entry from backend */
export interface ItemName {
language: string;
value: string;
}
/** Localized description entry from backend */
export interface ItemDescription {
language: string;
value: string;
}
/** Key-value attribute pair */
export interface ItemAttribute {
key: string;
value: string;
}
export interface Item {
categoryID: number;
itemID: number;
@@ -36,7 +73,28 @@ export interface Item {
rating: number;
callbacks: Review[] | null;
questions: Question[] | null;
partnerID?: string;
quantity?: number;
// Backend API fields
colour?: string;
size?: string;
language?: string;
names?: ItemName[];
descriptions?: ItemDescription[];
attributes?: ItemAttribute[];
// BackOffice API fields
id?: string;
visible?: boolean;
priority?: number;
imgs?: string[];
tags?: string[];
badges?: string[];
simpleDescription?: string;
descriptionFields?: DescriptionField[];
subcategoryId?: string;
translations?: Record<string, ItemTranslation>;
comments?: Comment[];
}
export interface CartItem extends Item {

View File

@@ -31,12 +31,12 @@
(touchstart)="onSwipeStart(item.itemID, $event)">
<div class="cart-item">
<a [routerLink]="['/item', item.itemID] | langRoute" class="item-image">
<img [src]="getMainImage(item)" [alt]="item.name" loading="lazy" />
<img [src]="getMainImage(item)" [alt]="itemName(item)" loading="lazy" />
</a>
<div class="item-info">
<div class="item-header">
<a [routerLink]="['/item', item.itemID] | langRoute" class="item-name">{{ item.name }}</a>
<a [routerLink]="['/item', item.itemID] | langRoute" class="item-name">{{ itemName(item) }}</a>
<button class="remove-btn" (click)="removeItem(item.itemID)" title="Remove">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12"/>
@@ -44,17 +44,36 @@
</button>
</div>
<p class="item-description">{{ item.description.substring(0, 100) }}...</p>
<p class="item-description">{{ itemDesc(item) || '' }}...</p>
@if (item.colour || item.size) {
<div class="cart-item-variants">
@if (item.colour) {
<span class="cart-variant">{{ 'itemDetail.colour' | translate }}: {{ item.colour }}</span>
}
@if (item.size) {
<span class="cart-variant">{{ 'itemDetail.size' | translate }}: {{ item.size }}</span>
}
</div>
}
@if (item.badges && item.badges.length > 0) {
<div class="cart-item-badges">
@for (badge of item.badges; track badge) {
<span class="item-badge" [class]="getBadgeClass(badge)">{{ badge }}</span>
}
</div>
}
<div class="item-footer">
<div class="item-pricing">
@if (item.discount > 0) {
<div class="price-with-discount">
<span class="original-price">{{ item.price }} </span>
<span class="current-price">{{ getDiscountedPrice(item) | number:'1.2-2' }} </span>
<span class="original-price">{{ item.price }} {{ item.currency }}</span>
<span class="current-price">{{ getDiscountedPrice(item) | number:'1.2-2' }} {{ item.currency }}</span>
</div>
} @else {
<span class="current-price">{{ item.price }} </span>
<span class="current-price">{{ item.price }} {{ item.currency }}</span>
}
</div>
@@ -91,17 +110,17 @@
<div class="summary-row">
<span>{{ 'cart.items' | translate }} ({{ itemCount() }})</span>
<span class="value">{{ totalPrice() | number:'1.2-2' }} </span>
<span class="value">{{ totalPrice() | number:'1.2-2' }} {{ currentCurrency }}</span>
</div>
<div class="summary-row delivery">
<span>{{ 'cart.deliveryLabel' | translate }}</span>
<span>0 </span>
<span>0 {{ currentCurrency }}</span>
</div>
<div class="summary-row total">
<span>{{ 'cart.toPay' | translate }}</span>
<span class="total-price">{{ totalPrice() | number:'1.2-2' }} </span>
<span class="total-price">{{ totalPrice() | number:'1.2-2' }} {{ currentCurrency }}</span>
</div>
<div class="terms-agreement">
@@ -130,6 +149,36 @@
>
{{ 'cart.checkout' | translate }}
</button>
@if (!isAuthenticated()) {
<div class="cart-login-gate">
<div class="login-gate-icon">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
</svg>
</div>
<p class="login-gate-title">{{ 'cart.loginRequired' | translate }}</p>
<p class="login-gate-desc">{{ 'cart.loginRequiredDesc' | translate }}</p>
<button class="telegram-login-btn" (click)="requestLogin()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
</svg>
{{ 'cart.loginWithTelegram' | translate }}
</button>
<div class="login-gate-qr">
<p class="qr-hint">{{ 'cart.orScanQr' | translate }}</p>
<div class="qr-wrapper">
<img [src]="'https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=' + loginUrl()"
alt="QR Code"
width="150"
height="150"
loading="lazy" />
</div>
</div>
</div>
}
</div>
</div>
}
@@ -166,7 +215,7 @@
<div class="payment-info">
<div class="payment-amount">
<span class="label">{{ 'cart.amountToPay' | translate }}</span>
<span class="amount">{{ totalPrice() | number:'1.2-2' }} RUB</span>
<span class="amount">{{ totalPrice() | number:'1.2-2' }} {{ currentCurrency }}</span>
</div>
<div class="waiting-indicator">
@@ -256,3 +305,5 @@
</div>
</div>
}
<app-telegram-login />

View File

@@ -364,6 +364,22 @@
line-height: 1.5;
}
.cart-item-variants {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 4px;
.cart-variant {
font-size: 0.8rem;
color: #497671;
background: rgba(73, 118, 113, 0.08);
padding: 3px 10px;
border-radius: 6px;
font-weight: 500;
}
}
.item-footer {
display: flex;
justify-content: space-between;
@@ -464,6 +480,22 @@
line-height: 1.6;
}
.cart-item-variants {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 4px;
.cart-variant {
font-size: 0.8rem;
color: #6366f1;
background: rgba(99, 102, 241, 0.08);
padding: 3px 10px;
border-radius: 6px;
font-weight: 500;
}
}
.item-footer {
display: flex;
justify-content: space-between;
@@ -689,6 +721,85 @@
cursor: not-allowed;
}
}
.cart-login-gate {
margin-top: 16px;
padding: 20px;
border-radius: 14px;
background: rgba(42, 171, 238, 0.05);
border: 1px dashed rgba(42, 171, 238, 0.3);
text-align: center;
.login-gate-icon {
margin: 0 auto 10px;
width: 56px;
height: 56px;
border-radius: 50%;
background: rgba(42, 171, 238, 0.1);
color: #2AABEE;
display: flex;
align-items: center;
justify-content: center;
}
.login-gate-title {
margin: 0 0 4px;
font-size: 1rem;
font-weight: 700;
color: #1a1a1a;
}
.login-gate-desc {
margin: 0 0 16px;
font-size: 0.85rem;
color: #6b7280;
line-height: 1.4;
}
.telegram-login-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border: none;
border-radius: 10px;
background: #2AABEE;
color: #fff;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #229ED9;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(42, 171, 238, 0.3);
}
}
.login-gate-qr {
margin-top: 14px;
.qr-hint {
margin: 0 0 8px;
font-size: 0.8rem;
color: #999;
}
.qr-wrapper {
display: inline-flex;
padding: 10px;
background: #fff;
border-radius: 10px;
border: 1px solid #e5e7eb;
img {
display: block;
border-radius: 4px;
}
}
}
}
}
// Novo Cart Summary - Green Modern

View File

@@ -7,15 +7,17 @@ import { Item, CartItem } from '../../models';
import { interval, Subscription } from 'rxjs';
import { switchMap, take } from 'rxjs/operators';
import { EmptyCartIconComponent } from '../../components/empty-cart-icon/empty-cart-icon.component';
import { TelegramLoginComponent } from '../../components/telegram-login/telegram-login.component';
import { environment } from '../../../environments/environment';
import { getDiscountedPrice, getMainImage, trackByItemId } from '../../utils/item.utils';
import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass, getTranslatedField } from '../../utils/item.utils';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service';
import { PAYMENT_POLL_INTERVAL_MS, PAYMENT_MAX_CHECKS, PAYMENT_TIMEOUT_CLOSE_MS, PAYMENT_ERROR_CLOSE_MS, LINK_COPIED_DURATION_MS } from '../../config/constants';
@Component({
selector: 'app-cart',
imports: [DecimalPipe, RouterLink, FormsModule, EmptyCartIconComponent, LangRoutePipe, TranslatePipe],
imports: [DecimalPipe, RouterLink, FormsModule, EmptyCartIconComponent, TelegramLoginComponent, LangRoutePipe, TranslatePipe],
templateUrl: './cart.component.html',
styleUrls: ['./cart.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@@ -30,6 +32,9 @@ export class CartComponent implements OnDestroy {
private i18n = inject(TranslateService);
private authService = inject(AuthService);
isAuthenticated = this.authService.isAuthenticated;
loginUrl = signal('');
// Swipe state
swipedItemId = signal<number | null>(null);
@@ -51,7 +56,7 @@ export class CartComponent implements OnDestroy {
emailSubmitting = signal<boolean>(false);
paidItems: CartItem[] = [];
maxChecks = 36; // 36 checks * 5 seconds = 180 seconds (3 minutes)
maxChecks = PAYMENT_MAX_CHECKS;
private pollingSubscription?: Subscription;
private closeTimeout?: ReturnType<typeof setTimeout>;
@@ -64,6 +69,11 @@ export class CartComponent implements OnDestroy {
this.items = this.cartService.items;
this.itemCount = this.cartService.itemCount;
this.totalPrice = this.cartService.totalPrice;
this.loginUrl.set(this.authService.getTelegramLoginUrl());
}
requestLogin(): void {
this.authService.requestLogin();
}
ngOnDestroy(): void {
@@ -129,6 +139,11 @@ export class CartComponent implements OnDestroy {
readonly getMainImage = getMainImage;
readonly trackByItemId = trackByItemId;
readonly getDiscountedPrice = getDiscountedPrice;
readonly getBadgeClass = getBadgeClass;
itemName(item: Item): string { return getTranslatedField(item, 'name', this.langService.currentLanguage()); }
itemDesc(item: Item): string { return getTranslatedField(item, 'simpleDescription', this.langService.currentLanguage()); }
get currentCurrency(): string { return this.langService.currentCurrency(); }
checkout(): void {
if (!this.termsAccepted) {
@@ -173,7 +188,7 @@ export class CartComponent implements OnDestroy {
const paymentData = {
amount: this.totalPrice(),
currency: 'RUB',
currency: this.langService.currentCurrency(),
siteuserID: userId,
siteorderID: orderId,
redirectUrl: '',
@@ -199,15 +214,17 @@ export class CartComponent implements OnDestroy {
error: (err) => {
console.error('Error creating payment:', err);
this.paymentStatus.set('timeout');
if (this.closeTimeout) clearTimeout(this.closeTimeout);
this.closeTimeout = setTimeout(() => {
this.closePaymentPopup();
}, 4000);
}, PAYMENT_ERROR_CLOSE_MS);
}
});
}
startPolling(): void {
this.pollingSubscription = interval(5000) // every 5 seconds
this.stopPolling();
this.pollingSubscription = interval(PAYMENT_POLL_INTERVAL_MS)
.pipe(
take(this.maxChecks), // maximum 36 checks (3 minutes)
switchMap(() => {
@@ -231,17 +248,19 @@ export class CartComponent implements OnDestroy {
if (this.paymentStatus() === 'waiting') {
this.paymentStatus.set('timeout');
// Close popup after showing timeout message
if (this.closeTimeout) clearTimeout(this.closeTimeout);
this.closeTimeout = setTimeout(() => {
this.closePaymentPopup();
}, 3000);
}, PAYMENT_TIMEOUT_CLOSE_MS);
}
},
error: (err) => {
console.error('Error checking payment status:', err);
// Continue checking even on error until time runs out
if (this.closeTimeout) clearTimeout(this.closeTimeout);
this.closeTimeout = setTimeout(() => {
this.closePaymentPopup();
}, 3000);
}, PAYMENT_TIMEOUT_CLOSE_MS);
}
});
}
@@ -257,7 +276,7 @@ export class CartComponent implements OnDestroy {
if (url) {
navigator.clipboard.writeText(url).then(() => {
this.linkCopied.set(true);
setTimeout(() => this.linkCopied.set(false), 2000);
setTimeout(() => this.linkCopied.set(false), LINK_COPIED_DURATION_MS);
}).catch(err => {
console.error(this.i18n.t('cart.copyError'), err);
});

View File

@@ -9,17 +9,24 @@
@if (!error()) {
<div class="items-grid">
@for (item of items(); track trackByItemId($index, item)) {
<div class="item-card">
<div class="item-card" (mouseenter)="onItemHover(item.itemID)">
<a [routerLink]="['/item', item.itemID] | langRoute" class="item-link">
<div class="item-image">
<img [src]="getMainImage(item)" [alt]="item.name" loading="lazy" decoding="async" width="300" height="300" />
<img [src]="getMainImage(item)" [alt]="itemName(item)" loading="lazy" decoding="async" width="300" height="300" />
@if (item.discount > 0) {
<div class="discount-badge">-{{ item.discount }}%</div>
}
@if (item.badges && item.badges.length > 0) {
<div class="item-badges-overlay">
@for (badge of item.badges; track badge) {
<span class="item-badge" [class]="getBadgeClass(badge)">{{ badge }}</span>
}
</div>
}
</div>
<div class="item-details">
<h3 class="item-name">{{ item.name }}</h3>
<h3 class="item-name">{{ itemName(item) }}</h3>
<div class="item-rating">
<span class="rating-stars">⭐ {{ item.rating }}</span>
@@ -45,19 +52,29 @@
</div>
</a>
<button class="add-to-cart-btn" (click)="addToCart(item.itemID, $event)">
<button class="add-to-cart-btn" (click)="addToCart(item.itemID, $event)" [attr.aria-label]="('category.addToCart' | translate) + ': ' + item.name">
{{ 'category.addToCart' | translate }}
</button>
</div>
}
</div>
@if (loading() && items().length > 0) {
<div class="loading-more">
<div class="spinner"></div>
<p>{{ 'category.loadingMore' | translate }}</p>
</div>
}
@if (loading() && items().length > 0) {
@for (i of skeletonSlots; track i) {
<div class="item-card skeleton-card">
<div class="item-link">
<div class="item-image skeleton-image"></div>
<div class="item-details">
<div class="skeleton-line skeleton-title"></div>
<div class="skeleton-line skeleton-rating"></div>
<div class="skeleton-line skeleton-price"></div>
<div class="skeleton-line skeleton-stock"></div>
</div>
</div>
<div class="skeleton-btn"></div>
</div>
}
}
</div>
@if (!hasMore() && items().length > 0) {
<div class="no-more">

View File

@@ -95,7 +95,7 @@
.items-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 30px;
margin-bottom: 40px;
width: 100%;
@@ -103,8 +103,10 @@
.item-card {
width: 100%;
min-width: 0;
display: flex;
flex-direction: column;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
@@ -139,7 +141,7 @@
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
background: #f0f0f0;
img {
width: 100%;
@@ -147,7 +149,7 @@
object-fit: contain;
background: white;
padding: 12px;
transition: transform 0.3s ease;
transition: transform 0.3s ease, opacity 0.3s ease;
}
&:hover img {
@@ -192,6 +194,7 @@
margin: 0;
line-height: 1.3;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
@@ -287,11 +290,6 @@
}
}
.loading-more {
text-align: center;
padding: 40px 20px;
}
.spinner {
width: 40px;
height: 40px;
@@ -312,24 +310,77 @@
padding: 40px 20px;
}
// Skeleton loading cards
.skeleton-card {
pointer-events: none;
.skeleton-image {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.skeleton-line {
border-radius: 6px;
background: linear-gradient(90deg, #e8e8e8 25%, #d8d8d8 50%, #e8e8e8 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.skeleton-title {
height: 16px;
width: 80%;
}
.skeleton-rating {
height: 12px;
width: 50%;
}
.skeleton-price {
height: 18px;
width: 40%;
margin-top: auto;
}
.skeleton-stock {
height: 6px;
width: 60px;
}
.skeleton-btn {
height: 42px;
background: linear-gradient(90deg, #5a8a85 25%, #497671 50%, #5a8a85 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 0 0 13px 13px;
margin-top: -1px;
}
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
// Responsive
@media (max-width: 1200px) {
.items-grid {
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 24px;
}
}
@media (max-width: 992px) {
.items-grid {
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 20px;
}
}
@media (max-width: 768px) {
.items-grid {
grid-template-columns: repeat(2, 1fr);
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
@@ -353,7 +404,7 @@
}
.items-grid {
grid-template-columns: repeat(2, 1fr);
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}

View File

@@ -1,12 +1,15 @@
import { Component, OnInit, OnDestroy, signal, HostListener, ChangeDetectionStrategy } from '@angular/core';
import { Component, OnInit, OnDestroy, signal, HostListener, ChangeDetectionStrategy, inject } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { ApiService, CartService } from '../../services';
import { PrefetchService } from '../../services/prefetch.service';
import { Item } from '../../models';
import { Subscription } from 'rxjs';
import { getDiscountedPrice, getMainImage, trackByItemId } from '../../utils/item.utils';
import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass, getTranslatedField } from '../../utils/item.utils';
import { LanguageService } from '../../services/language.service';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
import { SCROLL_THRESHOLD_PX, SCROLL_DEBOUNCE_MS, ITEMS_PER_PAGE } from '../../config/constants';
@Component({
selector: 'app-category',
@@ -23,7 +26,7 @@ export class CategoryComponent implements OnInit, OnDestroy {
hasMore = signal(true);
private skip = 0;
private readonly count = 20;
private readonly count = ITEMS_PER_PAGE;
private isLoadingMore = false;
private routeSubscription?: Subscription;
private scrollTimeout?: ReturnType<typeof setTimeout>;
@@ -31,7 +34,8 @@ export class CategoryComponent implements OnInit, OnDestroy {
constructor(
private route: ActivatedRoute,
private apiService: ApiService,
private cartService: CartService
private cartService: CartService,
private prefetchService: PrefetchService
) {}
ngOnInit(): void {
@@ -90,12 +94,12 @@ export class CategoryComponent implements OnInit, OnDestroy {
this.scrollTimeout = setTimeout(() => {
const scrollPosition = window.innerHeight + window.scrollY;
const bottomPosition = document.documentElement.scrollHeight - 500;
const bottomPosition = document.documentElement.scrollHeight - SCROLL_THRESHOLD_PX;
if (scrollPosition >= bottomPosition && !this.loading() && this.hasMore() && !this.isLoadingMore) {
this.loadItems();
}
}, 100);
}, SCROLL_DEBOUNCE_MS);
}
addToCart(itemID: number, event: Event): void {
@@ -104,7 +108,16 @@ export class CategoryComponent implements OnInit, OnDestroy {
this.cartService.addItem(itemID);
}
onItemHover(itemID: number): void {
this.prefetchService.prefetchItem(itemID);
}
readonly skeletonSlots = Array.from({ length: 8 });
readonly getDiscountedPrice = getDiscountedPrice;
readonly getMainImage = getMainImage;
readonly trackByItemId = trackByItemId;
readonly getBadgeClass = getBadgeClass;
private langService = inject(LanguageService);
itemName(item: Item): string { return getTranslatedField(item, 'name', this.langService.currentLanguage()); }
}

View File

@@ -18,6 +18,30 @@
<h2>{{ parentName() }}</h2>
</header>
<!-- Nested subcategories from API (backOffice format with hasItems) -->
@if (nestedSubcategories().length > 0) {
<div class="categories-grid">
@for (sub of nestedSubcategories(); track trackBySubId($index, sub)) {
<a [routerLink]="['/category', sub.id] | langRoute" class="category-card">
<div class="category-image">
@if (sub.img) {
<img [src]="sub.img" [alt]="sub.name" loading="lazy" decoding="async" />
} @else {
<div class="category-fallback">{{ sub.name.charAt(0) }}</div>
}
</div>
<div class="category-info">
<h3 class="category-name">{{ sub.name }}</h3>
@if (sub.itemCount) {
<span class="category-count">{{ sub.itemCount }}</span>
}
</div>
</a>
}
</div>
}
<!-- Legacy flat subcategories -->
@if (subcategories().length > 0) {
<div class="categories-grid">
@for (cat of subcategories(); track trackByCategoryId($index, cat)) {
@@ -35,7 +59,53 @@
</a>
}
</div>
} @else {
}
<!-- Items directly in this category -->
@if (categoryItems().length > 0) {
<div class="category-items-section">
<h3 class="items-section-title">{{ 'subcategories.itemsInCategory' | translate }}</h3>
<div class="items-grid">
@for (item of categoryItems(); track trackByItemId($index, item)) {
<a [routerLink]="['/item', item.itemID] | langRoute" class="item-card">
<div class="item-image">
<img [src]="getMainImage(item)" [alt]="itemName(item)" loading="lazy" decoding="async" />
@if (item.discount > 0) {
<span class="item-discount">-{{ item.discount }}%</span>
}
@if (item.badges && item.badges.length > 0) {
<div class="item-badges">
@for (badge of item.badges; track badge) {
<span class="item-badge" [class]="getBadgeClass(badge)">{{ badge }}</span>
}
</div>
}
</div>
<div class="item-info">
<h4 class="item-name">{{ itemName(item) }}</h4>
<div class="item-price">
@if (item.discount > 0) {
<span class="old-price">{{ item.price }} {{ item.currency }}</span>
<span class="current-price">{{ getDiscountedPrice(item) | number:'1.0-0' }} {{ item.currency }}</span>
} @else {
<span class="current-price">{{ item.price }} {{ item.currency }}</span>
}
</div>
<button class="item-cart-btn" (click)="addToCart(item.itemID, $event)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="9" cy="21" r="1"></circle>
<circle cx="20" cy="21" r="1"></circle>
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
</svg>
</button>
</div>
</a>
}
</div>
</div>
}
@if (!hasSubcategories() && categoryItems().length === 0) {
<div class="no-subcats">
<div class="no-subcats-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">

View File

@@ -235,6 +235,149 @@
min-height: calc(2 * 1.3em);
}
.category-count {
font-family: "DM Sans", sans-serif;
font-size: 0.8rem;
color: #697777;
}
// Items section within subcategories page
.category-items-section {
margin-top: 40px;
.items-section-title {
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 1.5rem;
font-weight: 700;
color: #1e3c38;
margin: 0 0 20px 0;
}
}
.items-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
}
.item-card {
display: flex;
flex-direction: column;
text-decoration: none;
border: 1px solid #d3dad9;
border-radius: 13px;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: #fff;
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
}
.item-image {
position: relative;
aspect-ratio: 1;
overflow: hidden;
background: #f5f5f5;
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.item-card:hover & img {
transform: scale(1.05);
}
.item-discount {
position: absolute;
top: 8px;
right: 8px;
background: #dc2626;
color: white;
font-size: 0.75rem;
font-weight: 700;
padding: 2px 8px;
border-radius: 6px;
}
.item-badges {
position: absolute;
top: 8px;
left: 8px;
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.item-badge {
font-size: 0.65rem;
font-weight: 600;
padding: 2px 6px;
border-radius: 4px;
text-transform: uppercase;
}
}
.item-info {
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.item-name {
font-family: "DM Sans", sans-serif;
font-size: 0.9rem;
font-weight: 600;
color: #1e3c38;
margin: 0;
line-height: 1.3;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.item-price {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
.old-price {
font-size: 0.8rem;
color: #a1b4b5;
text-decoration: line-through;
}
.current-price {
font-size: 1rem;
font-weight: 700;
color: #1e3c38;
}
}
.item-cart-btn {
align-self: flex-end;
background: #497671;
color: white;
border: none;
border-radius: 8px;
padding: 6px 10px;
cursor: pointer;
transition: background 0.2s ease;
&:hover {
background: #3a5f5b;
}
}
// Keyframes
@keyframes spin {
to { transform: rotate(360deg); }
@@ -248,6 +391,11 @@
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.items-grid {
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
}
@media (max-width: 992px) {
@@ -273,6 +421,11 @@
gap: 16px;
}
.items-grid {
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.category-info {
padding: 10px 12px;
}
@@ -294,6 +447,11 @@
gap: 12px;
}
.items-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.category-info {
padding: 8px 10px;
}

View File

@@ -1,15 +1,17 @@
import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy, inject } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { ApiService, LanguageService } from '../../services';
import { Category } from '../../models';
import { ApiService, CartService, LanguageService } from '../../services';
import { Category, Item, Subcategory } from '../../models';
import { Subscription } from 'rxjs';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service';
import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass, getTranslatedField } from '../../utils/item.utils';
@Component({
selector: 'app-subcategories',
imports: [RouterLink, LangRoutePipe, TranslatePipe],
imports: [DecimalPipe, RouterLink, LangRoutePipe, TranslatePipe],
templateUrl: './subcategories.component.html',
styleUrls: ['./subcategories.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
@@ -17,6 +19,10 @@ import { TranslateService } from '../../i18n/translate.service';
export class SubcategoriesComponent implements OnInit, OnDestroy {
categories = signal<Category[]>([]);
subcategories = signal<Category[]>([]);
/** Nested subcategories from API with hasItems support */
nestedSubcategories = signal<Subcategory[]>([]);
/** Items belonging directly to this category (when hasItems is true) */
categoryItems = signal<Item[]>([]);
loading = signal(true);
error = signal<string | null>(null);
@@ -29,7 +35,8 @@ export class SubcategoriesComponent implements OnInit, OnDestroy {
private route: ActivatedRoute,
private router: Router,
private apiService: ApiService,
private langService: LanguageService
private langService: LanguageService,
private cartService: CartService
) {}
ngOnInit(): void {
@@ -45,19 +52,40 @@ export class SubcategoriesComponent implements OnInit, OnDestroy {
private loadForParent(parentID: number): void {
this.loading.set(true);
this.categoryItems.set([]);
this.nestedSubcategories.set([]);
this.apiService.getCategories().subscribe({
next: (cats) => {
this.categories.set(cats);
const subs = cats.filter(c => c.parentID === parentID);
const parent = cats.find(c => c.categoryID === parentID);
this.parentName.set(parent ? parent.name : this.i18n.t('home.categoriesTitle'));
if (!subs || subs.length === 0) {
// Check for nested subcategories from API response (backOffice format)
const nested = parent?.subcategories || [];
const visibleNested = nested.filter(s => s.visible !== false);
// Also check flat legacy subcategories
const flatSubs = cats.filter(c => c.parentID === parentID);
if (visibleNested.length > 0) {
// Use nested subcategories from API
this.nestedSubcategories.set(visibleNested);
this.subcategories.set([]);
// If this category itself has items, load them too
this.loadCategoryItems(parentID);
} else if (flatSubs.length > 0) {
// Legacy flat subcategories
this.subcategories.set(flatSubs);
this.nestedSubcategories.set([]);
// Also load items for this category in case it has direct items
this.loadCategoryItems(parentID);
} else {
// No subcategories: redirect to items list for this category
const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}/category`, parentID, 'items'], { replaceUrl: true });
} else {
this.subcategories.set(subs);
}
this.loading.set(false);
@@ -70,8 +98,41 @@ export class SubcategoriesComponent implements OnInit, OnDestroy {
});
}
/** Load items that belong directly to this category */
private loadCategoryItems(categoryID: number): void {
this.apiService.getCategoryItems(categoryID, 50, 0).subscribe({
next: (items) => {
this.categoryItems.set(items);
},
error: () => {
// Not critical — subcategories still work
}
});
}
hasSubcategories(): boolean {
return this.subcategories().length > 0 || this.nestedSubcategories().length > 0;
}
addToCart(itemID: number, event: Event): void {
event.preventDefault();
event.stopPropagation();
this.cartService.addItem(itemID);
}
// TrackBy function for performance optimization
trackByCategoryId(index: number, category: Category): number {
return category.categoryID;
}
trackBySubId(index: number, sub: Subcategory): string {
return sub.id;
}
readonly getDiscountedPrice = getDiscountedPrice;
readonly getMainImage = getMainImage;
readonly trackByItemId = trackByItemId;
readonly getBadgeClass = getBadgeClass;
itemName(item: Item): string { return getTranslatedField(item, 'name', this.langService.currentLanguage()); }
}

View File

@@ -19,10 +19,23 @@
<app-items-carousel />
@if (loading()) {
<div class="novo-loading">
<div class="novo-spinner"></div>
<p>{{ 'home.loading' | translate }}</p>
</div>
<section class="novo-categories">
<div class="novo-section-header">
<div class="skeleton-line" style="height: 32px; width: 200px; margin: 0 auto 12px;"></div>
<div class="skeleton-line" style="height: 18px; width: 300px; margin: 0 auto;"></div>
</div>
<div class="novo-categories-grid">
@for (i of skeletonSlots; track i) {
<div class="novo-category-card skeleton-card">
<div class="novo-category-image skeleton-image"></div>
<div class="novo-category-info">
<div class="skeleton-line" style="height: 18px; width: 70%;"></div>
<div class="skeleton-line" style="height: 18px; width: 20px;"></div>
</div>
</div>
}
</div>
</section>
}
@if (error()) {
@@ -101,10 +114,20 @@
<app-items-carousel />
@if (loading()) {
<div class="dexar-loading">
<div class="dexar-spinner"></div>
<p>{{ 'home.loadingDexar' | translate }}</p>
</div>
<section class="dexar-categories">
<div class="skeleton-line" style="height: 36px; width: 220px; margin-bottom: 40px;"></div>
<div class="dexar-categories-grid">
@for (i of skeletonSlots; track i) {
<div class="dexar-category-card skeleton-card">
<div class="dexar-category-image skeleton-image"></div>
<div class="dexar-category-info">
<div class="skeleton-line" style="height: 16px; width: 75%;"></div>
<div class="skeleton-line" style="height: 12px; width: 40%; margin-top: 4px;"></div>
</div>
</div>
}
</div>
</section>
}
@if (error()) {

View File

@@ -896,3 +896,26 @@
transform: translateY(-2px);
}
}
// Skeleton loading cards
.skeleton-card {
pointer-events: none;
}
.skeleton-image {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.skeleton-line {
border-radius: 6px;
background: linear-gradient(90deg, #e8e8e8 25%, #d8d8d8 50%, #e8e8e8 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, signal, computed, ChangeDetectionStrategy } from '@angular/core';
import { Component, OnInit, OnDestroy, signal, computed, ChangeDetectionStrategy } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { ApiService, LanguageService } from '../../services';
import { Category } from '../../models';
@@ -14,13 +14,14 @@ import { TranslatePipe } from '../../i18n/translate.pipe';
styleUrls: ['./home.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HomeComponent implements OnInit {
export class HomeComponent implements OnInit, OnDestroy {
brandName = environment.brandFullName;
isnovo = environment.theme === 'novo';
categories = signal<Category[]>([]);
wideCategories = signal<Set<number>>(new Set());
loading = signal(true);
error = signal<string | null>(null);
readonly skeletonSlots = Array.from({ length: 6 });
// Memoized computed values for performance
topLevelCategories = computed(() => {
@@ -56,6 +57,14 @@ export class HomeComponent implements OnInit {
this.loadCategories();
}
ngOnDestroy(): void {
this.pendingImages.forEach(img => {
img.onload = null;
img.onerror = null;
});
this.pendingImages.clear();
}
loadCategories(): void {
this.loading.set(true);
this.apiService.getCategories().subscribe({
@@ -84,13 +93,17 @@ export class HomeComponent implements OnInit {
return this.wideCategories().has(categoryID);
}
private pendingImages = new Set<HTMLImageElement>();
private detectWideImages(categories: Category[]): void {
const topLevel = categories.filter(c => c.parentID === 0);
topLevel.forEach(cat => {
if (!cat.wideBanner) return;
const img = new Image();
this.pendingImages.add(img);
img.onload = () => {
this.pendingImages.delete(img);
const ratio = img.naturalWidth / img.naturalHeight;
if (ratio > 2) {
this.wideCategories.update(set => {
@@ -100,6 +113,7 @@ export class HomeComponent implements OnInit {
});
}
};
img.onerror = () => this.pendingImages.delete(img);
img.src = cat.wideBanner;
});
}

View File

@@ -1,4 +1,4 @@
<div class="legal-page">
<div class="legal-page">
<div class="legal-container">
<h1>About the company LLC «INT FIN LOGISTIC»</h1>
@@ -65,7 +65,7 @@
<p><strong>Director:</strong> Оганнисян Ашот Рафикович</p>
<p><strong>Legal address:</strong><br>АРМЕНИЯ, 2301, КОТАЙКСКАЯ ОБЛАСТЬ, РАЗДАН, ХАЧАТРЯНА ул, 31, 4</p>
<p><strong>Office in Armenia:</strong><br>0033, Ереван, улица Братьев Орбели, 47</p>
<p><strong>Office in Russia:</strong><br>121059, Москва, наб. Тараса Шевченко, 3к2</p>
<p><strong>Key details:</strong><br>ИНН (RF): 9909697628<br>ИНН (Armenia): 03033502<br>КПП: 770287001<br>ОГРН: 85.110.1408711</p>
<p><strong>Banking details:</strong><br>Bank: АО "Райффайзенбанк"<br>Settlement account: 40807810500000002376<br>Correspondent account: 30101810200000000700<br>БИК: 044525700</p>
<p><strong>Contact information:</strong><br>Phone (Russia): +7 (926) 459-31-57<br>Phone (Armenia): +374 94 86 18 16<br>Email: info&#64;dexarmarket.ru<br>Website: www.dexarmarket.ru</p>

View File

@@ -1,4 +1,4 @@
<div class="legal-page">
<div class="legal-page">
<div class="legal-container">
<h1>«ИНТ ФИН ЛОГИСТИК» ՍՊԸ ընկերության մասին</h1>
@@ -65,7 +65,6 @@
<p><strong>Տնօրեն՝</strong> Оганнисян Ашот Рафикович</p>
<p><strong>Իրավաբանական հասցե՝</strong><br>АРМЕНИЯ, 2301, КОТАЙКСКАЯ ОБЛАСТЬ, РАЗДАН, ХАЧАТРЯНА ул, 31, 4</p>
<p><strong>Գրասենյակ Հայաստանում՝</strong><br>0033, Երևան, Եղբայրներ Օրբելի փողոց, 47</p>
<p><strong>Գրասենյակ Ռուսաստանում՝</strong><br>121059, Москва, наб. Тараса Шевченко, 3к2</p>
<p><strong>Հիմնական վավերապայմանները՝</strong><br>ՀՍՀ (ՌՄ)՝ 9909697628<br>ՀՍՀ (Հայաստան)՝ 03033502<br>ԿՊՊ՝ 770287001<br>ՕԳՌՆ՝ 85.110.1408711</p>
<p><strong>Բանկային վավերապայմանները՝</strong><br>Բանկ՝ АО "Райффайзенбанк"<br>Հաշվարկային հաշիվ՝ 40807810500000002376<br>Թղթակցային հաշիվ՝ 30101810200000000700<br>ԲԻԿ՝ 044525700</p>
<p><strong>Կապի տեղեկատվություն՝</strong><br>Հեռախոս (Ռուսաստան)՝ +7 (926) 459-31-57<br>Հեռախոս (Հայաստան)՝ +374 94 86 18 16<br>Էլ. փոստ՝ info&#64;dexarmarket.ru<br>Կայք՝ www.dexarmarket.ru</p>

View File

@@ -1,4 +1,4 @@
<div class="legal-page">
<div class="legal-page">
<div class="legal-container">
<h1>О компании ООО «ИНТ ФИН ЛОГИСТИК»</h1>
@@ -65,7 +65,7 @@
<p><strong>Директор:</strong> Оганнисян Ашот Рафикович</p>
<p><strong>Юридический адрес:</strong><br>АРМЕНИЯ, 2301, КОТАЙКСКАЯ ОБЛАСТЬ, РАЗДАН, ХАЧАТРЯНА ул, 31, 4</p>
<p><strong>Офис в Армении:</strong><br>0033, Ереван, улица Братьев Орбели, 47</p>
<p><strong>Офис в России:</strong><br>121059, Москва, наб. Тараса Шевченко, 3к2</p>
<p><strong>Основные реквизиты:</strong><br>ИНН (РФ): 9909697628<br>ИНН (Армения): 03033502<br>КПП: 770287001<br>ОГРН: 85.110.1408711</p>
<p><strong>Банковские реквизиты:</strong><br>Банк: АО "Райффайзенбанк"<br>Расчетный счет: 40807810500000002376<br>Корр. счет: 30101810200000000700<br>БИК: 044525700</p>
<p><strong>Контактная информация:</strong><br>Телефон (Россия): +7 (926) 459-31-57<br>Телефон (Армения): +374 94 86 18 16<br>Email: info&#64;dexarmarket.ru<br>Сайт: www.dexarmarket.ru</p>

View File

@@ -1,4 +1,4 @@
<div class="legal-page">
<div class="legal-page">
<div class="legal-container">
<h1>Contacts</h1>
@@ -35,7 +35,7 @@
<section class="legal-section">
<h2>Office Addresses</h2>
<p><strong>Office in Armenia:</strong> 0033, Yerevan, Orbeli Brothers Street, 47</p>
<p><strong>Office in Russia:</strong> 121059, Moscow, Taras Shevchenko Embankment, 3/2</p>
</section>
<section class="legal-section">

View File

@@ -1,4 +1,4 @@
<div class="legal-page">
<div class="legal-page">
<div class="legal-container">
<h1>Կապ</h1>
@@ -35,8 +35,7 @@
<section class="legal-section">
<h2>Գրասենյակների հասցեներ</h2>
<p><strong>Գրասենյակ Հայաստանում՝</strong> 0033, Երևան, Եղբայրներ Օրբելի փողոց, 47</p>
<p><strong>Գրասենյակ Ռուսաստանում՝</strong> 121059, Մոսկվա, Տարաս Շևչենկոի փողոց, 3կ2</p>
</section>
</section>
<section class="legal-section">
<h2>Տեխնիկական աջակցություն</h2>

View File

@@ -1,4 +1,4 @@
<div class="legal-page">
<div class="legal-page">
<div class="legal-container">
<h1>Контакты</h1>
@@ -35,7 +35,7 @@
<section class="legal-section">
<h2>Адреса офисов</h2>
<p><strong>Офис в Армении:</strong> 0033, Ереван, улица Братьев Орбели, 47</p>
<p><strong>Офис в России:</strong> 121059, Москва, наб. Тараса Шевченко, 3к2</p>
</section>
<section class="legal-section">

View File

@@ -1,240 +1,244 @@
<h1>Frequently Asked Questions (FAQ) 📌</h1>
<section class="legal-section">
<h2>General Questions</h2>
<div class="faq-item">
<h3>What is DexarMarket?</h3>
<p>DexarMarket is an online marketplace where independent sellers list their products and services. Our goal is to provide a convenient and secure shopping environment — we do not manufacture or sell products ourselves.</p>
<div class="legal-page">
<div class="legal-container">
<h1>Frequently Asked Questions (FAQ) 📌</h1>
<section class="legal-section">
<h2>General Questions</h2>
<div class="faq-item">
<h3>What is DexarMarket?</h3>
<p>DexarMarket is an online marketplace where independent sellers list their products and services. Our goal is to provide a convenient and secure shopping environment — we do not manufacture or sell products ourselves.</p>
</div>
<div class="faq-item">
<h3>How do I register on the platform?</h3>
<p>Registration is easy: open our app within Telegram. Your profile will be created automatically as soon as you log in.</p>
</div>
<div class="faq-item">
<h3>Is it safe to buy on the site?</h3>
<p>Absolutely! All transactions are protected by modern encryption technologies (PCI DSS and 3D Secure). Your banking details are securely stored separately from our system.</p>
</div>
</section>
<section class="legal-section">
<h2>Placing an Order</h2>
<div class="faq-item">
<h3>How do I place an order?</h3>
<p>
1⃣ Find the product you like and add it to your cart.<br>
2⃣ Open the cart and click "Place Order".<br>
3⃣ Enter your delivery address and contact information.<br>
4⃣ Choose a convenient payment method.<br>
5⃣ Review and accept the public offer agreement.<br>
6⃣ Complete the payment for your order.
</p>
</div>
<div class="faq-item">
<h3>Can I make changes to my order after purchase?</h3>
<p>If the seller has not yet shipped the product, contact our support — we will help adjust the order. However, once the product has been shipped, changes are not possible.</p>
</div>
<div class="faq-item">
<h3>How do I cancel an order?</h3>
<p>Contact us or the seller directly through order chats. As long as the package has not been shipped, cancellation is possible with a full refund.</p>
</div>
</section>
<section class="legal-section">
<h2>Payment 💳</h2>
<div class="faq-item">
<h3>What payment methods are available?</h3>
<p>We accept:</p>
<ul>
<li>Bank cards: Visa, MasterCard, MIR,</li>
<li>Fast Payment System (FPS),</li>
<li>Electronic wallets: YooMoney, QIWI,</li>
<li>Cash on delivery (if offered by the seller).</li>
</ul>
</div>
<div class="faq-item">
<h3>When will funds be charged from my card?</h3>
<p>The transaction is processed instantly, but the funds are temporarily held until you receive the product. Once you confirm receipt, the funds are released to the seller.</p>
</div>
<div class="faq-item">
<h3>Why might my payment be declined?</h3>
<p>Possible reasons for decline:</p>
<ul>
<li>Insufficient funds in the account,</li>
<li>Card transaction limit exceeded,</li>
<li>Card blocked by the bank,</li>
<li>Technical issues.</li>
</ul>
<p>We recommend contacting your bank for details.</p>
</div>
<div class="faq-item">
<h3>Will I receive a payment receipt?</h3>
<p>Yes, an electronic receipt will be sent to your specified email immediately after a successful payment in accordance with Federal Law No. 54-FZ.</p>
</div>
</section>
<section class="legal-section">
<h2>Delivery 🚚</h2>
<div class="faq-item">
<h3>Which delivery services are used?</h3>
<p><strong>Digital products:</strong> DexarMarket automatically sends digital products (licenses, keys, certificates) to your email or personal account instantly after payment.</p>
<p><strong>Physical products:</strong> The seller works independently with leading delivery services:</p>
<ul>
<li>CDEK,</li>
<li>Russian Post,</li>
<li>Boxberry,</li>
<li>DPD,</li>
<li>Yandex.Delivery.</li>
</ul>
<p>The delivery method for physical products is chosen based on the seller's preferences and destination.</p>
</div>
<div class="faq-item">
<h3>How much does delivery cost?</h3>
<p><strong>Digital products:</strong> Delivery is free — the product arrives to your email instantly.</p>
<p><strong>Physical products:</strong> The price is determined by the seller and depends on the weight, dimensions, chosen method, and delivery region. The final cost is displayed at checkout.</p>
</div>
<div class="faq-item">
<h3>What is the delivery time for my order?</h3>
<p><strong>Digital products:</strong> Instant delivery to email (within a few minutes after payment).</p>
<p><strong>Physical products</strong> (estimated times):</p>
<ul>
<li>CDEK: 27 business days,</li>
<li>Russian Post: 514 business days,</li>
<li>Boxberry: 25 business days,</li>
<li>DPD: 13 business days,</li>
<li>Yandex.Delivery: same day (if available in your city).</li>
</ul>
</div>
<div class="faq-item">
<h3>How do I track my package?</h3>
<p>You will receive a tracking code via email and can view the order status in your personal account. You can track the package on the official website of the chosen courier service.</p>
</div>
<div class="faq-item">
<h3>What should I do if the product arrived damaged? ⛑</h3>
<p>Inspect the product in front of the courier. If defects are found, fill out a refusal of acceptance report and immediately notify the seller and support service.</p>
</div>
</section>
<section class="legal-section">
<h2>Returns and Exchanges ✅</h2>
<div class="faq-item">
<h3>Can I return a product?</h3>
<p>Yes, the law allows you to return a quality product within 7 days of receipt. Defective items are returned under special rules.</p>
</div>
<div class="faq-item">
<h3>Which products cannot be returned?</h3>
<p>Products listed in Russian Government Decree No. 2463 cannot be returned: medications, cosmetics, underwear, activated digital products, and custom orders. See details in the <a [routerLink]="'/return-policy' | langRoute">"Return Policy"</a> section.</p>
</div>
<div class="faq-item">
<h3>How do I get a refund?</h3>
<p>
1. Notify the seller of your intention to return the product.<br>
2. Return the product in its original packaging.<br>
3. The seller will inspect the product and refund the amount using the same payment method (up to 30 days for the refund to process).
</p>
</div>
<div class="faq-item">
<h3>Who pays for return shipping?</h3>
<p>The buyer pays for return shipping of quality products. If a defect is found, the seller covers the costs.</p>
</div>
</section>
<section class="legal-section">
<h2>Warranty 🔧</h2>
<div class="faq-item">
<h3>Is there a warranty on products?</h3>
<p>Most of our products come with an official manufacturer's warranty. The period ranges from 12 to 36 months and is indicated on each product page.</p>
</div>
<div class="faq-item">
<h3>How do I use the warranty?</h3>
<p>Notify the seller of the defect, attaching photos of the damage and the warranty card. The seller will handle repair or replacement of the product.</p>
</div>
<div class="faq-item">
<h3>What is not covered by warranty?</h3>
<p>Mechanical damage, consequences of improper use, unauthorized repair, moisture exposure (if not protected), and natural wear — none of these are covered under warranty.</p>
</div>
</section>
<section class="legal-section">
<h2>Security and Privacy 🔐</h2>
<div class="faq-item">
<h3>How do you protect my personal data?</h3>
<p>We use modern SSL/TLS encryption methods, do not store card details, and comply with Federal Law No. 152-FZ on personal data protection. Details are available in our <a [routerLink]="'/privacy-policy' | langRoute">Privacy Policy</a>.</p>
</div>
<div class="faq-item">
<h3>Who has access to my data?</h3>
<p>Only your seller for order processing and delivery services. Data is not used by third parties for marketing without your permission.</p>
</div>
<div class="faq-item">
<h3>How do I delete my account?</h3>
<p>Send a request to our support service to delete your account. The account and personal information will be removed within one month.</p>
</div>
</section>
<section class="legal-section">
<h2>Information for Sellers 📋</h2>
<div class="faq-item">
<h3>How do I start selling on the platform?</h3>
<p>To join our sellers, contact our support service by email at <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a>.</p>
</div>
<div class="faq-item">
<h3>What is the platform's commission?</h3>
<p>The commission depends on the product type and sales volume. You can find out the exact terms during registration.</p>
</div>
<div class="faq-item">
<h3>When will I receive payment for a sale?</h3>
<p>The seller receives funds after the customer confirms receipt of the product or two weeks after delivery if the customer has not reported any issues.</p>
</div>
</section>
<section class="legal-section">
<h2>Customer Support 💬</h2>
<div class="faq-item">
<h3>How do I contact support?</h3>
<p>
✉️ <strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a><br>
📞 <strong>Phone (Russia):</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a><br>
📞 <strong>Phone (Armenia):</strong> <a href="tel:+37494861816">+374 94 86 18 16</a><br>
🏢 <strong>Office hours:</strong> 10:0019:00 (MSK)<br>
❄️ <strong>Technical support is available 24/7.</strong>
</p>
</div>
<div class="faq-item">
<h3>How long does it take to get a response?</h3>
<p>During business hours, you will receive a response within two hours. On holidays and weekends, delays of up to 24 hours are possible.</p>
</div>
</section>
<section class="legal-section">
<h2>Need Help?</h2>
<p>If you have any additional questions, please contact us at <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a> — we will promptly resolve any of your questions!</p>
</section>
</div>
<div class="faq-item">
<h3>How do I register on the platform?</h3>
<p>Registration is easy: open our app within Telegram. Your profile will be created automatically as soon as you log in.</p>
</div>
<div class="faq-item">
<h3>Is it safe to buy on the site?</h3>
<p>Absolutely! All transactions are protected by modern encryption technologies (PCI DSS and 3D Secure). Your banking details are securely stored separately from our system.</p>
</div>
</section>
<section class="legal-section">
<h2>Placing an Order</h2>
<div class="faq-item">
<h3>How do I place an order?</h3>
<p>
1⃣ Find the product you like and add it to your cart.<br>
2⃣ Open the cart and click "Place Order".<br>
3⃣ Enter your delivery address and contact information.<br>
4⃣ Choose a convenient payment method.<br>
5⃣ Review and accept the public offer agreement.<br>
6⃣ Complete the payment for your order.
</p>
</div>
<div class="faq-item">
<h3>Can I make changes to my order after purchase?</h3>
<p>If the seller has not yet shipped the product, contact our support — we will help adjust the order. However, once the product has been shipped, changes are not possible.</p>
</div>
<div class="faq-item">
<h3>How do I cancel an order?</h3>
<p>Contact us or the seller directly through order chats. As long as the package has not been shipped, cancellation is possible with a full refund.</p>
</div>
</section>
<section class="legal-section">
<h2>Payment 💳</h2>
<div class="faq-item">
<h3>What payment methods are available?</h3>
<p>We accept:</p>
<ul>
<li>Bank cards: Visa, MasterCard, MIR,</li>
<li>Fast Payment System (FPS),</li>
<li>Electronic wallets: YooMoney, QIWI,</li>
<li>Cash on delivery (if offered by the seller).</li>
</ul>
</div>
<div class="faq-item">
<h3>When will funds be charged from my card?</h3>
<p>The transaction is processed instantly, but the funds are temporarily held until you receive the product. Once you confirm receipt, the funds are released to the seller.</p>
</div>
<div class="faq-item">
<h3>Why might my payment be declined?</h3>
<p>Possible reasons for decline:</p>
<ul>
<li>Insufficient funds in the account,</li>
<li>Card transaction limit exceeded,</li>
<li>Card blocked by the bank,</li>
<li>Technical issues.</li>
</ul>
<p>We recommend contacting your bank for details.</p>
</div>
<div class="faq-item">
<h3>Will I receive a payment receipt?</h3>
<p>Yes, an electronic receipt will be sent to your specified email immediately after a successful payment in accordance with Federal Law No. 54-FZ.</p>
</div>
</section>
<section class="legal-section">
<h2>Delivery 🚚</h2>
<div class="faq-item">
<h3>Which delivery services are used?</h3>
<p><strong>Digital products:</strong> DexarMarket automatically sends digital products (licenses, keys, certificates) to your email or personal account instantly after payment.</p>
<p><strong>Physical products:</strong> The seller works independently with leading delivery services:</p>
<ul>
<li>CDEK,</li>
<li>Russian Post,</li>
<li>Boxberry,</li>
<li>DPD,</li>
<li>Yandex.Delivery.</li>
</ul>
<p>The delivery method for physical products is chosen based on the seller's preferences and destination.</p>
</div>
<div class="faq-item">
<h3>How much does delivery cost?</h3>
<p><strong>Digital products:</strong> Delivery is free — the product arrives to your email instantly.</p>
<p><strong>Physical products:</strong> The price is determined by the seller and depends on the weight, dimensions, chosen method, and delivery region. The final cost is displayed at checkout.</p>
</div>
<div class="faq-item">
<h3>What is the delivery time for my order?</h3>
<p><strong>Digital products:</strong> Instant delivery to email (within a few minutes after payment).</p>
<p><strong>Physical products</strong> (estimated times):</p>
<ul>
<li>CDEK: 27 business days,</li>
<li>Russian Post: 514 business days,</li>
<li>Boxberry: 25 business days,</li>
<li>DPD: 13 business days,</li>
<li>Yandex.Delivery: same day (if available in your city).</li>
</ul>
</div>
<div class="faq-item">
<h3>How do I track my package?</h3>
<p>You will receive a tracking code via email and can view the order status in your personal account. You can track the package on the official website of the chosen courier service.</p>
</div>
<div class="faq-item">
<h3>What should I do if the product arrived damaged? ⛑</h3>
<p>Inspect the product in front of the courier. If defects are found, fill out a refusal of acceptance report and immediately notify the seller and support service.</p>
</div>
</section>
<section class="legal-section">
<h2>Returns and Exchanges ✅</h2>
<div class="faq-item">
<h3>Can I return a product?</h3>
<p>Yes, the law allows you to return a quality product within 7 days of receipt. Defective items are returned under special rules.</p>
</div>
<div class="faq-item">
<h3>Which products cannot be returned?</h3>
<p>Products listed in Russian Government Decree No. 2463 cannot be returned: medications, cosmetics, underwear, activated digital products, and custom orders. See details in the <a [routerLink]="'/return-policy' | langRoute">"Return Policy"</a> section.</p>
</div>
<div class="faq-item">
<h3>How do I get a refund?</h3>
<p>
1. Notify the seller of your intention to return the product.<br>
2. Return the product in its original packaging.<br>
3. The seller will inspect the product and refund the amount using the same payment method (up to 30 days for the refund to process).
</p>
</div>
<div class="faq-item">
<h3>Who pays for return shipping?</h3>
<p>The buyer pays for return shipping of quality products. If a defect is found, the seller covers the costs.</p>
</div>
</section>
<section class="legal-section">
<h2>Warranty 🔧</h2>
<div class="faq-item">
<h3>Is there a warranty on products?</h3>
<p>Most of our products come with an official manufacturer's warranty. The period ranges from 12 to 36 months and is indicated on each product page.</p>
</div>
<div class="faq-item">
<h3>How do I use the warranty?</h3>
<p>Notify the seller of the defect, attaching photos of the damage and the warranty card. The seller will handle repair or replacement of the product.</p>
</div>
<div class="faq-item">
<h3>What is not covered by warranty?</h3>
<p>Mechanical damage, consequences of improper use, unauthorized repair, moisture exposure (if not protected), and natural wear — none of these are covered under warranty.</p>
</div>
</section>
<section class="legal-section">
<h2>Security and Privacy 🔐</h2>
<div class="faq-item">
<h3>How do you protect my personal data?</h3>
<p>We use modern SSL/TLS encryption methods, do not store card details, and comply with Federal Law No. 152-FZ on personal data protection. Details are available in our <a [routerLink]="'/privacy-policy' | langRoute">Privacy Policy</a>.</p>
</div>
<div class="faq-item">
<h3>Who has access to my data?</h3>
<p>Only your seller for order processing and delivery services. Data is not used by third parties for marketing without your permission.</p>
</div>
<div class="faq-item">
<h3>How do I delete my account?</h3>
<p>Send a request to our support service to delete your account. The account and personal information will be removed within one month.</p>
</div>
</section>
<section class="legal-section">
<h2>Information for Sellers 📋</h2>
<div class="faq-item">
<h3>How do I start selling on the platform?</h3>
<p>To join our sellers, contact our support service by email at <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a>.</p>
</div>
<div class="faq-item">
<h3>What is the platform's commission?</h3>
<p>The commission depends on the product type and sales volume. You can find out the exact terms during registration.</p>
</div>
<div class="faq-item">
<h3>When will I receive payment for a sale?</h3>
<p>The seller receives funds after the customer confirms receipt of the product or two weeks after delivery if the customer has not reported any issues.</p>
</div>
</section>
<section class="legal-section">
<h2>Customer Support 💬</h2>
<div class="faq-item">
<h3>How do I contact support?</h3>
<p>
✉️ <strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a><br>
📞 <strong>Phone (Russia):</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a><br>
📞 <strong>Phone (Armenia):</strong> <a href="tel:+37494861816">+374 94 86 18 16</a><br>
🏢 <strong>Office hours:</strong> 10:0019:00 (MSK)<br>
❄️ <strong>Technical support is available 24/7.</strong>
</p>
</div>
<div class="faq-item">
<h3>How long does it take to get a response?</h3>
<p>During business hours, you will receive a response within two hours. On holidays and weekends, delays of up to 24 hours are possible.</p>
</div>
</section>
<section class="legal-section">
<h2>Need Help?</h2>
<p>If you have any additional questions, please contact us at <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a> — we will promptly resolve any of your questions!</p>
</section>
</div>

View File

@@ -1,240 +1,244 @@
<h1>Հաճախ տրվող հարցեր (FAQ) 📌</h1>
<section class="legal-section">
<h2>Ընդհանուր հարցեր</h2>
<div class="faq-item">
<h3>Ինչ է DexarMarket-ը։</h3>
<p>DexarMarket-ը օնլայն հարթակ է, որտեղ անկախ վաճառողները տեղադրում են իրենց ապրանքներն ու ծառայությունները։ Մեր նպատակն է ապահովել հարմար ու անվտանգ միջավայր գնումների համար — մենք ինքներս ապրանքներ չենք արտադրում ու չենք վաճառում։</p>
<div class="legal-page">
<div class="legal-container">
<h1>Հաճախ տրվող հարցեր (FAQ) 📌</h1>
<section class="legal-section">
<h2>Ընդհանուր հարցեր</h2>
<div class="faq-item">
<h3>Ինչ է DexarMarket-ը։</h3>
<p>DexarMarket-ը օնլայն հարթակ է, որտեղ անկախ վաճառողները տեղադրում են իրենց ապրանքներն ու ծառայությունները։ Մեր նպատակն է ապահովել հարմար ու անվտանգ միջավայր գնումների համար — մենք ինքներս ապրանքներ չենք արտադրում ու չենք վաճառում։</p>
</div>
<div class="faq-item">
<h3>Ինչպես գրանցվել հարթակում։</h3>
<p>Գրանցվելը հեշտ է՝ բացեք մեր հավելվածը Telegram-ում։ Ձեր պրոֆիլը կստեղծվի ավտոմատ կերպով մուտք գործելուց հետո։</p>
</div>
<div class="faq-item">
<h3>Անվտանգ է գնել կայքում։</h3>
<p>Անպայմանորեն՝ Բոլոր գործարքները պաշտպանված են ժամանակակից գաղտնագրման տեխնոլոգիաներով (PCI DSS և 3D Secure)։ Ձեր բանկային տվյալները պահվում են ապահով կերպով՝ անջատորեն մեր համակարգից։</p>
</div>
</section>
<section class="legal-section">
<h2>Պատվերի ձևակերպում</h2>
<div class="faq-item">
<h3>Ինչպես կատարել գնում։</h3>
<p>
1⃣ Գտեք ձեզհամած ապրանքը և ավելացրեք զամբյուղին։<br>
2⃣ Բացեք զամբյուղը և սեղմեք «Ձևակերպել պատվերը»։<br>
3⃣ Նշեք առաքման հասցեն և կապի տեղեկատվությունը։<br>
4⃣ Ընտրեք հարմար վճարման եղանակ։<br>
5⃣ Ծանոթացեք և հաստատեք հանրային օֆերտան։<br>
6⃣ Ավարտեք վճարումը։
</p>
</div>
<div class="faq-item">
<h3>Կարելի։ է փոփոխություններ կատարել գնումից հետո։</h3>
<p>Եթե վաճառողը դեռ չի ուղարկել ապրանքը, գրեք մեզ աջակցության ծառայություն — մենք կօգնենք ճշտել պատվերը։ Սակայն ապրանքը ուղարկելուց հետո փոփոխություն հնարավոր չէ։</p>
</div>
<div class="faq-item">
<h3>Ինչպես հրաժարվել պատվերից։</h3>
<p>Կապվեք մեզ հետ կամ անմիջապես վաճառողի հետ պատվերների չատերով։ Մինչև ծանրը չի ուղարկվել, հնարավոր է հրաժարվել լիովին գումարի վերադարձով։</p>
</div>
</section>
<section class="legal-section">
<h2>Վճարում 💳</h2>
<div class="faq-item">
<h3>Ինչ վճարման եղանակներ կան։</h3>
<p>Ընդունվում են՝</p>
<ul>
<li>Visa, MasterCard, ՄԻՌ բանկային քարտեր,</li>
<li>Արագ վճարման համակարգ (ՍԲՊ),</li>
<li>Ելեկտրոնային դրամապանակներ՝ YooMoney, QIWI,</li>
<li>Կանխիկ՝ ստանալուց (եթե վաճառողի կողմից նախատեսված է)։</li>
</ul>
</div>
<div class="faq-item">
<h3>Երբ կգրանցվեն գումարը քարտից։</h3>
<p>Գործարքը կատարվում է անմիջապես, սակայն գումարը ժամանակավորապես սառեցվում է մինչև ապրանքը ստանանք։ Ստացումը հաստատելուց հետո գումարը ֆոխանցվում է վաճառողին։</p>
</div>
<div class="faq-item">
<h3>Ինչու կարող է վճարումս մերժվել։</h3>
<p>Մերժման հնարավոր պատճառները՝</p>
<ul>
<li>Հաշվին անբավարար միջոցներ,</li>
<li>Քարտի գործառնությունների սահմանափակման գերազանցում,</li>
<li>Քարտը արգելափակված է բանկի կողմից,</li>
<li>Տեխնիկական խնդիրներ։</li>
</ul>
<p>Խորհուրդ ենք տալիս դիմել բանկ մանրամասների ճշտելման համար։</p>
</div>
<div class="faq-item">
<h3>Կստանամ վճարման անդրութ։</h3>
<p>Այո, էլեկտրոնային անդրագիրը կուգա ձեր նշված էլ. հասցեին հաջողված վճարմանից անմիջապես հետո՝ համաձայն Վ դաշնային օրենքի № 54-ԿԶ-ի պահանջներին։</p>
</div>
</section>
<section class="legal-section">
<h2>Առաքում 🚚</h2>
<div class="faq-item">
<h3>Ինչ առաքման ծառայություններ կան։</h3>
<p><strong>Թվային ապրանքներ՝</strong> DexarMarket հարթակն ավտոմատ կերպով ուղարկում է թվային ապրանքները (լիցենզիաներ, բանալիներ, վկայագրեր) ձեր էլ. հասցեին կամ անձնական հաշվին՝ վճարմանից անմիջապես հետո։</p>
<p><strong>Նյութական ապրանքներ՝</strong> Վաճառողն ինքնուրույն աշխատում է առաքման ծառայությունների հետ՝</p>
<ul>
<li>ՍԴԷԿ,</li>
<li>Ռուսաստանի փոստ,</li>
<li>Boxberry,</li>
<li>DPD,</li>
<li>Yandex.Առաքում։</li>
</ul>
<p>Նյութական ապրանքների առաքման եղանակը ընտրվում է վաճառողի նախապատվություններին և նպատակակետին համապատասխան։</p>
</div>
<div class="faq-item">
<h3>Ինչքան արժե առաքումը։</h3>
<p><strong>Թվային ապրանքներ՝</strong> Առաքումն անվճար է — ապրանքը կհասնի էլ. հասցեին անմիջապես։</p>
<p><strong>Նյութական ապրանքներ՝</strong> Գինը որոշվում է վաճառողի կողմից և կախված է ապրանքի քաշից, չափսերից, առաքման եղանակից և տարածաշրջանից։ Վերջնական արժեքը ցուցադրվում է պատվերի ձևակերպման ժամանակ։</p>
</div>
<div class="faq-item">
<h3>Ինչքան ժամանակ է առաքումը։</h3>
<p><strong>Թվային ապրանքներ՝</strong> Ակնթարծիկ առաքում էլ. հասցեին (վճարումից հետո մի քանի րոպեի ընթացքում)։</p>
<p><strong>Նյութական ապրանքներ</strong> (մոտավոր ժամկետներ)՝</p>
<ul>
<li>ՍԴԷԿ՝ 27 աշխատանքային օր,</li>
<li>Ռուսաստանի փոստ՝ 514 աշխատանքային օր,</li>
<li>Boxberry՝ 25 աշխատանքային օր,</li>
<li>DPD՝ 13 աշխատանքային օր,</li>
<li>Yandex.Առաքում՝ նույն օրը (եթե ձեր քաղաքում հասանելի է)։</li>
</ul>
</div>
<div class="faq-item">
<h3>Ինչպես հետևել ծանրուկը։</h3>
<p>Դուք կստանաք հետևման կոդը ձեր էլ. հասցեին և կկարողանաք տեսնել պատվերի կարգավիճակը անձնական հաշվում։ Հետևել ծանրուկը կարելի է ընտրված սուրհանդակային ծառայության պաշտոնական կայքում։</p>
</div>
<div class="faq-item">
<h3>Ինչ անել, եթե ապրանքը եկել է վնասված։ ⛑</h3>
<p>Ստուգեք ապրանքը սուրհանդակի ներկայությամբ։ Թերություններ հայտնաբերելու դեպքում կազմեք ընդունման մերժման ակտ և անհապաղ տեղեկացրեք վաճառողին և աջակցության ծառայությանը։</p>
</div>
</section>
<section class="legal-section">
<h2>Վերադարձ և փոխանակում ✅</h2>
<div class="faq-item">
<h3>Կարելի։ է վերադարձնել ապրանքը։</h3>
<p>Այո, օրենքը թույլ է տալիս վերադարձնել որակյալ ապրանքը 7 օրվա ընթացքում։ Թերությունով ապրանքները վերադարձվում են հատուկ կանոններով։</p>
</div>
<div class="faq-item">
<h3>Ինչ ապրանքներ հնարավոր չէ վերադարձնել։</h3>
<p>ՌԴ Կառավարության որոշման № 2463-ով նշված ապրանքները հնարավոր չէ վերադարձնել՝ դեղամիջոցներ, կոսմետիկա, ներքնակի սպիտակեղեն, ակտիվացված թվային ապրանքներ և անհատական պատվերներ։ Մանրամասները տեսեք <a [routerLink]="'/return-policy' | langRoute">«Վերադարձի քաղաքականություն»</a> բաժնում։</p>
</div>
<div class="faq-item">
<h3>Ինչպես վերադարձնել գումարը։</h3>
<p>
1. Տեղեկացրեք վաճառողին ապրանքը վերադարձնելու մասին։<br>
2. Վերադարձրեք ապրանքը սկզբնական փաթեթավորման մեջ։<br>
3. Վաճառողը կստուգի ապրանքի վիճակը և կվերադարձնի գումարը նույն վճարման եղանակով (մինչև 30 օր սպասման ժամկետ)։
</p>
</div>
<div class="faq-item">
<h3>Ով կվճարի հետադարձի առաքումը։</h3>
<p>Գնորդը վճարում է որակյալ ապրանքի հետադարձի առաքումը։ Թերություն հայտնաբերելու դեպքում ծախսերը կրում է վաճառողը։</p>
</div>
</section>
<section class="legal-section">
<h2>Երաշխիք 🔧</h2>
<div class="faq-item">
<h3>Կա։ երաշխիք ապրանքների համար։</h3>
<p>Մեր ապրանքների մեծ մասը ունի արտադրողի պաշտոնական երաշխիք։ Պայմանագիրը տատանվում է 12-ից 36 ամիս և նշված է յուրաքանչյուր ապրանքի էջում։</p>
</div>
<div class="faq-item">
<h3>Ինչպես օգտվել երաշխիքով։</h3>
<p>Տեղեկացրեք վաճառողին թերության մասին՝ կցելով թերությունների լուսանկարներ և երաշխիքային տալոն։ Վաճառողը կզբաղվի վերանորոգմամբ կամ ապրանքի փոխարինումով։</p>
</div>
<div class="faq-item">
<h3>Ինչը չի ընդգրկվում երաշխիքով։</h3>
<p>Մեխանիկական վնասվածքներ, ոչ պատշաճ շահագործման հետևանքներ, ինքնակամ վերանորոգում, խոնավի ազդեցություն (եթե պաշտպանություն չկա), բնական մաշվածում — այս ամենը չի ընդգրկվում երաշխիքով։</p>
</div>
</section>
<section class="legal-section">
<h2>Անվտանգություն և գաղտնիություն 🔐</h2>
<div class="faq-item">
<h3>Ինչպես եք պաշտպանում իմ անձնական տվյալները։</h3>
<p>Մենք օգտագործում ենք SSL/TLS գաղտնագրման ժամանակակից մեթոդներ, չենք պահում քարտերի տվյալները և կատարում ենք № 152-ԿԶ դաշնային օրենքի պահանջները։ Մանրամասները — <a [routerLink]="'/privacy-policy' | langRoute">Գաղտնիության քաղաքականություն</a>ում։</p>
</div>
<div class="faq-item">
<h3>Ում է տրամադրվում իմ տվյալները։</h3>
<p>Միայն ձեր վաճառողին՝ պատվերի մշակման և առաքման ծառայություններին։ Տվյալները չեն օգտագործվում երրորդ կողմերի կողմից շուկայական նպատակներով առանց ձեր թույլտվության։</p>
</div>
<div class="faq-item">
<h3>Ինչպես ջնջել հաշվիս։</h3>
<p>Գրեք մեզ աջակցության ծառայություն՝ հաշվի ջնջման հայտով։ Հաշիվը կլիքվիդացվի անձնական տեղեկատվությամբ մեկ ամսվա ընթացքում։</p>
</div>
</section>
<section class="legal-section">
<h2>Տեղեկատվություն վաճառողների համար 📋</h2>
<div class="faq-item">
<h3>Ինչպես սկսել վաճառել հարթակում։</h3>
<p>Մեր վաճառողներին միանալու համար դիմեք աջակցության ծառայությանը՝ էլեկտրոնային փոստով <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a>։</p>
</div>
<div class="faq-item">
<h3>Ինչքան է հարթակի միջնորդավճարը։</h3>
<p>Միջնորդավճարը կախված է ապրանքի տեսակից և վաճառքի ծավալից։ Ճիշտ պայմանները կարող եք իմանալ գրանցվելուց։</p>
</div>
<div class="faq-item">
<h3>Երբ կստանամ վաճառքի գումարը։</h3>
<p>Վաճառողը ստանում է գումարը հաճախորդի կողմից ապրանքի ստացումը հաստատելուց հետո կամ առաքմանից երկու շաբաթ հետո, եթե հաճախորդը խնդիրներ չի նշել։</p>
</div>
</section>
<section class="legal-section">
<h2>Աջակցության ծառայություն 💬</h2>
<div class="faq-item">
<h3>Ինչպես կապվել աջակցության հետ։</h3>
<p>
✉️ <strong>Էլ. հասցե՝</strong> <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a><br>
📞 <strong>Հեռախոս (Ռուսաստան)՝</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a><br>
📞 <strong>Հեռախոս (Հայաստան)՝</strong> <a href="tel:+37494861816">+374 94 86 18 16</a><br>
🏢 <strong>Գրասենյակի աշխատանքային ժամերը՝</strong> 10:00—19:00 (ՄՍԿ)<br>
❄️ <strong>Տեխնիկական աջակցությունը հասանելի է 24/7։</strong>
</p>
</div>
<div class="faq-item">
<h3>Ինչքան սպասել պատասխան։</h3>
<p>Աշխատանքային օրերին պատասխանը գալիս է երկու ժամվա ընթացքում։ Տոներին և հանգստյան օրերին հնարավոր են ուշացումներ մինչև մեկ օր։</p>
</div>
</section>
<section class="legal-section">
<h2>Օգնություն է հարկավոր։</h2>
<p>Եթե լրացուցիչ հարցեր ունեք, դիմեք <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a> — մենք արագորեն կլուծենք ձեր հարցերը։</p>
</section>
</div>
<div class="faq-item">
<h3>Ինչպես գրանցվել հարթակում։</h3>
<p>Գրանցվելը հեշտ է՝ բացեք մեր հավելվածը Telegram-ում։ Ձեր պրոֆիլը կստեղծվի ավտոմատ կերպով մուտք գործելուց հետո։</p>
</div>
<div class="faq-item">
<h3>Անվտանգ է գնել կայքում։</h3>
<p>Անպայմանորեն՝ Բոլոր գործարքները պաշտպանված են ժամանակակից գաղտնագրման տեխնոլոգիաներով (PCI DSS և 3D Secure)։ Ձեր բանկային տվյալները պահվում են ապահով կերպով՝ անջատորեն մեր համակարգից։</p>
</div>
</section>
<section class="legal-section">
<h2>Պատվերի ձևակերպում</h2>
<div class="faq-item">
<h3>Ինչպես կատարել գնում։</h3>
<p>
1⃣ Գտեք ձեզհամած ապրանքը և ավելացրեք զամբյուղին։<br>
2⃣ Բացեք զամբյուղը և սեղմեք «Ձևակերպել պատվերը»։<br>
3⃣ Նշեք առաքման հասցեն և կապի տեղեկատվությունը։<br>
4⃣ Ընտրեք հարմար վճարման եղանակ։<br>
5⃣ Ծանոթացեք և հաստատեք հանրային օֆերտան։<br>
6⃣ Ավարտեք վճարումը։
</p>
</div>
<div class="faq-item">
<h3>Կարելի։ է փոփոխություններ կատարել գնումից հետո։</h3>
<p>Եթե վաճառողը դեռ չի ուղարկել ապրանքը, գրեք մեզ աջակցության ծառայություն — մենք կօգնենք ճշտել պատվերը։ Սակայն ապրանքը ուղարկելուց հետո փոփոխություն հնարավոր չէ։</p>
</div>
<div class="faq-item">
<h3>Ինչպես հրաժարվել պատվերից։</h3>
<p>Կապվեք մեզ հետ կամ անմիջապես վաճառողի հետ պատվերների չատերով։ Մինչև ծանրը չի ուղարկվել, հնարավոր է հրաժարվել լիովին գումարի վերադարձով։</p>
</div>
</section>
<section class="legal-section">
<h2>Վճարում 💳</h2>
<div class="faq-item">
<h3>Ինչ վճարման եղանակներ կան։</h3>
<p>Ընդունվում են՝</p>
<ul>
<li>Visa, MasterCard, ՄԻՌ բանկային քարտեր,</li>
<li>Արագ վճարման համակարգ (ՍԲՊ),</li>
<li>Ելեկտրոնային դրամապանակներ՝ YooMoney, QIWI,</li>
<li>Կանխիկ՝ ստանալուց (եթե վաճառողի կողմից նախատեսված է)։</li>
</ul>
</div>
<div class="faq-item">
<h3>Երբ կգրանցվեն գումարը քարտից։</h3>
<p>Գործարքը կատարվում է անմիջապես, սակայն գումարը ժամանակավորապես սառեցվում է մինչև ապրանքը ստանանք։ Ստացումը հաստատելուց հետո գումարը ֆոխանցվում է վաճառողին։</p>
</div>
<div class="faq-item">
<h3>Ինչու կարող է վճարումս մերժվել։</h3>
<p>Մերժման հնարավոր պատճառները՝</p>
<ul>
<li>Հաշվին անբավարար միջոցներ,</li>
<li>Քարտի գործառնությունների սահմանափակման գերազանցում,</li>
<li>Քարտը արգելափակված է բանկի կողմից,</li>
<li>Տեխնիկական խնդիրներ։</li>
</ul>
<p>Խորհուրդ ենք տալիս դիմել բանկ մանրամասների ճշտելման համար։</p>
</div>
<div class="faq-item">
<h3>Կստանամ վճարման անդրութ։</h3>
<p>Այո, էլեկտրոնային անդրագիրը կուգա ձեր նշված էլ. հասցեին հաջողված վճարմանից անմիջապես հետո՝ համաձայն Վ դաշնային օրենքի № 54-ԿԶ-ի պահանջներին։</p>
</div>
</section>
<section class="legal-section">
<h2>Առաքում 🚚</h2>
<div class="faq-item">
<h3>Ինչ առաքման ծառայություններ կան։</h3>
<p><strong>Թվային ապրանքներ՝</strong> DexarMarket հարթակն ավտոմատ կերպով ուղարկում է թվային ապրանքները (լիցենզիաներ, բանալիներ, վկայագրեր) ձեր էլ. հասցեին կամ անձնական հաշվին՝ վճարմանից անմիջապես հետո։</p>
<p><strong>Նյութական ապրանքներ՝</strong> Վաճառողն ինքնուրույն աշխատում է առաքման ծառայությունների հետ՝</p>
<ul>
<li>ՍԴԷԿ,</li>
<li>Ռուսաստանի փոստ,</li>
<li>Boxberry,</li>
<li>DPD,</li>
<li>Yandex.Առաքում։</li>
</ul>
<p>Նյութական ապրանքների առաքման եղանակը ընտրվում է վաճառողի նախապատվություններին և նպատակակետին համապատասխան։</p>
</div>
<div class="faq-item">
<h3>Ինչքան արժե առաքումը։</h3>
<p><strong>Թվային ապրանքներ՝</strong> Առաքումն անվճար է — ապրանքը կհասնի էլ. հասցեին անմիջապես։</p>
<p><strong>Նյութական ապրանքներ՝</strong> Գինը որոշվում է վաճառողի կողմից և կախված է ապրանքի քաշից, չափսերից, առաքման եղանակից և տարածաշրջանից։ Վերջնական արժեքը ցուցադրվում է պատվերի ձևակերպման ժամանակ։</p>
</div>
<div class="faq-item">
<h3>Ինչքան ժամանակ է առաքումը։</h3>
<p><strong>Թվային ապրանքներ՝</strong> Ակնթարծիկ առաքում էլ. հասցեին (վճարումից հետո մի քանի րոպեի ընթացքում)։</p>
<p><strong>Նյութական ապրանքներ</strong> (մոտավոր ժամկետներ)՝</p>
<ul>
<li>ՍԴԷԿ՝ 27 աշխատանքային օր,</li>
<li>Ռուսաստանի փոստ՝ 514 աշխատանքային օր,</li>
<li>Boxberry՝ 25 աշխատանքային օր,</li>
<li>DPD՝ 13 աշխատանքային օր,</li>
<li>Yandex.Առաքում՝ նույն օրը (եթե ձեր քաղաքում հասանելի է)։</li>
</ul>
</div>
<div class="faq-item">
<h3>Ինչպես հետևել ծանրուկը։</h3>
<p>Դուք կստանաք հետևման կոդը ձեր էլ. հասցեին և կկարողանաք տեսնել պատվերի կարգավիճակը անձնական հաշվում։ Հետևել ծանրուկը կարելի է ընտրված սուրհանդակային ծառայության պաշտոնական կայքում։</p>
</div>
<div class="faq-item">
<h3>Ինչ անել, եթե ապրանքը եկել է վնասված։ ⛑</h3>
<p>Ստուգեք ապրանքը սուրհանդակի ներկայությամբ։ Թերություններ հայտնաբերելու դեպքում կազմեք ընդունման մերժման ակտ և անհապաղ տեղեկացրեք վաճառողին և աջակցության ծառայությանը։</p>
</div>
</section>
<section class="legal-section">
<h2>Վերադարձ և փոխանակում ✅</h2>
<div class="faq-item">
<h3>Կարելի։ է վերադարձնել ապրանքը։</h3>
<p>Այո, օրենքը թույլ է տալիս վերադարձնել որակյալ ապրանքը 7 օրվա ընթացքում։ Թերությունով ապրանքները վերադարձվում են հատուկ կանոններով։</p>
</div>
<div class="faq-item">
<h3>Ինչ ապրանքներ հնարավոր չէ վերադարձնել։</h3>
<p>ՌԴ Կառավարության որոշման № 2463-ով նշված ապրանքները հնարավոր չէ վերադարձնել՝ դեղամիջոցներ, կոսմետիկա, ներքնակի սպիտակեղեն, ակտիվացված թվային ապրանքներ և անհատական պատվերներ։ Մանրամասները տեսեք <a [routerLink]="'/return-policy' | langRoute">«Վերադարձի քաղաքականություն»</a> բաժնում։</p>
</div>
<div class="faq-item">
<h3>Ինչպես վերադարձնել գումարը։</h3>
<p>
1. Տեղեկացրեք վաճառողին ապրանքը վերադարձնելու մասին։<br>
2. Վերադարձրեք ապրանքը սկզբնական փաթեթավորման մեջ։<br>
3. Վաճառողը կստուգի ապրանքի վիճակը և կվերադարձնի գումարը նույն վճարման եղանակով (մինչև 30 օր սպասման ժամկետ)։
</p>
</div>
<div class="faq-item">
<h3>Ով կվճարի հետադարձի առաքումը։</h3>
<p>Գնորդը վճարում է որակյալ ապրանքի հետադարձի առաքումը։ Թերություն հայտնաբերելու դեպքում ծախսերը կրում է վաճառողը։</p>
</div>
</section>
<section class="legal-section">
<h2>Երաշխիք 🔧</h2>
<div class="faq-item">
<h3>Կա։ երաշխիք ապրանքների համար։</h3>
<p>Մեր ապրանքների մեծ մասը ունի արտադրողի պաշտոնական երաշխիք։ Պայմանագիրը տատանվում է 12-ից 36 ամիս և նշված է յուրաքանչյուր ապրանքի էջում։</p>
</div>
<div class="faq-item">
<h3>Ինչպես օգտվել երաշխիքով։</h3>
<p>Տեղեկացրեք վաճառողին թերության մասին՝ կցելով թերությունների լուսանկարներ և երաշխիքային տալոն։ Վաճառողը կզբաղվի վերանորոգմամբ կամ ապրանքի փոխարինումով։</p>
</div>
<div class="faq-item">
<h3>Ինչը չի ընդգրկվում երաշխիքով։</h3>
<p>Մեխանիկական վնասվածքներ, ոչ պատշաճ շահագործման հետևանքներ, ինքնակամ վերանորոգում, խոնավի ազդեցություն (եթե պաշտպանություն չկա), բնական մաշվածում — այս ամենը չի ընդգրկվում երաշխիքով։</p>
</div>
</section>
<section class="legal-section">
<h2>Անվտանգություն և գաղտնիություն 🔐</h2>
<div class="faq-item">
<h3>Ինչպես եք պաշտպանում իմ անձնական տվյալները։</h3>
<p>Մենք օգտագործում ենք SSL/TLS գաղտնագրման ժամանակակից մեթոդներ, չենք պահում քարտերի տվյալները և կատարում ենք № 152-ԿԶ դաշնային օրենքի պահանջները։ Մանրամասները — <a [routerLink]="'/privacy-policy' | langRoute">Գաղտնիության քաղաքականություն</a>ում։</p>
</div>
<div class="faq-item">
<h3>Ում է տրամադրվում իմ տվյալները։</h3>
<p>Միայն ձեր վաճառողին՝ պատվերի մշակման և առաքման ծառայություններին։ Տվյալները չեն օգտագործվում երրորդ կողմերի կողմից շուկայական նպատակներով առանց ձեր թույլտվության։</p>
</div>
<div class="faq-item">
<h3>Ինչպես ջնջել հաշվիս։</h3>
<p>Գրեք մեզ աջակցության ծառայություն՝ հաշվի ջնջման հայտով։ Հաշիվը կլիքվիդացվի անձնական տեղեկատվությամբ մեկ ամսվա ընթացքում։</p>
</div>
</section>
<section class="legal-section">
<h2>Տեղեկատվություն վաճառողների համար 📋</h2>
<div class="faq-item">
<h3>Ինչպես սկսել վաճառել հարթակում։</h3>
<p>Մեր վաճառողներին միանալու համար դիմեք աջակցության ծառայությանը՝ էլեկտրոնային փոստով <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a>։</p>
</div>
<div class="faq-item">
<h3>Ինչքան է հարթակի միջնորդավճարը։</h3>
<p>Միջնորդավճարը կախված է ապրանքի տեսակից և վաճառքի ծավալից։ Ճիշտ պայմանները կարող եք իմանալ գրանցվելուց։</p>
</div>
<div class="faq-item">
<h3>Երբ կստանամ վաճառքի գումարը։</h3>
<p>Վաճառողը ստանում է գումարը հաճախորդի կողմից ապրանքի ստացումը հաստատելուց հետո կամ առաքմանից երկու շաբաթ հետո, եթե հաճախորդը խնդիրներ չի նշել։</p>
</div>
</section>
<section class="legal-section">
<h2>Աջակցության ծառայություն 💬</h2>
<div class="faq-item">
<h3>Ինչպես կապվել աջակցության հետ։</h3>
<p>
✉️ <strong>Էլ. հասցե՝</strong> <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a><br>
📞 <strong>Հեռախոս (Ռուսաստան)՝</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a><br>
📞 <strong>Հեռախոս (Հայաստան)՝</strong> <a href="tel:+37494861816">+374 94 86 18 16</a><br>
🏢 <strong>Գրասենյակի աշխատանքային ժամերը՝</strong> 10:00—19:00 (ՄՍԿ)<br>
❄️ <strong>Տեխնիկական աջակցությունը հասանելի է 24/7։</strong>
</p>
</div>
<div class="faq-item">
<h3>Ինչքան սպասել պատասխան։</h3>
<p>Աշխատանքային օրերին պատասխանը գալիս է երկու ժամվա ընթացքում։ Տոներին և հանգստյան օրերին հնարավոր են ուշացումներ մինչև մեկ օր։</p>
</div>
</section>
<section class="legal-section">
<h2>Օգնություն է հարկավոր։</h2>
<p>Եթե լրացուցիչ հարցեր ունեք, դիմեք <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a> — մենք արագորեն կլուծենք ձեր հարցերը։</p>
</section>
</div>

View File

@@ -1,240 +1,244 @@
<h1>Часто задаваемые вопросы (FAQ) 📌</h1>
<section class="legal-section">
<h2>Общие вопросы</h2>
<div class="faq-item">
<h3>Что такое DexarMarket?</h3>
<p>DexarMarket — это онлайн-площадка, где независимые продавцы размещают свои товары и услуги. Наша задача — обеспечить удобную и безопасную среду для совершения покупок, мы сами не производим и не продаем товары.</p>
<div class="legal-page">
<div class="legal-container">
<h1>Часто задаваемые вопросы (FAQ) 📌</h1>
<section class="legal-section">
<h2>Общие вопросы</h2>
<div class="faq-item">
<h3>Что такое DexarMarket?</h3>
<p>DexarMarket — это онлайн-площадка, где независимые продавцы размещают свои товары и услуги. Наша задача — обеспечить удобную и безопасную среду для совершения покупок, мы сами не производим и не продаем товары.</p>
</div>
<div class="faq-item">
<h3>Как зарегистрироваться на платформе?</h3>
<p>Зарегистрироваться легко: откройте наше приложение внутри Telegram. Ваш профиль будет создан автоматически сразу же после входа.</p>
</div>
<div class="faq-item">
<h3>Безопасно ли покупать на сайте?</h3>
<p>Абсолютно да! Все транзакции защищены современными технологиями шифрования (PCI DSS и 3D Secure). Ваши банковские данные надежно хранятся отдельно от нашей системы.</p>
</div>
</section>
<section class="legal-section">
<h2>Оформление заказа</h2>
<div class="faq-item">
<h3>Как правильно оформить покупку?</h3>
<p>
1⃣ Найдите понравившийся товар и добавьте его в корзину.<br>
2⃣ Открыв корзину, кликните «Оформить заказ».<br>
3⃣ Укажите адрес доставки и контактную информацию.<br>
4⃣ Выберите удобный способ оплаты.<br>
5⃣ Ознакомьтесь и подтвердите публичную оферту.<br>
6⃣ Завершите оплату вашего заказа.
</p>
</div>
<div class="faq-item">
<h3>Можно ли внести изменения в заказ после завершения покупки?</h3>
<p>Если продавец ещё не отправил товар, напишите нам в поддержку — мы поможем скорректировать заказ. Но после отправки товара изменение невозможно.</p>
</div>
<div class="faq-item">
<h3>Как отказаться от заказа?</h3>
<p>Свяжитесь с нами или непосредственно с продавцом через чаты заказов. Пока посылка не отправлена, отказ возможен с полным возвратом денег.</p>
</div>
</section>
<section class="legal-section">
<h2>Оплата 💳</h2>
<div class="faq-item">
<h3>Какие способы оплаты доступны?</h3>
<p>Принимаются:</p>
<ul>
<li>Банковские карты Visa, MasterCard, МИР,</li>
<li>Система быстрых платежей (СБП),</li>
<li>Электронные кошельки ЮMoney, QIWI,</li>
<li>Наличные при получении (если предусмотрено продавцом).</li>
</ul>
</div>
<div class="faq-item">
<h3>Когда спишутся средства с моей карты?</h3>
<p>Операция проходит мгновенно, но деньги временно замораживаются до момента получения вами товара. Как только вы подтверждаете получение, средства отправляются продавцу.</p>
</div>
<div class="faq-item">
<h3>Почему мой платёж мог быть отклонён?</h3>
<p>Возможные причины отказа:</p>
<ul>
<li>Недостаточно средств на счёте,</li>
<li>Превышение лимита операций по карте,</li>
<li>Карта заблокирована банком,</li>
<li>Технические сбои.</li>
</ul>
<p>Рекомендуем обратиться в банк для выяснения деталей.</p>
</div>
<div class="faq-item">
<h3>Получу ли я чек об оплате?</h3>
<p>Да, электронная квитанция придёт на указанный вами e-mail сразу после успешного платежа согласно закону № 54-ФЗ.</p>
</div>
</section>
<section class="legal-section">
<h2>Доставка 🚚</h2>
<div class="faq-item">
<h3>Через какие службы осуществляется доставка?</h3>
<p><strong>Цифровые товары:</strong> Платформа DexarMarket автоматически отправляет цифровые товары (лицензии, ключи, сертификаты) на вашу электронную почту или в личный кабинет мгновенно после оплаты.</p>
<p><strong>Материальные товары:</strong> Продавец самостоятельно работает с ведущими службами доставки:</p>
<ul>
<li>СДЭК,</li>
<li>Почта России,</li>
<li>Boxberry,</li>
<li>DPD,</li>
<li>Яндекс.Доставка.</li>
</ul>
<p>Способ доставки материальных товаров выбирается исходя из предпочтений продавца и места назначения.</p>
</div>
<div class="faq-item">
<h3>Сколько стоит доставка моего заказа?</h3>
<p><strong>Цифровые товары:</strong> Доставка бесплатная — товар придёт на email мгновенно.</p>
<p><strong>Материальные товары:</strong> Цена определяется продавцом и зависит от веса, габаритов товара, выбранного способа и региона доставки. Окончательная стоимость видна при оформлении заказа.</p>
</div>
<div class="faq-item">
<h3>Какой срок доставки моего заказа?</h3>
<p><strong>Цифровые товары:</strong> Мгновенная доставка на email (в течение нескольких минут после оплаты).</p>
<p><strong>Материальные товары</strong> (примерные сроки):</p>
<ul>
<li>СДЭК: 27 рабочих дней,</li>
<li>Почта России: 514 рабочих дней,</li>
<li>Boxberry: 25 рабочих дней,</li>
<li>DPD: 13 рабочих дня,</li>
<li>Яндекс.Доставка: в тот же день (при наличии возможности в вашем городе).</li>
</ul>
</div>
<div class="faq-item">
<h3>Как отслеживать мою посылку?</h3>
<p>Вы получите трек-код на свою электронную почту и сможете увидеть статус заказа в личном кабинете. Отслеживать посылку можно на официальном сайте выбранной курьерской службы.</p>
</div>
<div class="faq-item">
<h3>Что делать, если товар пришёл повреждённым? ⛑</h3>
<p>Осмотрите товар прямо при курьере. Если обнаружились дефекты — оформляйте акт отказа от приёмки и незамедлительно сообщайте продавцу и службе поддержки.</p>
</div>
</section>
<section class="legal-section">
<h2>Возврат и обмен ✅</h2>
<div class="faq-item">
<h3>Можно ли вернуть товар?</h3>
<p>Да, закон позволяет вернуть качественный товар в течение 7 дней после получения. Бракованные изделия возвращаются по особым правилам.</p>
</div>
<div class="faq-item">
<h3>Какие товары вернуть нельзя?</h3>
<p>Нельзя вернуть товары, перечисленные в Постановлении Правительства РФ №2463: медикаменты, косметику, бельё, активированные цифровые продукты и индивидуальные заказы. Подробности смотрите в разделе <a [routerLink]="'/return-policy' | langRoute">«Политика возврата»</a>.</p>
</div>
<div class="faq-item">
<h3>Как вернуть деньги?</h3>
<p>
1. Сообщите продавцу о намерении вернуть товар.<br>
2. Верните товар в оригинальной упаковке.<br>
3. Продавец проверит состояние товара и вернёт средства таким же способом, каким была произведена покупка (до 30 дней ожидания возврата).
</p>
</div>
<div class="faq-item">
<h3>Кто оплатит обратный путь?</h3>
<p>Покупатель оплачивает обратную доставку качественного товара. Если обнаружен брак — расходы несёт продавец.</p>
</div>
</section>
<section class="legal-section">
<h2>Гарантия 🔧</h2>
<div class="faq-item">
<h3>Есть ли гарантия на товары?</h3>
<p>Большинство наших товаров имеют официальную гарантию производителя. Срок варьируется от 12 до 36 месяцев и указывается в карточке каждого продукта.</p>
</div>
<div class="faq-item">
<h3>Как воспользоваться гарантией?</h3>
<p>Сообщите продавцу о неисправности, приложив фотографии дефектов и гарантийный талон. Продавец займётся ремонтом или заменой товара.</p>
</div>
<div class="faq-item">
<h3>Что не покрыто гарантией?</h3>
<p>Механические повреждения, последствия неправильной эксплуатации, самостоятельный ремонт, влияние влаги (если нет защиты), естественный износ — всё это не является страховым случаем.</p>
</div>
</section>
<section class="legal-section">
<h2>Безопасность и конфиденциальность 🔐</h2>
<div class="faq-item">
<h3>Как вы защищаете мои личные данные?</h3>
<p>Используем современные методы шифрования SSL/TLS, не храним реквизиты карт, выполняем требования закона №152-ФЗ о защите персональной информации. Подробности — в <a [routerLink]="'/privacy-policy' | langRoute">Политике конфиденциальности</a>.</p>
</div>
<div class="faq-item">
<h3>Кому предоставляются мои данные?</h3>
<p>Только вашему продавцу для обработки заказа и службам доставки. Данные не используются третьими лицами для маркетинга без вашего разрешения.</p>
</div>
<div class="faq-item">
<h3>Как удалить свой аккаунт?</h3>
<p>Напишите нам в службу поддержки запрос на удаление учётной записи. Аккаунт будет ликвидирован вместе с личной информацией в течение месяца.</p>
</div>
</section>
<section class="legal-section">
<h2>Информация для продавцов 📋</h2>
<div class="faq-item">
<h3>Как начать продавать на площадке?</h3>
<p>Чтобы присоединиться к нашим продавцам, обратитесь в службу поддержки по электронной почте <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a>.</p>
</div>
<div class="faq-item">
<h3>Какова комиссия площадки?</h3>
<p>Комиссия зависит от типа товара и объёма продаж. Узнать точные условия можно при регистрации.</p>
</div>
<div class="faq-item">
<h3>Когда поступят деньги за продажу?</h3>
<p>Продавец получает средства после подтверждения клиентом получения товара либо спустя две недели с момента доставки, если клиент не указал проблем с покупкой.</p>
</div>
</section>
<section class="legal-section">
<h2>Служба поддержки 💬</h2>
<div class="faq-item">
<h3>Как связаться с поддержкой?</h3>
<p>
✉️ <strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a><br>
📞 <strong>Телефон (Россия):</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a><br>
📞 <strong>Телефон (Армения):</strong> <a href="tel:+37494861816">+374 94 86 18 16</a><br>
🏢 <strong>Время работы офиса:</strong> 10:00—19:00 (МСК)<br>
❄️ <strong>Техподдержка доступна круглосуточно.</strong>
</p>
</div>
<div class="faq-item">
<h3>Сколько ждать ответа?</h3>
<p>Во время рабочего дня ответ поступает в течение двух часов. В праздники и выходные возможны задержки до суток.</p>
</div>
</section>
<section class="legal-section">
<h2>Нужна помощь?</h2>
<p>Если возникнут дополнительные вопросы, обращайтесь на <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a> — мы оперативно решим любые ваши вопросы!</p>
</section>
</div>
<div class="faq-item">
<h3>Как зарегистрироваться на платформе?</h3>
<p>Зарегистрироваться легко: откройте наше приложение внутри Telegram. Ваш профиль будет создан автоматически сразу же после входа.</p>
</div>
<div class="faq-item">
<h3>Безопасно ли покупать на сайте?</h3>
<p>Абсолютно да! Все транзакции защищены современными технологиями шифрования (PCI DSS и 3D Secure). Ваши банковские данные надежно хранятся отдельно от нашей системы.</p>
</div>
</section>
<section class="legal-section">
<h2>Оформление заказа</h2>
<div class="faq-item">
<h3>Как правильно оформить покупку?</h3>
<p>
1⃣ Найдите понравившийся товар и добавьте его в корзину.<br>
2⃣ Открыв корзину, кликните «Оформить заказ».<br>
3⃣ Укажите адрес доставки и контактную информацию.<br>
4⃣ Выберите удобный способ оплаты.<br>
5⃣ Ознакомьтесь и подтвердите публичную оферту.<br>
6⃣ Завершите оплату вашего заказа.
</p>
</div>
<div class="faq-item">
<h3>Можно ли внести изменения в заказ после завершения покупки?</h3>
<p>Если продавец ещё не отправил товар, напишите нам в поддержку — мы поможем скорректировать заказ. Но после отправки товара изменение невозможно.</p>
</div>
<div class="faq-item">
<h3>Как отказаться от заказа?</h3>
<p>Свяжитесь с нами или непосредственно с продавцом через чаты заказов. Пока посылка не отправлена, отказ возможен с полным возвратом денег.</p>
</div>
</section>
<section class="legal-section">
<h2>Оплата 💳</h2>
<div class="faq-item">
<h3>Какие способы оплаты доступны?</h3>
<p>Принимаются:</p>
<ul>
<li>Банковские карты Visa, MasterCard, МИР,</li>
<li>Система быстрых платежей (СБП),</li>
<li>Электронные кошельки ЮMoney, QIWI,</li>
<li>Наличные при получении (если предусмотрено продавцом).</li>
</ul>
</div>
<div class="faq-item">
<h3>Когда спишутся средства с моей карты?</h3>
<p>Операция проходит мгновенно, но деньги временно замораживаются до момента получения вами товара. Как только вы подтверждаете получение, средства отправляются продавцу.</p>
</div>
<div class="faq-item">
<h3>Почему мой платёж мог быть отклонён?</h3>
<p>Возможные причины отказа:</p>
<ul>
<li>Недостаточно средств на счёте,</li>
<li>Превышение лимита операций по карте,</li>
<li>Карта заблокирована банком,</li>
<li>Технические сбои.</li>
</ul>
<p>Рекомендуем обратиться в банк для выяснения деталей.</p>
</div>
<div class="faq-item">
<h3>Получу ли я чек об оплате?</h3>
<p>Да, электронная квитанция придёт на указанный вами e-mail сразу после успешного платежа согласно закону № 54-ФЗ.</p>
</div>
</section>
<section class="legal-section">
<h2>Доставка 🚚</h2>
<div class="faq-item">
<h3>Через какие службы осуществляется доставка?</h3>
<p><strong>Цифровые товары:</strong> Платформа DexarMarket автоматически отправляет цифровые товары (лицензии, ключи, сертификаты) на вашу электронную почту или в личный кабинет мгновенно после оплаты.</p>
<p><strong>Материальные товары:</strong> Продавец самостоятельно работает с ведущими службами доставки:</p>
<ul>
<li>СДЭК,</li>
<li>Почта России,</li>
<li>Boxberry,</li>
<li>DPD,</li>
<li>Яндекс.Доставка.</li>
</ul>
<p>Способ доставки материальных товаров выбирается исходя из предпочтений продавца и места назначения.</p>
</div>
<div class="faq-item">
<h3>Сколько стоит доставка моего заказа?</h3>
<p><strong>Цифровые товары:</strong> Доставка бесплатная — товар придёт на email мгновенно.</p>
<p><strong>Материальные товары:</strong> Цена определяется продавцом и зависит от веса, габаритов товара, выбранного способа и региона доставки. Окончательная стоимость видна при оформлении заказа.</p>
</div>
<div class="faq-item">
<h3>Какой срок доставки моего заказа?</h3>
<p><strong>Цифровые товары:</strong> Мгновенная доставка на email (в течение нескольких минут после оплаты).</p>
<p><strong>Материальные товары</strong> (примерные сроки):</p>
<ul>
<li>СДЭК: 27 рабочих дней,</li>
<li>Почта России: 514 рабочих дней,</li>
<li>Boxberry: 25 рабочих дней,</li>
<li>DPD: 13 рабочих дня,</li>
<li>Яндекс.Доставка: в тот же день (при наличии возможности в вашем городе).</li>
</ul>
</div>
<div class="faq-item">
<h3>Как отслеживать мою посылку?</h3>
<p>Вы получите трек-код на свою электронную почту и сможете увидеть статус заказа в личном кабинете. Отслеживать посылку можно на официальном сайте выбранной курьерской службы.</p>
</div>
<div class="faq-item">
<h3>Что делать, если товар пришёл повреждённым? ⛑</h3>
<p>Осмотрите товар прямо при курьере. Если обнаружились дефекты — оформляйте акт отказа от приёмки и незамедлительно сообщайте продавцу и службе поддержки.</p>
</div>
</section>
<section class="legal-section">
<h2>Возврат и обмен ✅</h2>
<div class="faq-item">
<h3>Можно ли вернуть товар?</h3>
<p>Да, закон позволяет вернуть качественный товар в течение 7 дней после получения. Бракованные изделия возвращаются по особым правилам.</p>
</div>
<div class="faq-item">
<h3>Какие товары вернуть нельзя?</h3>
<p>Нельзя вернуть товары, перечисленные в Постановлении Правительства РФ №2463: медикаменты, косметику, бельё, активированные цифровые продукты и индивидуальные заказы. Подробности смотрите в разделе <a [routerLink]="'/return-policy' | langRoute">«Политика возврата»</a>.</p>
</div>
<div class="faq-item">
<h3>Как вернуть деньги?</h3>
<p>
1. Сообщите продавцу о намерении вернуть товар.<br>
2. Верните товар в оригинальной упаковке.<br>
3. Продавец проверит состояние товара и вернёт средства таким же способом, каким была произведена покупка (до 30 дней ожидания возврата).
</p>
</div>
<div class="faq-item">
<h3>Кто оплатит обратный путь?</h3>
<p>Покупатель оплачивает обратную доставку качественного товара. Если обнаружен брак — расходы несёт продавец.</p>
</div>
</section>
<section class="legal-section">
<h2>Гарантия 🔧</h2>
<div class="faq-item">
<h3>Есть ли гарантия на товары?</h3>
<p>Большинство наших товаров имеют официальную гарантию производителя. Срок варьируется от 12 до 36 месяцев и указывается в карточке каждого продукта.</p>
</div>
<div class="faq-item">
<h3>Как воспользоваться гарантией?</h3>
<p>Сообщите продавцу о неисправности, приложив фотографии дефектов и гарантийный талон. Продавец займётся ремонтом или заменой товара.</p>
</div>
<div class="faq-item">
<h3>Что не покрыто гарантией?</h3>
<p>Механические повреждения, последствия неправильной эксплуатации, самостоятельный ремонт, влияние влаги (если нет защиты), естественный износ — всё это не является страховым случаем.</p>
</div>
</section>
<section class="legal-section">
<h2>Безопасность и конфиденциальность 🔐</h2>
<div class="faq-item">
<h3>Как вы защищаете мои личные данные?</h3>
<p>Используем современные методы шифрования SSL/TLS, не храним реквизиты карт, выполняем требования закона №152-ФЗ о защите персональной информации. Подробности — в <a [routerLink]="'/privacy-policy' | langRoute">Политике конфиденциальности</a>.</p>
</div>
<div class="faq-item">
<h3>Кому предоставляются мои данные?</h3>
<p>Только вашему продавцу для обработки заказа и службам доставки. Данные не используются третьими лицами для маркетинга без вашего разрешения.</p>
</div>
<div class="faq-item">
<h3>Как удалить свой аккаунт?</h3>
<p>Напишите нам в службу поддержки запрос на удаление учётной записи. Аккаунт будет ликвидирован вместе с личной информацией в течение месяца.</p>
</div>
</section>
<section class="legal-section">
<h2>Информация для продавцов 📋</h2>
<div class="faq-item">
<h3>Как начать продавать на площадке?</h3>
<p>Чтобы присоединиться к нашим продавцам, обратитесь в службу поддержки по электронной почте <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a>.</p>
</div>
<div class="faq-item">
<h3>Какова комиссия площадки?</h3>
<p>Комиссия зависит от типа товара и объёма продаж. Узнать точные условия можно при регистрации.</p>
</div>
<div class="faq-item">
<h3>Когда поступят деньги за продажу?</h3>
<p>Продавец получает средства после подтверждения клиентом получения товара либо спустя две недели с момента доставки, если клиент не указал проблем с покупкой.</p>
</div>
</section>
<section class="legal-section">
<h2>Служба поддержки 💬</h2>
<div class="faq-item">
<h3>Как связаться с поддержкой?</h3>
<p>
✉️ <strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a><br>
📞 <strong>Телефон (Россия):</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a><br>
📞 <strong>Телефон (Армения):</strong> <a href="tel:+37494861816">+374 94 86 18 16</a><br>
🏢 <strong>Время работы офиса:</strong> 10:00—19:00 (МСК)<br>
❄️ <strong>Техподдержка доступна круглосуточно.</strong>
</p>
</div>
<div class="faq-item">
<h3>Сколько ждать ответа?</h3>
<p>Во время рабочего дня ответ поступает в течение двух часов. В праздники и выходные возможны задержки до суток.</p>
</div>
</section>
<section class="legal-section">
<h2>Нужна помощь?</h2>
<p>Если возникнут дополнительные вопросы, обращайтесь на <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a> — мы оперативно решим любые ваши вопросы!</p>
</section>
</div>

View File

@@ -55,7 +55,23 @@
</div>
<div class="novo-info">
<h1 class="novo-title">{{ item()!.name }}</h1>
<h1 class="novo-title">{{ getItemName() }}</h1>
@if (item()!.badges && item()!.badges!.length > 0) {
<div class="novo-badges">
@for (badge of item()!.badges!; track badge) {
<span class="item-badge" [class]="getBadgeClass(badge)">{{ badge }}</span>
}
</div>
}
@if (item()!.tags && item()!.tags!.length > 0) {
<div class="novo-tags">
@for (tag of item()!.tags!; track tag) {
<span class="item-tag">#{{ tag }}</span>
}
</div>
}
<div class="novo-rating">
<span class="stars">{{ getRatingStars(item()!.rating) }}</span>
@@ -77,12 +93,43 @@
<div class="novo-stock">
<span class="stock-label">{{ 'itemDetail.stock' | translate }}</span>
<div class="stock-indicator" [class.high]="item()!.remainings === 'high'" [class.medium]="item()!.remainings === 'medium'" [class.low]="item()!.remainings === 'low'">
<div class="stock-indicator" [class]="getStockClass()">
<span class="dot"></span>
{{ item()!.remainings === 'high' ? ('itemDetail.inStock' | translate) : item()!.remainings === 'medium' ? ('itemDetail.mediumStock' | translate) : ('itemDetail.lowStock' | translate) }}
{{ getStockLabel() }}
</div>
@if (item()!.quantity != null) {
<span class="stock-qty">({{ item()!.quantity }} шт.)</span>
}
</div>
@if (item()!.colour || item()!.size) {
<div class="novo-variants">
@if (item()!.colour) {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.colour' | translate }}:</span>
<span class="variant-chip colour-chip">{{ item()!.colour }}</span>
</div>
}
@if (item()!.size) {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.size' | translate }}:</span>
<span class="variant-chip size-chip">{{ item()!.size }}</span>
</div>
}
</div>
}
@if (item()!.attributes && item()!.attributes!.length > 0) {
<div class="novo-attributes">
@for (attr of item()!.attributes!; track attr.key) {
<div class="attribute-row">
<span class="attribute-key">{{ attr.key }}</span>
<span class="attribute-value">{{ attr.value }}</span>
</div>
}
</div>
}
<button class="novo-add-cart" (click)="addToCart()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="9" cy="21" r="1"></circle>
@@ -93,8 +140,26 @@
</button>
<div class="novo-description">
<h3>{{ 'itemDetail.description' | translate }}</h3>
<div [innerHTML]="getSafeHtml(item()!.description)"></div>
@if (getSimpleDescription()) {
<p class="novo-simple-desc">{{ getSimpleDescription() }}</p>
}
@if (hasDescriptionFields()) {
<h3>{{ 'itemDetail.specifications' | translate }}</h3>
<table class="novo-specs-table">
<tbody>
@for (field of getTranslatedDescriptionFields(); track field.key) {
<tr>
<td class="spec-key">{{ field.key }}</td>
<td class="spec-value">{{ field.value }}</td>
</tr>
}
</tbody>
</table>
} @else {
<h3>{{ 'itemDetail.description' | translate }}</h3>
<div [innerHTML]="getSafeHtml(item()!.description)"></div>
}
</div>
</div>
</div>
@@ -249,7 +314,23 @@
<!-- Item Info -->
<div class="dx-info">
<h1 class="dx-title">{{ item()!.name }}</h1>
<h1 class="dx-title">{{ getItemName() }}</h1>
@if (item()!.badges && item()!.badges!.length > 0) {
<div class="dx-badges">
@for (badge of item()!.badges!; track badge) {
<span class="item-badge" [class]="getBadgeClass(badge)">{{ badge }}</span>
}
</div>
}
@if (item()!.tags && item()!.tags!.length > 0) {
<div class="dx-tags">
@for (tag of item()!.tags!; track tag) {
<span class="item-tag">#{{ tag }}</span>
}
</div>
}
<div class="dx-rating">
<div class="dx-stars">
@@ -277,15 +358,43 @@
<div class="dx-stock">
<span class="dx-stock-label">{{ 'itemDetail.stock' | translate }}</span>
<span class="dx-stock-status"
[class.high]="item()!.remainings === 'high'"
[class.medium]="item()!.remainings === 'medium'"
[class.low]="item()!.remainings === 'low'">
<span class="dx-stock-status" [class]="getStockClass()">
<span class="dx-stock-dot"></span>
{{ item()!.remainings === 'high' ? ('itemDetail.inStock' | translate) : item()!.remainings === 'medium' ? ('itemDetail.mediumStock' | translate) : ('itemDetail.lastItems' | translate) }}
{{ getStockLabel() }}
</span>
@if (item()!.quantity != null) {
<span class="dx-stock-qty">({{ item()!.quantity }} шт.)</span>
}
</div>
@if (item()!.colour || item()!.size) {
<div class="dx-variants">
@if (item()!.colour) {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.colour' | translate }}:</span>
<span class="variant-chip colour-chip">{{ item()!.colour }}</span>
</div>
}
@if (item()!.size) {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.size' | translate }}:</span>
<span class="variant-chip size-chip">{{ item()!.size }}</span>
</div>
}
</div>
}
@if (item()!.attributes && item()!.attributes!.length > 0) {
<div class="dx-attributes">
@for (attr of item()!.attributes!; track attr.key) {
<div class="attribute-row">
<span class="attribute-key">{{ attr.key }}</span>
<span class="attribute-value">{{ attr.value }}</span>
</div>
}
</div>
}
<button class="dx-add-cart" (click)="addToCart()">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="9" cy="21" r="1"></circle>
@@ -296,8 +405,26 @@
</button>
<div class="dx-description">
<h2>{{ 'itemDetail.description' | translate }}</h2>
<div class="dx-description-text" [innerHTML]="getSafeHtml(item()!.description)"></div>
@if (getSimpleDescription()) {
<p class="dx-simple-desc">{{ getSimpleDescription() }}</p>
}
@if (hasDescriptionFields()) {
<h2>{{ 'itemDetail.specifications' | translate }}</h2>
<table class="dx-specs-table">
<tbody>
@for (field of getTranslatedDescriptionFields(); track field.key) {
<tr>
<td class="spec-key">{{ field.key }}</td>
<td class="spec-value">{{ field.value }}</td>
</tr>
}
</tbody>
</table>
} @else {
<h2>{{ 'itemDetail.description' | translate }}</h2>
<div class="dx-description-text" [innerHTML]="getSafeHtml(item()!.description)"></div>
}
</div>
</div>
</div>

View File

@@ -1,4 +1,6 @@
// ========== DEXAR ITEM DETAIL - Redesigned 2026 ==========
@use 'sass:color';
// ========== DEXAR ITEM DETAIL - Redesigned 2026 ==========
$dx-font: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
$dx-dark: #1e3c38;
$dx-primary: #497671;
@@ -50,7 +52,7 @@ $dx-card-bg: #f5f3f9;
transition: all 0.2s;
&:hover {
background: darken($dx-primary, 8%);
background: color.adjust($dx-primary, $lightness: -8%);
transform: translateY(-1px);
}
}
@@ -281,7 +283,7 @@ $dx-card-bg: #f5f3f9;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
&:hover {
background: darken($dx-primary, 8%);
background: color.adjust($dx-primary, $lightness: -8%);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(73, 118, 113, 0.3);
}
@@ -291,6 +293,64 @@ $dx-card-bg: #f5f3f9;
}
}
// Variant chips (colour/size) — shared between dexar and novo
.dx-variants, .novo-variants {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 12px;
.variant-group {
display: flex;
align-items: center;
gap: 8px;
}
.variant-label {
font-size: 0.9rem;
color: #6b7280;
font-weight: 500;
}
.variant-chip {
display: inline-flex;
align-items: center;
padding: 6px 14px;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 600;
border: 1.5px solid $dx-border;
background: rgba(73, 118, 113, 0.06);
color: $dx-primary;
}
}
.dx-attributes, .novo-attributes {
display: flex;
flex-wrap: wrap;
gap: 6px 16px;
margin-bottom: 14px;
padding: 10px 14px;
background: #f8fafa;
border-radius: 10px;
.attribute-row {
display: flex;
gap: 6px;
font-size: 0.85rem;
}
.attribute-key {
color: #6b7280;
&::after { content: ':'; }
}
.attribute-value {
font-weight: 600;
color: #1a1a1a;
}
}
.dx-description {
padding-top: 8px;
border-top: 1px solid $dx-border;
@@ -434,7 +494,7 @@ $dx-card-bg: #f5f3f9;
justify-content: center;
&:hover:not(:disabled) {
background: darken($dx-primary, 8%);
background: color.adjust($dx-primary, $lightness: -8%);
transform: translateY(-1px);
}
@@ -642,22 +702,70 @@ $dx-card-bg: #f5f3f9;
}
}
// Responsive
// ========== DEXAR RESPONSIVE ==========
// Large desktop — constrain gallery height
@media (min-width: 1201px) {
.dx-main-photo {
max-height: 560px;
}
}
// Tablet landscape / small desktop
@media (max-width: 1200px) {
.dx-item-content {
gap: 32px;
}
.dx-title {
font-size: 1.5rem;
}
}
// Tablet portrait
@media (max-width: 992px) {
.dx-item-content {
grid-template-columns: 1fr;
gap: 32px;
}
.dx-gallery {
max-width: 600px;
margin: 0 auto;
}
.dx-main-photo {
max-height: 480px;
aspect-ratio: auto;
}
.dx-add-cart {
max-width: 100%;
}
.dx-reviews-section,
.dx-qa-section {
h2 {
font-size: 1.3rem;
}
}
}
// Mobile
@media (max-width: 768px) {
.dx-item-container {
padding: 16px;
}
// On mobile: thumbnails go below main photo
.dx-item-content {
gap: 24px;
margin-bottom: 32px;
}
// Thumbnails go below main photo
.dx-gallery {
flex-direction: column;
max-width: 100%;
}
.dx-thumbnails {
@@ -666,14 +774,16 @@ $dx-card-bg: #f5f3f9;
max-height: none;
overflow-x: auto;
overflow-y: hidden;
order: 1; // put below main photo
order: 1;
scrollbar-width: none;
&::-webkit-scrollbar { display: none; }
}
.dx-main-photo {
order: 0; // main photo first
order: 0;
max-height: 400px;
aspect-ratio: auto;
}
.dx-thumb {
@@ -690,8 +800,32 @@ $dx-card-bg: #f5f3f9;
font-size: 1.8rem;
}
.dx-old-price {
font-size: 1rem;
}
.dx-add-cart {
max-width: 100%;
padding: 14px 20px;
font-size: 1rem;
}
.dx-description {
h2 {
font-size: 1.15rem;
}
}
.dx-specs-table {
.spec-key {
white-space: normal;
width: auto;
}
td {
padding: 8px 10px;
font-size: 0.85rem;
}
}
.dx-review-form {
@@ -707,21 +841,153 @@ $dx-card-bg: #f5f3f9;
width: 100%;
}
}
.dx-review-card {
padding: 16px;
}
.dx-reviews-section,
.dx-qa-section {
margin-bottom: 32px;
h2 {
font-size: 1.25rem;
margin-bottom: 16px;
}
}
.dx-qa-card {
padding: 16px;
}
.dx-question,
.dx-answer {
font-size: 0.9rem;
}
}
// Small mobile
@media (max-width: 480px) {
.dx-item-container {
padding: 12px;
}
.dx-item-content {
gap: 20px;
margin-bottom: 24px;
}
.dx-main-photo {
max-height: 300px;
border-radius: 10px;
img, video {
padding: 8px;
}
}
.dx-thumb {
width: 56px;
height: 56px;
min-width: 56px;
width: 52px;
height: 52px;
min-width: 52px;
}
.dx-title {
font-size: 1.25rem;
font-size: 1.2rem;
}
.dx-info {
gap: 16px;
}
.dx-current-price {
font-size: 1.6rem;
font-size: 1.5rem;
}
.dx-rating {
flex-wrap: wrap;
gap: 6px;
}
.dx-stock {
flex-wrap: wrap;
gap: 6px;
}
.dx-add-cart {
padding: 12px 16px;
font-size: 0.95rem;
border-radius: 10px;
}
.dx-review-form {
padding: 14px;
h3 {
font-size: 1rem;
}
}
.dx-star-selector {
.dx-star-pick {
font-size: 1.5rem;
}
}
.dx-textarea {
padding: 12px;
font-size: 0.9rem;
}
.dx-review-card {
padding: 14px;
}
.dx-reviewer-name {
font-size: 0.9rem;
}
.dx-review-text {
font-size: 0.9rem;
}
.dx-specs-table {
td {
padding: 6px 8px;
font-size: 0.8rem;
display: block;
}
.spec-key {
width: 100%;
padding-bottom: 2px;
}
.spec-value {
padding-top: 0;
}
tr {
display: flex;
flex-direction: column;
padding: 6px 0;
}
}
.dx-qa-card {
padding: 14px;
}
.dx-question,
.dx-answer {
font-size: 0.85rem;
gap: 8px;
}
.dx-qa-label {
width: 24px;
height: 24px;
font-size: 0.7rem;
}
}
@@ -1299,12 +1565,20 @@ $dx-card-bg: #f5f3f9;
}
}
// ========== NOVO RESPONSIVE ==========
// Tablet portrait
@media (max-width: 968px) {
.novo-item-content {
grid-template-columns: 1fr;
gap: 2rem;
}
.novo-gallery {
max-width: 600px;
margin: 0 auto;
}
.novo-info .novo-title {
font-size: 1.5rem;
}
@@ -1313,6 +1587,10 @@ $dx-card-bg: #f5f3f9;
font-size: 2rem;
}
.novo-info .novo-add-cart {
max-width: 100%;
}
.novo-review-form {
padding: 1.5rem;
@@ -1327,3 +1605,302 @@ $dx-card-bg: #f5f3f9;
}
}
}
// Mobile
@media (max-width: 768px) {
.novo-item-container {
padding: 1rem;
}
.novo-item-content {
gap: 1.5rem;
margin-bottom: 2rem;
}
.novo-gallery {
max-width: 100%;
.novo-main-photo {
border-radius: var(--radius-lg);
margin-bottom: 0.75rem;
}
.novo-thumbnails {
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 0.5rem;
}
}
.novo-info {
.novo-title {
font-size: 1.35rem;
}
.novo-rating {
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.novo-price-block {
padding: 1rem;
.current-price {
font-size: 1.75rem;
}
.old-price {
font-size: 0.95rem;
}
}
.novo-stock {
flex-wrap: wrap;
gap: 0.5rem;
padding: 0.75rem;
}
.novo-add-cart {
padding: 1rem 1.5rem;
font-size: 1rem;
margin-bottom: 1.5rem;
}
.novo-description {
h3 {
font-size: 1.1rem;
}
}
}
.novo-specs-table {
.spec-key {
white-space: normal;
width: auto;
}
td {
padding: 8px 10px;
font-size: 0.85rem;
}
}
.novo-reviews {
margin-top: 2rem;
padding-top: 2rem;
h2 {
font-size: 1.35rem;
margin-bottom: 1.5rem;
}
}
.novo-review-card {
padding: 1rem;
}
.novo-review-form {
padding: 1.25rem;
h3 {
font-size: 1.1rem;
}
}
}
// Small mobile
@media (max-width: 480px) {
.novo-item-container {
padding: 0.75rem;
}
.novo-item-content {
gap: 1.25rem;
margin-bottom: 1.5rem;
}
.novo-gallery {
.novo-thumbnails {
grid-template-columns: repeat(auto-fill, minmax(52px, 1fr));
gap: 0.4rem;
}
.novo-main-photo {
border-radius: var(--radius-md);
}
}
.novo-info {
.novo-title {
font-size: 1.15rem;
}
.novo-price-block {
padding: 0.75rem;
.current-price {
font-size: 1.5rem;
}
}
.novo-stock {
padding: 0.6rem;
font-size: 0.85rem;
}
.novo-add-cart {
padding: 0.85rem 1rem;
font-size: 0.95rem;
border-radius: var(--radius-md);
}
}
.novo-review-form {
padding: 1rem;
.novo-rating-input {
.novo-star-selector {
.novo-star {
font-size: 1.6rem;
}
}
}
.novo-textarea {
padding: 0.75rem;
font-size: 0.9rem;
}
}
.novo-review-card {
padding: 0.75rem;
.review-header {
flex-direction: column;
gap: 0.5rem;
.review-stars {
align-self: flex-start;
}
}
.review-text {
font-size: 0.9rem;
}
}
.novo-specs-table {
td {
padding: 6px 8px;
font-size: 0.8rem;
display: block;
}
.spec-key {
width: 100%;
padding-bottom: 2px;
}
.spec-value {
padding-top: 0;
}
tr {
display: flex;
flex-direction: column;
padding: 6px 0;
}
}
}
// ========== BADGES, TAGS & SPECS (shared) ==========
// Badges
.novo-badges, .dx-badges {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 8px 0;
}
.item-badge {
display: inline-block;
padding: 3px 10px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #fff;
&.badge-new { background: #4caf50; }
&.badge-sale { background: #f44336; }
&.badge-exclusive { background: #9c27b0; }
&.badge-hot { background: #ff5722; }
&.badge-limited { background: #ff9800; }
&.badge-bestseller { background: #2196f3; }
&.badge-featured { background: #607d8b; }
&.badge-custom { background: #78909c; }
}
// Tags
.novo-tags, .dx-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 6px 0 12px;
}
.item-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75rem;
color: #497671;
background: rgba(73, 118, 113, 0.08);
border: 1px solid rgba(73, 118, 113, 0.15);
}
// Specs table
.novo-specs-table, .dx-specs-table {
width: 100%;
border-collapse: collapse;
margin: 12px 0;
tr {
border-bottom: 1px solid #e8ecec;
&:last-child { border-bottom: none; }
}
td {
padding: 10px 12px;
font-size: 0.9rem;
vertical-align: top;
}
.spec-key {
color: #697777;
font-weight: 500;
width: 40%;
white-space: nowrap;
}
.spec-value {
color: #1e3c38;
}
}
// Simple description
.novo-simple-desc, .dx-simple-desc {
font-size: 0.95rem;
color: #697777;
line-height: 1.6;
margin-bottom: 16px;
}
// Stock quantity
.stock-qty, .dx-stock-qty {
font-size: 0.8rem;
color: #697777;
margin-left: 8px;
}

View File

@@ -2,13 +2,13 @@ import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy, inject }
import { DecimalPipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { ApiService, CartService, TelegramService, SeoService } from '../../services';
import { Item } from '../../models';
import { ApiService, CartService, TelegramService, LanguageService, SeoService } from '../../services';
import { Item, DescriptionField } from '../../models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { Subscription } from 'rxjs';
import { environment } from '../../../environments/environment';
import { SecurityContext } from '@angular/core';
import { getDiscountedPrice } from '../../utils/item.utils';
import { getDiscountedPrice, getAllImages, getStockStatus, getBadgeClass, getTranslatedField } from '../../utils/item.utils';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service';
@@ -48,7 +48,8 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
private apiService: ApiService,
private cartService: CartService,
private telegramService: TelegramService,
private sanitizer: DomSanitizer
private sanitizer: DomSanitizer,
private languageService: LanguageService
) {}
ngOnInit(): void {
@@ -100,6 +101,57 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
return getDiscountedPrice(currentItem);
}
// BackOffice integration helpers
getItemName(): string {
const currentItem = this.item();
if (!currentItem) return '';
const lang = this.languageService.currentLanguage();
return getTranslatedField(currentItem, 'name', lang);
}
getSimpleDescription(): string {
const currentItem = this.item();
if (!currentItem) return '';
const lang = this.languageService.currentLanguage();
return getTranslatedField(currentItem, 'simpleDescription', lang);
}
hasDescriptionFields(): boolean {
const currentItem = this.item();
return !!(currentItem?.descriptionFields && currentItem.descriptionFields.length > 0);
}
getTranslatedDescriptionFields(): DescriptionField[] {
const currentItem = this.item();
if (!currentItem) return [];
const lang = this.languageService.currentLanguage();
const translation = currentItem.translations?.[lang];
if (translation?.description && translation.description.length > 0) {
return translation.description;
}
return currentItem.descriptionFields || [];
}
getStockClass(): string {
const currentItem = this.item();
if (!currentItem) return 'high';
return getStockStatus(currentItem);
}
getStockLabel(): string {
const status = this.getStockClass();
switch (status) {
case 'high': return 'В наличии';
case 'medium': return 'Заканчивается';
case 'low': return 'Последние штуки';
case 'out': return 'Нет в наличии';
default: return 'В наличии';
}
}
readonly getBadgeClass = getBadgeClass;
getSafeHtml(html: string): SafeHtml {
return this.sanitizer.sanitize(SecurityContext.HTML, html) || '';
}

View File

@@ -1,98 +1,102 @@
<h1>Company Details</h1>
<section class="legal-section">
<h2>Full Company Name</h2>
<p>LIMITED LIABILITY COMPANY «INT FIN LOGISTIC»</p>
<p><strong>Abbreviated name:</strong> LLC «INT FIN LOGISTIC»</p>
</section>
<section class="legal-section">
<h2>Legal Address</h2>
<p>ARMENIA, 2301, KOTAYK REGION, HRAZDAN, KHACHATRYAN st., 31, 4</p>
</section>
<section class="legal-section">
<h2>Actual Address</h2>
<p><strong>Office in Armenia:</strong> 0033, Yerevan, Orbeli Brothers St., 47</p>
<p><strong>Office in Russia:</strong> 121059, Moscow, Taras Shevchenko Emb., 3/2</p>
</section>
<section class="legal-section">
<h2>Main Details</h2>
<div class="details-grid">
<div class="detail-item">
<strong>TIN (RUS):</strong>
<span>9909697628</span>
</div>
<div class="detail-item">
<strong>TIN (ARM):</strong>
<span>03033502</span>
</div>
<div class="detail-item">
<strong>KPP:</strong>
<span>770287001</span>
</div>
<div class="detail-item">
<strong>OGRN:</strong>
<span>85.110.1408711</span>
</div>
<div class="legal-page">
<div class="legal-container">
<h1>Company Details</h1>
<section class="legal-section">
<h2>Full Company Name</h2>
<p>LIMITED LIABILITY COMPANY «INT FIN LOGISTIC»</p>
<p><strong>Abbreviated name:</strong> LLC «INT FIN LOGISTIC»</p>
</section>
<section class="legal-section">
<h2>Legal Address</h2>
<p>ARMENIA, 2301, KOTAYK REGION, HRAZDAN, KHACHATRYAN st., 31, 4</p>
</section>
<section class="legal-section">
<h2>Actual Address</h2>
<p><strong>Office in Armenia:</strong> 0033, Yerevan, Orbeli Brothers St., 47</p>
<p><strong>Office in Russia:</strong> 121059, Moscow, Taras Shevchenko Emb., 3/2</p>
</section>
<section class="legal-section">
<h2>Main Details</h2>
<div class="details-grid">
<div class="detail-item">
<strong>TIN (RUS):</strong>
<span>9909697628</span>
</div>
<div class="detail-item">
<strong>TIN (ARM):</strong>
<span>03033502</span>
</div>
<div class="detail-item">
<strong>KPP:</strong>
<span>770287001</span>
</div>
<div class="detail-item">
<strong>OGRN:</strong>
<span>85.110.1408711</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Bank Details</h2>
<div class="details-grid">
<div class="detail-item">
<strong>Bank:</strong>
<span>JSC "Raiffeisenbank"</span>
</div>
<div class="detail-item">
<strong>Settlement account:</strong>
<span>40807810500000002376</span>
</div>
<div class="detail-item">
<strong>Correspondent account:</strong>
<span>30101810200000000700</span>
</div>
<div class="detail-item">
<strong>BIC:</strong>
<span>044525700</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Contact Information</h2>
<div class="details-grid">
<div class="detail-item">
<strong>Phone (RUS):</strong>
<span><a href="tel:+79264593157">+7 (926) 459-31-57</a></span>
</div>
<div class="detail-item">
<strong>Phone (ARM):</strong>
<span><a href="tel:+37494861816">+374 94 86 18 16</a></span>
</div>
<div class="detail-item">
<strong>Email:</strong>
<span><a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></span>
</div>
<div class="detail-item">
<strong>Website:</strong>
<span><a href="https://dexarmarket.ru" target="_blank">dexarmarket.ru</a></span>
</div>
<div class="detail-item">
<strong>Office hours:</strong>
<span>10:00 - 19:00 (MSK)</span>
</div>
<div class="detail-item">
<strong>Support:</strong>
<span>24/7</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Management</h2>
<p><strong>General Director:</strong> Hovhannisyan Ashot Rafikovich</p>
<p><strong>Basis of authority:</strong> Charter</p>
</section>
</div>
</section>
<section class="legal-section">
<h2>Bank Details</h2>
<div class="details-grid">
<div class="detail-item">
<strong>Bank:</strong>
<span>JSC "Raiffeisenbank"</span>
</div>
<div class="detail-item">
<strong>Settlement account:</strong>
<span>40807810500000002376</span>
</div>
<div class="detail-item">
<strong>Correspondent account:</strong>
<span>30101810200000000700</span>
</div>
<div class="detail-item">
<strong>BIC:</strong>
<span>044525700</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Contact Information</h2>
<div class="details-grid">
<div class="detail-item">
<strong>Phone (RUS):</strong>
<span><a href="tel:+79264593157">+7 (926) 459-31-57</a></span>
</div>
<div class="detail-item">
<strong>Phone (ARM):</strong>
<span><a href="tel:+37494861816">+374 94 86 18 16</a></span>
</div>
<div class="detail-item">
<strong>Email:</strong>
<span><a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></span>
</div>
<div class="detail-item">
<strong>Website:</strong>
<span><a href="https://dexarmarket.ru" target="_blank">dexarmarket.ru</a></span>
</div>
<div class="detail-item">
<strong>Office hours:</strong>
<span>10:00 - 19:00 (MSK)</span>
</div>
<div class="detail-item">
<strong>Support:</strong>
<span>24/7</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Management</h2>
<p><strong>General Director:</strong> Hovhannisyan Ashot Rafikovich</p>
<p><strong>Basis of authority:</strong> Charter</p>
</section>
</div>

View File

@@ -1,98 +1,102 @@
<h1>Կազմակերպության տվյալներ</h1>
<section class="legal-section">
<h2>Կազմակերպության լիարժեկ անվանումը</h2>
<p>ՍԱՀՄԱՆԱՓԱԿ ՊԱՏԱՍԽԱՆԱՏվությամբ Ընկերություն «ԻՆՏ ՖԻՆ ԼՈԳԻՍՏԻԿ»</p>
<p><strong>Հապավոր անվանումը՝</strong> ՍՊԸ «ԻՆՏ ՖԻՆ ԼՈԳԻՍՏԻԿ»</p>
</section>
<section class="legal-section">
<h2>Իրավաբանական հասցե</h2>
<p>ՀԱՅԱՍՏԱՆ, 2301, ԿՈՏԱՅԿԻ ՄԱՐԶ, ՀՐԱԶԴԱՆ, ԽԱՉԱՏՐՅԱՆ փկ., 31, 4</p>
</section>
<section class="legal-section">
<h2>Գործնական հասցե</h2>
<p><strong>Գրասենյակ Հայաստանում՝</strong> 0033, Երևան, Եղբայրներ Օրբելի փկ., 47</p>
<p><strong>Գրասենյակ Ռուսաստանում՝</strong> 121059, Մոսկվա, Տարաս Շևչենկոի փակ., 3կ2</p>
</section>
<section class="legal-section">
<h2>Հիմնական մանրամասներ</h2>
<div class="details-grid">
<div class="detail-item">
<strong>ՀՎՀՀ (RUS)՝</strong>
<span>9909697628</span>
</div>
<div class="detail-item">
<strong>ՀՎՀՀ (ARM)՝</strong>
<span>03033502</span>
</div>
<div class="detail-item">
<strong>ԿՊՊ՝</strong>
<span>770287001</span>
</div>
<div class="detail-item">
<strong>ՊԳՐՆ՝</strong>
<span>85.110.1408711</span>
</div>
<div class="legal-page">
<div class="legal-container">
<h1>Կազմակերպության տվյալներ</h1>
<section class="legal-section">
<h2>Կազմակերպության լիարժեկ անվանումը</h2>
<p>ՍԱՀՄԱՆԱՓԱԿ ՊԱՏԱՍԽԱՆԱՏվությամբ Ընկերություն «ԻՆՏ ՖԻՆ ԼՈԳԻՍՏԻԿ»</p>
<p><strong>Հապավոր անվանումը՝</strong> ՍՊԸ «ԻՆՏ ՖԻՆ ԼՈԳԻՍՏԻԿ»</p>
</section>
<section class="legal-section">
<h2>Իրավաբանական հասցե</h2>
<p>ՀԱՅԱՍՏԱՆ, 2301, ԿՈՏԱՅԿԻ ՄԱՐԶ, ՀՐԱԶԴԱՆ, ԽԱՉԱՏՐՅԱՆ փկ., 31, 4</p>
</section>
<section class="legal-section">
<h2>Գործնական հասցե</h2>
<p><strong>Գրասենյակ Հայաստանում՝</strong> 0033, Երևան, Եղբայրներ Օրբելի փկ., 47</p>
<p><strong>Գրասենյակ Ռուսաստանում՝</strong> 121059, Մոսկվա, Տարաս Շևչենկոի փակ., 3կ2</p>
</section>
<section class="legal-section">
<h2>Հիմնական մանրամասներ</h2>
<div class="details-grid">
<div class="detail-item">
<strong>ՀՎՀՀ (RUS)՝</strong>
<span>9909697628</span>
</div>
<div class="detail-item">
<strong>ՀՎՀՀ (ARM)՝</strong>
<span>03033502</span>
</div>
<div class="detail-item">
<strong>ԿՊՊ՝</strong>
<span>770287001</span>
</div>
<div class="detail-item">
<strong>ՊԳՐՆ՝</strong>
<span>85.110.1408711</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Բանկային տվյալներ</h2>
<div class="details-grid">
<div class="detail-item">
<strong>Բանկ՝</strong>
<span>«Ռայֆֆայզենբանկ» ԲԲ</span>
</div>
<div class="detail-item">
<strong>Հաշվարկային հաշիվ՝</strong>
<span>40807810500000002376</span>
</div>
<div class="detail-item">
<strong>Թղակցային հաշիվ՝</strong>
<span>30101810200000000700</span>
</div>
<div class="detail-item">
<strong>ԲԻԿ՝</strong>
<span>044525700</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Կապի տեղեկատվություն</h2>
<div class="details-grid">
<div class="detail-item">
<strong>Հեռախոս (RUS)՝</strong>
<span><a href="tel:+79264593157">+7 (926) 459-31-57</a></span>
</div>
<div class="detail-item">
<strong>Հեռախոս (ARM)՝</strong>
<span><a href="tel:+37494861816">+374 94 86 18 16</a></span>
</div>
<div class="detail-item">
<strong>Էլ. փոստ՝</strong>
<span><a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></span>
</div>
<div class="detail-item">
<strong>Կայք՝</strong>
<span><a href="https://dexarmarket.ru" target="_blank">dexarmarket.ru</a></span>
</div>
<div class="detail-item">
<strong>Գրասենյակի աշխատանքային ժամեր՝</strong>
<span>10:00 - 19:00 (ՄՍԿ)</span>
</div>
<div class="detail-item">
<strong>Տեխնիկական աջակցություն՝</strong>
<span>24/7</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Ղեկավարություն</h2>
<p><strong>Գլխավոր տնօրեն՝</strong> Օհաննիսյան Աշոտ Ռաֆիկի</p>
<p><strong>Գործողության հիմք՝</strong> Կանոնադրություն</p>
</section>
</div>
</section>
<section class="legal-section">
<h2>Բանկային տվյալներ</h2>
<div class="details-grid">
<div class="detail-item">
<strong>Բանկ՝</strong>
<span>«Ռայֆֆայզենբանկ» ԲԲ</span>
</div>
<div class="detail-item">
<strong>Հաշվարկային հաշիվ՝</strong>
<span>40807810500000002376</span>
</div>
<div class="detail-item">
<strong>Թղակցային հաշիվ՝</strong>
<span>30101810200000000700</span>
</div>
<div class="detail-item">
<strong>ԲԻԿ՝</strong>
<span>044525700</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Կապի տեղեկատվություն</h2>
<div class="details-grid">
<div class="detail-item">
<strong>Հեռախոս (RUS)՝</strong>
<span><a href="tel:+79264593157">+7 (926) 459-31-57</a></span>
</div>
<div class="detail-item">
<strong>Հեռախոս (ARM)՝</strong>
<span><a href="tel:+37494861816">+374 94 86 18 16</a></span>
</div>
<div class="detail-item">
<strong>Էլ. փոստ՝</strong>
<span><a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></span>
</div>
<div class="detail-item">
<strong>Կայք՝</strong>
<span><a href="https://dexarmarket.ru" target="_blank">dexarmarket.ru</a></span>
</div>
<div class="detail-item">
<strong>Գրասենյակի աշխատանքային ժամեր՝</strong>
<span>10:00 - 19:00 (ՄՍԿ)</span>
</div>
<div class="detail-item">
<strong>Տեխնիկական աջակցություն՝</strong>
<span>24/7</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Ղեկավարություն</h2>
<p><strong>Գլխավոր տնօրեն՝</strong> Օհաննիսյան Աշոտ Ռաֆիկի</p>
<p><strong>Գործողության հիմք՝</strong> Կանոնադրություն</p>
</section>
</div>

View File

@@ -1,98 +1,102 @@
<h1>Реквизиты организации</h1>
<section class="legal-section">
<h2>Полное наименование организации</h2>
<p>ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ИНТ ФИН ЛОГИСТИК»</p>
<p><strong>Сокращенное наименование:</strong> ООО «ИНТ ФИН ЛОГИСТИК»</p>
</section>
<section class="legal-section">
<h2>Юридический адрес</h2>
<p>АРМЕНИЯ, 2301, КОТАЙКСКАЯ ОБЛАСТЬ, РАЗДАН, ХАЧАТРЯНА ул, 31, 4</p>
</section>
<section class="legal-section">
<h2>Фактический адрес</h2>
<p><strong>Офис в Армении:</strong> 0033, Ереван, улица Братьев Орбели, 47</p>
<p><strong>Офис в России:</strong> 121059, Москва, наб. Тараса Шевченко, 3к2</p>
</section>
<section class="legal-section">
<h2>Основные реквизиты</h2>
<div class="details-grid">
<div class="detail-item">
<strong>ИНН (RUS):</strong>
<span>9909697628</span>
</div>
<div class="detail-item">
<strong>ИНН (ARM):</strong>
<span>03033502</span>
</div>
<div class="detail-item">
<strong>КПП:</strong>
<span>770287001</span>
</div>
<div class="detail-item">
<strong>ОГРН:</strong>
<span>85.110.1408711</span>
</div>
<div class="legal-page">
<div class="legal-container">
<h1>Реквизиты организации</h1>
<section class="legal-section">
<h2>Полное наименование организации</h2>
<p>ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ИНТ ФИН ЛОГИСТИК»</p>
<p><strong>Сокращенное наименование:</strong> ООО «ИНТ ФИН ЛОГИСТИК»</p>
</section>
<section class="legal-section">
<h2>Юридический адрес</h2>
<p>АРМЕНИЯ, 2301, КОТАЙКСКАЯ ОБЛАСТЬ, РАЗДАН, ХАЧАТРЯНА ул, 31, 4</p>
</section>
<section class="legal-section">
<h2>Фактический адрес</h2>
<p><strong>Офис в Армении:</strong> 0033, Ереван, улица Братьев Орбели, 47</p>
<p><strong>Офис в России:</strong> 121059, Москва, наб. Тараса Шевченко, 3к2</p>
</section>
<section class="legal-section">
<h2>Основные реквизиты</h2>
<div class="details-grid">
<div class="detail-item">
<strong>ИНН (RUS):</strong>
<span>9909697628</span>
</div>
<div class="detail-item">
<strong>ИНН (ARM):</strong>
<span>03033502</span>
</div>
<div class="detail-item">
<strong>КПП:</strong>
<span>770287001</span>
</div>
<div class="detail-item">
<strong>ОГРН:</strong>
<span>85.110.1408711</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Банковские реквизиты</h2>
<div class="details-grid">
<div class="detail-item">
<strong>Банк:</strong>
<span>АО "Райффайзенбанк"</span>
</div>
<div class="detail-item">
<strong>Расчетный счет:</strong>
<span>40807810500000002376</span>
</div>
<div class="detail-item">
<strong>Корр. счет:</strong>
<span>30101810200000000700</span>
</div>
<div class="detail-item">
<strong>БИК:</strong>
<span>044525700</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Контактная информация</h2>
<div class="details-grid">
<div class="detail-item">
<strong>Телефон (RUS):</strong>
<span><a href="tel:+79264593157">+7 (926) 459-31-57</a></span>
</div>
<div class="detail-item">
<strong>Телефон (ARM):</strong>
<span><a href="tel:+37494861816">+374 94 86 18 16</a></span>
</div>
<div class="detail-item">
<strong>Email:</strong>
<span><a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></span>
</div>
<div class="detail-item">
<strong>Сайт:</strong>
<span><a href="https://dexarmarket.ru" target="_blank">dexarmarket.ru</a></span>
</div>
<div class="detail-item">
<strong>Часы работы офиса:</strong>
<span>10:00 - 19:00 (МСК)</span>
</div>
<div class="detail-item">
<strong>Техподдержка:</strong>
<span>24/7</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Руководство</h2>
<p><strong>Генеральный директор:</strong> Оганнисян Ашот Рафикович</p>
<p><strong>Основание действий:</strong> Устав</p>
</section>
</div>
</section>
<section class="legal-section">
<h2>Банковские реквизиты</h2>
<div class="details-grid">
<div class="detail-item">
<strong>Банк:</strong>
<span>АО "Райффайзенбанк"</span>
</div>
<div class="detail-item">
<strong>Расчетный счет:</strong>
<span>40807810500000002376</span>
</div>
<div class="detail-item">
<strong>Корр. счет:</strong>
<span>30101810200000000700</span>
</div>
<div class="detail-item">
<strong>БИК:</strong>
<span>044525700</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Контактная информация</h2>
<div class="details-grid">
<div class="detail-item">
<strong>Телефон (RUS):</strong>
<span><a href="tel:+79264593157">+7 (926) 459-31-57</a></span>
</div>
<div class="detail-item">
<strong>Телефон (ARM):</strong>
<span><a href="tel:+37494861816">+374 94 86 18 16</a></span>
</div>
<div class="detail-item">
<strong>Email:</strong>
<span><a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></span>
</div>
<div class="detail-item">
<strong>Сайт:</strong>
<span><a href="https://dexarmarket.ru" target="_blank">dexarmarket.ru</a></span>
</div>
<div class="detail-item">
<strong>Часы работы офиса:</strong>
<span>10:00 - 19:00 (МСК)</span>
</div>
<div class="detail-item">
<strong>Техподдержка:</strong>
<span>24/7</span>
</div>
</div>
</section>
<section class="legal-section">
<h2>Руководство</h2>
<p><strong>Генеральный директор:</strong> Оганнисян Ашот Рафикович</p>
<p><strong>Основание действий:</strong> Устав</p>
</section>
</div>

View File

@@ -1,113 +1,117 @@
<h1>Payment Terms</h1>
<section class="legal-section">
<h2>1. General Provisions</h2>
<p>1.1. These Terms define the payment procedures for Goods and Services purchased by Buyers through the DexarMarket Marketplace.</p>
<p>1.2. Payment is made for Goods/Services listed by independent Sellers. The Marketplace acts as an information intermediary and provides the technical infrastructure for processing payments.</p>
<p>1.3. Payment for goods and services on the Marketplace is made in Russian rubles (RUB).</p>
<p>1.4. Prices for Goods/Services are set by Sellers independently and are indicated on the respective Goods/Services page.</p>
</section>
<section class="legal-section">
<h2>2. Payment Methods</h2>
<p>2.1. The Marketplace supports the following payment methods:</p>
<div class="payment-methods">
<div class="payment-logos">
<img src="/assets/images/mir-logo.svg" alt="MIR" loading="lazy" width="80" height="50" />
<img src="/assets/images/visa-logo.svg" alt="Visa" loading="lazy" width="80" height="50" />
<img src="/assets/images/mastercard-logo.svg" alt="Mastercard" loading="lazy" width="80" height="50" />
</div>
<div class="legal-page">
<div class="legal-container">
<h1>Payment Terms</h1>
<section class="legal-section">
<h2>1. General Provisions</h2>
<p>1.1. These Terms define the payment procedures for Goods and Services purchased by Buyers through the DexarMarket Marketplace.</p>
<p>1.2. Payment is made for Goods/Services listed by independent Sellers. The Marketplace acts as an information intermediary and provides the technical infrastructure for processing payments.</p>
<p>1.3. Payment for goods and services on the Marketplace is made in Russian rubles (RUB).</p>
<p>1.4. Prices for Goods/Services are set by Sellers independently and are indicated on the respective Goods/Services page.</p>
</section>
<section class="legal-section">
<h2>2. Payment Methods</h2>
<p>2.1. The Marketplace supports the following payment methods:</p>
<div class="payment-methods">
<div class="payment-logos">
<img src="/assets/images/mir-logo.svg" alt="MIR" loading="lazy" width="80" height="50" />
<img src="/assets/images/visa-logo.svg" alt="Visa" loading="lazy" width="80" height="50" />
<img src="/assets/images/mastercard-logo.svg" alt="Mastercard" loading="lazy" width="80" height="50" />
</div>
</div>
<ul>
<li><strong>Bank cards:</strong> Visa, Mastercard, MIR</li>
<li><strong>Fast Payment System (FPS):</strong> instant transfer via mobile banking app</li>
<li><strong>E-wallets:</strong> YooMoney, QIWI (if available)</li>
<li><strong>Payment by link:</strong> generation of a unique payment link for each order</li>
</ul>
<p>2.2. Available payment methods may vary depending on the Seller and type of Goods/Services.</p>
<p>2.3. All payments are processed through certified payment systems in compliance with PCI DSS security standards.</p>
</section>
<section class="legal-section">
<h2>3. Payment Process</h2>
<p>3.1. The order payment procedure includes the following steps:</p>
<ol>
<li>Selecting Goods/Services and adding them to the cart</li>
<li>Placing an order with contact details and delivery method (if applicable)</li>
<li>Choosing a payment method from available options</li>
<li>Redirecting to a secure payment system page or receiving a payment link</li>
<li>Entering payment details and confirming payment</li>
<li>Receiving a successful payment notification</li>
</ol>
<p>3.2. When paying by bank card, the Buyer may be redirected to the issuing bank's page for additional authentication (3D-Secure).</p>
<p>3.3. The Buyer's payment obligation is considered fulfilled upon receipt of funds by the payment system.</p>
</section>
<section class="legal-section">
<h2>4. Payment Security</h2>
<p>4.1. All payments are processed through a secure HTTPS connection using TLS 1.2 protocol and above.</p>
<p>4.2. The Marketplace does not store full bank card data of Buyers. Payment data processing is carried out by certified payment aggregators.</p>
<p>4.3. 3D-Secure technology is used to protect against fraud, requiring payment confirmation via SMS code or push notification from the bank.</p>
<p>4.4. In case of suspicious activity, the payment system reserves the right to request additional identity verification of the Buyer.</p>
</section>
<section class="legal-section">
<h2>5. Payment Confirmation</h2>
<p>5.1. After successful payment, the Buyer receives a confirmation to the email address provided during order placement.</p>
<p>5.2. The confirmation contains the following information:</p>
<ul>
<li>Order number</li>
<li>Date and time of payment</li>
<li>Payment amount</li>
<li>Order contents</li>
<li>Seller contact details</li>
</ul>
<p>5.3. Order information is also displayed in the Buyer's personal account on the Marketplace (if registered).</p>
<p>5.4. A fiscal receipt is sent by the Seller in accordance with the requirements of RF legislation.</p>
</section>
<section class="legal-section">
<h2>6. Refunds</h2>
<p>6.1. The refund procedure is governed by the <a [routerLink]="'/return-policy' | langRoute">Return Policy</a> and depends on the type of Goods/Services purchased.</p>
<p>6.2. Refunds are made to the same payment instrument used for the original payment.</p>
<p>6.3. Refund processing times are as follows:</p>
<ul>
<li>To a bank card: 3 to 30 banking days (depending on the issuing bank)</li>
<li>To an e-wallet: 1 to 5 business days</li>
<li>Via FPS: 1 to 3 business days</li>
</ul>
<p>6.4. The Marketplace does not charge a commission for processing refunds. Payment system and bank fees may apply in accordance with their tariffs.</p>
</section>
<section class="legal-section">
<h2>7. Failed Payments</h2>
<p>7.1. A payment may be declined for the following reasons:</p>
<ul>
<li>Insufficient funds in the account</li>
<li>Incorrectly entered payment details</li>
<li>Card is blocked or expired</li>
<li>Transaction limits set by the bank have been exceeded</li>
<li>Transaction rejected by the security system</li>
</ul>
<p>7.2. In case of an unsuccessful payment, the Buyer receives a notification indicating the reason for the decline.</p>
<p>7.3. If you experience payment issues, it is recommended to:</p>
<ul>
<li>Verify the accuracy of the entered data</li>
<li>Contact the card-issuing bank to clarify the reason for the decline</li>
<li>Try an alternative payment method</li>
<li>Contact support: <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></li>
</ul>
</section>
<section class="legal-section">
<h2>8. Payment Inquiries Contact</h2>
<p>For questions related to order payments, you can contact us:</p>
<ul>
<li><strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></li>
<li><strong>Working hours:</strong> 24/7 (technical support)</li>
<li><strong>Average response time:</strong> Up to 24 hours on business days</li>
</ul>
<p>When contacting us, please provide your order number and a brief description of the issue for a faster resolution.</p>
</section>
</div>
<ul>
<li><strong>Bank cards:</strong> Visa, Mastercard, MIR</li>
<li><strong>Fast Payment System (FPS):</strong> instant transfer via mobile banking app</li>
<li><strong>E-wallets:</strong> YooMoney, QIWI (if available)</li>
<li><strong>Payment by link:</strong> generation of a unique payment link for each order</li>
</ul>
<p>2.2. Available payment methods may vary depending on the Seller and type of Goods/Services.</p>
<p>2.3. All payments are processed through certified payment systems in compliance with PCI DSS security standards.</p>
</section>
<section class="legal-section">
<h2>3. Payment Process</h2>
<p>3.1. The order payment procedure includes the following steps:</p>
<ol>
<li>Selecting Goods/Services and adding them to the cart</li>
<li>Placing an order with contact details and delivery method (if applicable)</li>
<li>Choosing a payment method from available options</li>
<li>Redirecting to a secure payment system page or receiving a payment link</li>
<li>Entering payment details and confirming payment</li>
<li>Receiving a successful payment notification</li>
</ol>
<p>3.2. When paying by bank card, the Buyer may be redirected to the issuing bank's page for additional authentication (3D-Secure).</p>
<p>3.3. The Buyer's payment obligation is considered fulfilled upon receipt of funds by the payment system.</p>
</section>
<section class="legal-section">
<h2>4. Payment Security</h2>
<p>4.1. All payments are processed through a secure HTTPS connection using TLS 1.2 protocol and above.</p>
<p>4.2. The Marketplace does not store full bank card data of Buyers. Payment data processing is carried out by certified payment aggregators.</p>
<p>4.3. 3D-Secure technology is used to protect against fraud, requiring payment confirmation via SMS code or push notification from the bank.</p>
<p>4.4. In case of suspicious activity, the payment system reserves the right to request additional identity verification of the Buyer.</p>
</section>
<section class="legal-section">
<h2>5. Payment Confirmation</h2>
<p>5.1. After successful payment, the Buyer receives a confirmation to the email address provided during order placement.</p>
<p>5.2. The confirmation contains the following information:</p>
<ul>
<li>Order number</li>
<li>Date and time of payment</li>
<li>Payment amount</li>
<li>Order contents</li>
<li>Seller contact details</li>
</ul>
<p>5.3. Order information is also displayed in the Buyer's personal account on the Marketplace (if registered).</p>
<p>5.4. A fiscal receipt is sent by the Seller in accordance with the requirements of RF legislation.</p>
</section>
<section class="legal-section">
<h2>6. Refunds</h2>
<p>6.1. The refund procedure is governed by the <a [routerLink]="'/return-policy' | langRoute">Return Policy</a> and depends on the type of Goods/Services purchased.</p>
<p>6.2. Refunds are made to the same payment instrument used for the original payment.</p>
<p>6.3. Refund processing times are as follows:</p>
<ul>
<li>To a bank card: 3 to 30 banking days (depending on the issuing bank)</li>
<li>To an e-wallet: 1 to 5 business days</li>
<li>Via FPS: 1 to 3 business days</li>
</ul>
<p>6.4. The Marketplace does not charge a commission for processing refunds. Payment system and bank fees may apply in accordance with their tariffs.</p>
</section>
<section class="legal-section">
<h2>7. Failed Payments</h2>
<p>7.1. A payment may be declined for the following reasons:</p>
<ul>
<li>Insufficient funds in the account</li>
<li>Incorrectly entered payment details</li>
<li>Card is blocked or expired</li>
<li>Transaction limits set by the bank have been exceeded</li>
<li>Transaction rejected by the security system</li>
</ul>
<p>7.2. In case of an unsuccessful payment, the Buyer receives a notification indicating the reason for the decline.</p>
<p>7.3. If you experience payment issues, it is recommended to:</p>
<ul>
<li>Verify the accuracy of the entered data</li>
<li>Contact the card-issuing bank to clarify the reason for the decline</li>
<li>Try an alternative payment method</li>
<li>Contact support: <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></li>
</ul>
</section>
<section class="legal-section">
<h2>8. Payment Inquiries Contact</h2>
<p>For questions related to order payments, you can contact us:</p>
<ul>
<li><strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></li>
<li><strong>Working hours:</strong> 24/7 (technical support)</li>
<li><strong>Average response time:</strong> Up to 24 hours on business days</li>
</ul>
<p>When contacting us, please provide your order number and a brief description of the issue for a faster resolution.</p>
</section>
</div>

View File

@@ -1,113 +1,117 @@
<h1>Վճարման կանոններ</h1>
<section class="legal-section">
<h2>1. Ընդհանուր դրույթներ</h2>
<p>1.1. Սույն Կանոնները սահմանում են DexarMarket Մարկետփլեյսի միջոցով Գնորդների կողմից ձեռք բերված Ապրանքների և Ծառայությունների վճարման կարգը։</p>
<p>1.2. Վճարումը կատարվում է անկախ Վաճառողների կողմից տեղադրված Ապրանքների/Ծառայությունների համար։ Մարկետփլեյսը հանդես է գալիս որպես տեղեկատվական միջնորդ և ապահովում է վճարումների կատարման տեխնիկական ենթակառուցվածքը։</p>
<p>1.3. Մարկետփլեյսում ապրանքների և ծառայությունների վճարումը կատարվում է ռուսական ռուբլով (RUB)։</p>
<p>1.4. Ապրանքների/Ծառայությունների գները սահմանվում են Վաճառողների կողմից ինքնուրույն և նշված են համապատասխան Ապրանքի/Ծառայության էջում։</p>
</section>
<section class="legal-section">
<h2>2. Վճարման եղանակներ</h2>
<p>2.1. Մարկետփլեյսը աջակցում է վճարման հետևյալ եղանակները՝</p>
<div class="payment-methods">
<div class="payment-logos">
<img src="/assets/images/mir-logo.svg" alt="ՄԻՐ" loading="lazy" width="80" height="50" />
<img src="/assets/images/visa-logo.svg" alt="Visa" loading="lazy" width="80" height="50" />
<img src="/assets/images/mastercard-logo.svg" alt="Mastercard" loading="lazy" width="80" height="50" />
</div>
<div class="legal-page">
<div class="legal-container">
<h1>Վճարման կանոններ</h1>
<section class="legal-section">
<h2>1. Ընդհանուր դրույթներ</h2>
<p>1.1. Սույն Կանոնները սահմանում են DexarMarket Մարկետփլեյսի միջոցով Գնորդների կողմից ձեռք բերված Ապրանքների և Ծառայությունների վճարման կարգը։</p>
<p>1.2. Վճարումը կատարվում է անկախ Վաճառողների կողմից տեղադրված Ապրանքների/Ծառայությունների համար։ Մարկետփլեյսը հանդես է գալիս որպես տեղեկատվական միջնորդ և ապահովում է վճարումների կատարման տեխնիկական ենթակառուցվածքը։</p>
<p>1.3. Մարկետփլեյսում ապրանքների և ծառայությունների վճարումը կատարվում է ռուսական ռուբլով (RUB)։</p>
<p>1.4. Ապրանքների/Ծառայությունների գները սահմանվում են Վաճառողների կողմից ինքնուրույն և նշված են համապատասխան Ապրանքի/Ծառայության էջում։</p>
</section>
<section class="legal-section">
<h2>2. Վճարման եղանակներ</h2>
<p>2.1. Մարկետփլեյսը աջակցում է վճարման հետևյալ եղանակները՝</p>
<div class="payment-methods">
<div class="payment-logos">
<img src="/assets/images/mir-logo.svg" alt="ՄԻՐ" loading="lazy" width="80" height="50" />
<img src="/assets/images/visa-logo.svg" alt="Visa" loading="lazy" width="80" height="50" />
<img src="/assets/images/mastercard-logo.svg" alt="Mastercard" loading="lazy" width="80" height="50" />
</div>
</div>
<ul>
<li><strong>Բանկային քարտեր՝</strong> Visa, Mastercard, ՄԻՐ</li>
<li><strong>Արագ վճարման համակարգ (ՍԲՊ)՝</strong> ակնթարթ փոխանցում բանկի բջջային հավելվածի միջոցով</li>
<li><strong>Էլեկտրոնային դրամապանակներ՝</strong> ՅուMoney, QIWI (առկայության դեպքում)</li>
<li><strong>Վճարում հղման միջոցով՝</strong> յուրաքանչյուր պատվերի համար եզակի վճարման հղման ստեղծում</li>
</ul>
<p>2.2. Հասանելի վճարման եղանակները կարող են տարբերվել կախված Վաճառողից և Ապրանքի/Ծառայության տեսակից։</p>
<p>2.3. Բոլոր վճարումները մշակվում են սերտիֆիկացված վճարային համակարգերի միջոցով՝ PCI DSS անվտանգության ստանդարտներին համապատասխան։</p>
</section>
<section class="legal-section">
<h2>3. Վճարման գործընթացը</h2>
<p>3.1. Պատվերի վճարման գործընթացը ներառում է հետևյալ քայլերը՝</p>
<ol>
<li>Ապրանքների/Ծառայությունների ընտրություն և դրանց ավելացնել զամբյուղի մեջ</li>
<li>Պատվերի ձևակերպում՝ կապի տվյալների և առաքման եղանակի նշումով (անհրաժեշտության դեպքում)</li>
<li>Վճարման եղանակի ընտրություն հասանելի տարբերակներից</li>
<li>Վերահղորդում վճարային համակարգի պաշտպանված էջ կամ վճարման հղման ստացում</li>
<li>Վճարման տվյալների մուտքագրում և վճարման հաստատում</li>
<li>Հաջող վճարման մասին ծանուցման ստացում</li>
</ol>
<p>3.2. Բանկային քարտով վճարելիս Գնորդը կարող է վերահղորդվել թողարկող բանկի էջ՝ լրացուցիչ նույնականացման համար (3D-Secure)։</p>
<p>3.3. Գնորդի վճարման պարտավորությունը համարվում է կատարված վճարային համակարգի հաշվին դրամական միջոցների մուտքի պահից։</p>
</section>
<section class="legal-section">
<h2>4. Վճարումների անվտանգություն</h2>
<p>4.1. Բոլոր վճարումները մշակվում են պաշտպանված HTTPS կապի միջոցով՝ TLS 1.2 և ավելի բարձր պրոտոկոլի օգտագործմամբ։</p>
<p>4.2. Մարկետփլեյսը չի պահպանում Գնորդների բանկային քարտերի լիարժեկ տվյալները։ Վճարային տվյալների մշակումը կատարվում է սերտիֆիկացված վճարային ագրեգատորների կողմից։</p>
<p>4.3. Խարդախությունից պաշտպանության համար կիրառվում է 3D-Secure տեխնոլոգիան՝ վճարումը հաստատելու համար SMS կոդի կամ բանկի push ծանուցման միջոցով։</p>
<p>4.4. Կասկածելի գործողության դեպքում վճարային համակարգը իրավունք ունի պահանջելու Գնորդի ինքնության լրացուցիչ ստուգում։</p>
</section>
<section class="legal-section">
<h2>5. Վճարման հաստատում</h2>
<p>5.1. Հաջող վճարմանից հետո Գնորդը ստանում է հաստատում պատվերի ձևակերպման ժամանակ նշված էլեկտրոնային փոստի հասցեին։</p>
<p>5.2. Հաստատումը պարունակում է հետևյալ տեղեկատվությունը՝</p>
<ul>
<li>Պատվերի համարը</li>
<li>Վճարման ամսաթիվը և ժամը</li>
<li>Վճարման գումարը</li>
<li>Պատվերի բովանդակությունը</li>
<li>Վաճառողի կապի տվյալները</li>
</ul>
<p>5.3. Պատվերի մասին տեղեկատվությունը նաև ցուցադրվում է Գնորդի անձնական հաշվից Մարկետփլեյսում (գրանցման դեպքում)։</p>
<p>5.4. Ֆիսկալ կտրոնը ուղարկվում է Վաճառողի կողմից՝ ՌՀ օրենսդրության պահանջներին համապատասխան։</p>
</section>
<section class="legal-section">
<h2>6. Միջոցների վերադարձ</h2>
<p>6.1. Դրամական միջոցների վերադարձի կարգը կարգավորվում է <a [routerLink]="'/return-policy' | langRoute">Վերադարձի քաղաքականությամբ</a> և կախված է ձեռք բերված Ապրանքի/Ծառայության տեսակից։</p>
<p>6.2. Միջոցների վերադարձը կատարվում է նույն վճարային գործիքին՝ որից կատարվել էր վճարումը։</p>
<p>6.3. Դրամական միջոցների վերադարձի ժամկետները՝</p>
<ul>
<li>Բանկային քարտին՝ 3-ից 30 բանկային օր (կախված թողարկող բանկից)</li>
<li>Էլեկտրոնային դրամապանակին՝ 1-ից 5 աշխատանքային օր</li>
<li>ՍԲՊ-ի միջոցով՝ 1-ից 3 աշխատանքային օր</li>
</ul>
<p>6.4. Վերադարձի մշակման համար Մարկետփլեյսը միջնորդավճար չի գանձում։ Վճարային համակարգերի և բանկերի միջնորդավճարները կարող են կիրառվել իրենց սակագներին համապատասխան։</p>
</section>
<section class="legal-section">
<h2>7. Անհաջող վճարումներ</h2>
<p>7.1. Վճարումը կարող է մերժվել հետևյալ պատճառներով՝</p>
<ul>
<li>Հաշվին անբավարար միջոցներ</li>
<li>Վճարային տվյալները սխալ են մուտքագրված</li>
<li>Քարտը արգելափակված է կամ ժամկետն անցած է</li>
<li>Բանկի կողմից սահմանված գործարքների սահմանափակումները գերազանցված են</li>
<li>Անվտանգության համակարգի կողմից գործարքի մերժում</li>
</ul>
<p>7.2. Անհաջող վճարման դեպքում Գնորդը ստանում է ծանուցում՝ մերժման պատճառի նշումով։</p>
<p>7.3. Վճարման խնդիրների դեպքում խորհուրդ է տրվում՝</p>
<ul>
<li>Ստուգել մուտքագրված տվյալների ճշտությունը</li>
<li>Կապվել քարտ թողարկող բանկի հետ՝ մերժման պատճառը ճշտելու համար</li>
<li>Փորձել այլընտրանքային վճարման եղանակ</li>
<li>Դիմել աջակցության ծառայությանը՝ <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></li>
</ul>
</section>
<section class="legal-section">
<h2>8. Վճարման հարցերի կապի տեղեկատվություն</h2>
<p>Պատվերների վճարման հետ կապված հարցերի համար կարող եք դիմել՝</p>
<ul>
<li><strong>Էլ. փոստ՝</strong> <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></li>
<li><strong>Աշխատանքային ժամեր՝</strong> Շուրջօրյա (տեխնիկական աջակցություն)</li>
<li><strong>Միջին պատասխանի ժամը՝</strong> Մինչև 24 ժամ աշխատանքային օրերին</li>
</ul>
<p>Դիմելիս նշեք պատվերի համարը և խնդրի հակիրճ նկարագրությունը՝ հարցի ավելի արագ լուծման համար։</p>
</section>
</div>
<ul>
<li><strong>Բանկային քարտեր՝</strong> Visa, Mastercard, ՄԻՐ</li>
<li><strong>Արագ վճարման համակարգ (ՍԲՊ)՝</strong> ակնթարթ փոխանցում բանկի բջջային հավելվածի միջոցով</li>
<li><strong>Էլեկտրոնային դրամապանակներ՝</strong> ՅուMoney, QIWI (առկայության դեպքում)</li>
<li><strong>Վճարում հղման միջոցով՝</strong> յուրաքանչյուր պատվերի համար եզակի վճարման հղման ստեղծում</li>
</ul>
<p>2.2. Հասանելի վճարման եղանակները կարող են տարբերվել կախված Վաճառողից և Ապրանքի/Ծառայության տեսակից։</p>
<p>2.3. Բոլոր վճարումները մշակվում են սերտիֆիկացված վճարային համակարգերի միջոցով՝ PCI DSS անվտանգության ստանդարտներին համապատասխան։</p>
</section>
<section class="legal-section">
<h2>3. Վճարման գործընթացը</h2>
<p>3.1. Պատվերի վճարման գործընթացը ներառում է հետևյալ քայլերը՝</p>
<ol>
<li>Ապրանքների/Ծառայությունների ընտրություն և դրանց ավելացնել զամբյուղի մեջ</li>
<li>Պատվերի ձևակերպում՝ կապի տվյալների և առաքման եղանակի նշումով (անհրաժեշտության դեպքում)</li>
<li>Վճարման եղանակի ընտրություն հասանելի տարբերակներից</li>
<li>Վերահղորդում վճարային համակարգի պաշտպանված էջ կամ վճարման հղման ստացում</li>
<li>Վճարման տվյալների մուտքագրում և վճարման հաստատում</li>
<li>Հաջող վճարման մասին ծանուցման ստացում</li>
</ol>
<p>3.2. Բանկային քարտով վճարելիս Գնորդը կարող է վերահղորդվել թողարկող բանկի էջ՝ լրացուցիչ նույնականացման համար (3D-Secure)։</p>
<p>3.3. Գնորդի վճարման պարտավորությունը համարվում է կատարված վճարային համակարգի հաշվին դրամական միջոցների մուտքի պահից։</p>
</section>
<section class="legal-section">
<h2>4. Վճարումների անվտանգություն</h2>
<p>4.1. Բոլոր վճարումները մշակվում են պաշտպանված HTTPS կապի միջոցով՝ TLS 1.2 և ավելի բարձր պրոտոկոլի օգտագործմամբ։</p>
<p>4.2. Մարկետփլեյսը չի պահպանում Գնորդների բանկային քարտերի լիարժեկ տվյալները։ Վճարային տվյալների մշակումը կատարվում է սերտիֆիկացված վճարային ագրեգատորների կողմից։</p>
<p>4.3. Խարդախությունից պաշտպանության համար կիրառվում է 3D-Secure տեխնոլոգիան՝ վճարումը հաստատելու համար SMS կոդի կամ բանկի push ծանուցման միջոցով։</p>
<p>4.4. Կասկածելի գործողության դեպքում վճարային համակարգը իրավունք ունի պահանջելու Գնորդի ինքնության լրացուցիչ ստուգում։</p>
</section>
<section class="legal-section">
<h2>5. Վճարման հաստատում</h2>
<p>5.1. Հաջող վճարմանից հետո Գնորդը ստանում է հաստատում պատվերի ձևակերպման ժամանակ նշված էլեկտրոնային փոստի հասցեին։</p>
<p>5.2. Հաստատումը պարունակում է հետևյալ տեղեկատվությունը՝</p>
<ul>
<li>Պատվերի համարը</li>
<li>Վճարման ամսաթիվը և ժամը</li>
<li>Վճարման գումարը</li>
<li>Պատվերի բովանդակությունը</li>
<li>Վաճառողի կապի տվյալները</li>
</ul>
<p>5.3. Պատվերի մասին տեղեկատվությունը նաև ցուցադրվում է Գնորդի անձնական հաշվից Մարկետփլեյսում (գրանցման դեպքում)։</p>
<p>5.4. Ֆիսկալ կտրոնը ուղարկվում է Վաճառողի կողմից՝ ՌՀ օրենսդրության պահանջներին համապատասխան։</p>
</section>
<section class="legal-section">
<h2>6. Միջոցների վերադարձ</h2>
<p>6.1. Դրամական միջոցների վերադարձի կարգը կարգավորվում է <a [routerLink]="'/return-policy' | langRoute">Վերադարձի քաղաքականությամբ</a> և կախված է ձեռք բերված Ապրանքի/Ծառայության տեսակից։</p>
<p>6.2. Միջոցների վերադարձը կատարվում է նույն վճարային գործիքին՝ որից կատարվել էր վճարումը։</p>
<p>6.3. Դրամական միջոցների վերադարձի ժամկետները՝</p>
<ul>
<li>Բանկային քարտին՝ 3-ից 30 բանկային օր (կախված թողարկող բանկից)</li>
<li>Էլեկտրոնային դրամապանակին՝ 1-ից 5 աշխատանքային օր</li>
<li>ՍԲՊ-ի միջոցով՝ 1-ից 3 աշխատանքային օր</li>
</ul>
<p>6.4. Վերադարձի մշակման համար Մարկետփլեյսը միջնորդավճար չի գանձում։ Վճարային համակարգերի և բանկերի միջնորդավճարները կարող են կիրառվել իրենց սակագներին համապատասխան։</p>
</section>
<section class="legal-section">
<h2>7. Անհաջող վճարումներ</h2>
<p>7.1. Վճարումը կարող է մերժվել հետևյալ պատճառներով՝</p>
<ul>
<li>Հաշվին անբավարար միջոցներ</li>
<li>Վճարային տվյալները սխալ են մուտքագրված</li>
<li>Քարտը արգելափակված է կամ ժամկետն անցած է</li>
<li>Բանկի կողմից սահմանված գործարքների սահմանափակումները գերազանցված են</li>
<li>Անվտանգության համակարգի կողմից գործարքի մերժում</li>
</ul>
<p>7.2. Անհաջող վճարման դեպքում Գնորդը ստանում է ծանուցում՝ մերժման պատճառի նշումով։</p>
<p>7.3. Վճարման խնդիրների դեպքում խորհուրդ է տրվում՝</p>
<ul>
<li>Ստուգել մուտքագրված տվյալների ճշտությունը</li>
<li>Կապվել քարտ թողարկող բանկի հետ՝ մերժման պատճառը ճշտելու համար</li>
<li>Փորձել այլընտրանքային վճարման եղանակ</li>
<li>Դիմել աջակցության ծառայությանը՝ <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></li>
</ul>
</section>
<section class="legal-section">
<h2>8. Վճարման հարցերի կապի տեղեկատվություն</h2>
<p>Պատվերների վճարման հետ կապված հարցերի համար կարող եք դիմել՝</p>
<ul>
<li><strong>Էլ. փոստ՝</strong> <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></li>
<li><strong>Աշխատանքային ժամեր՝</strong> Շուրջօրյա (տեխնիկական աջակցություն)</li>
<li><strong>Միջին պատասխանի ժամը՝</strong> Մինչև 24 ժամ աշխատանքային օրերին</li>
</ul>
<p>Դիմելիս նշեք պատվերի համարը և խնդրի հակիրճ նկարագրությունը՝ հարցի ավելի արագ լուծման համար։</p>
</section>
</div>

View File

@@ -1,113 +1,117 @@
<h1>Правила оплаты</h1>
<section class="legal-section">
<h2>1. Общие положения</h2>
<p>1.1. Настоящие Правила определяют порядок оплаты Товаров и Услуг, приобретаемых Покупателями через Маркетплейс DexarMarket.</p>
<p>1.2. Оплата производится за Товары/Услуги, размещенные независимыми Продавцами. Маркетплейс выступает в качестве информационного посредника и обеспечивает техническую инфраструктуру для проведения платежей.</p>
<p>1.3. Оплата товаров и услуг на Маркетплейсе осуществляется в российских рублях (RUB).</p>
<p>1.4. Цены на Товары/Услуги устанавливаются Продавцами самостоятельно и указываются на странице соответствующего Товара/Услуги.</p>
</section>
<section class="legal-section">
<h2>2. Способы оплаты</h2>
<p>2.1. Маркетплейс поддерживает следующие способы оплаты:</p>
<div class="payment-methods">
<div class="payment-logos">
<img src="/assets/images/mir-logo.svg" alt="МИР" loading="lazy" width="80" height="50" />
<img src="/assets/images/visa-logo.svg" alt="Visa" loading="lazy" width="80" height="50" />
<img src="/assets/images/mastercard-logo.svg" alt="Mastercard" loading="lazy" width="80" height="50" />
</div>
<div class="legal-page">
<div class="legal-container">
<h1>Правила оплаты</h1>
<section class="legal-section">
<h2>1. Общие положения</h2>
<p>1.1. Настоящие Правила определяют порядок оплаты Товаров и Услуг, приобретаемых Покупателями через Маркетплейс DexarMarket.</p>
<p>1.2. Оплата производится за Товары/Услуги, размещенные независимыми Продавцами. Маркетплейс выступает в качестве информационного посредника и обеспечивает техническую инфраструктуру для проведения платежей.</p>
<p>1.3. Оплата товаров и услуг на Маркетплейсе осуществляется в российских рублях (RUB).</p>
<p>1.4. Цены на Товары/Услуги устанавливаются Продавцами самостоятельно и указываются на странице соответствующего Товара/Услуги.</p>
</section>
<section class="legal-section">
<h2>2. Способы оплаты</h2>
<p>2.1. Маркетплейс поддерживает следующие способы оплаты:</p>
<div class="payment-methods">
<div class="payment-logos">
<img src="/assets/images/mir-logo.svg" alt="МИР" loading="lazy" width="80" height="50" />
<img src="/assets/images/visa-logo.svg" alt="Visa" loading="lazy" width="80" height="50" />
<img src="/assets/images/mastercard-logo.svg" alt="Mastercard" loading="lazy" width="80" height="50" />
</div>
</div>
<ul>
<li><strong>Банковские карты:</strong> Visa, Mastercard, МИР</li>
<li><strong>Системы быстрых платежей (СБП):</strong> мгновенный перевод через мобильное приложение банка</li>
<li><strong>Электронные кошельки:</strong> ЮMoney, QIWI (при наличии)</li>
<li><strong>Оплата по ссылке:</strong> генерация уникальной платежной ссылки для каждого заказа</li>
</ul>
<p>2.2. Доступные способы оплаты могут различаться в зависимости от Продавца и типа Товара/Услуги.</p>
<p>2.3. Все платежи обрабатываются через сертифицированные платежные системы с соблюдением стандартов безопасности PCI DSS.</p>
</section>
<section class="legal-section">
<h2>3. Процесс оплаты</h2>
<p>3.1. Процедура оплаты заказа включает следующие этапы:</p>
<ol>
<li>Выбор Товаров/Услуг и добавление их в корзину</li>
<li>Оформление заказа с указанием контактных данных и способа доставки (при необходимости)</li>
<li>Выбор способа оплаты из доступных вариантов</li>
<li>Перенаправление на защищенную страницу платежной системы или получение платежной ссылки</li>
<li>Ввод платежных данных и подтверждение оплаты</li>
<li>Получение уведомления об успешной оплате</li>
</ol>
<p>3.2. При оплате банковской картой Покупатель может быть перенаправлен на страницу банка-эмитента для прохождения дополнительной аутентификации (3D-Secure).</p>
<p>3.3. Обязательство Покупателя по оплате считается исполненным с момента поступления денежных средств на счет платежной системы.</p>
</section>
<section class="legal-section">
<h2>4. Безопасность платежей</h2>
<p>4.1. Все платежи обрабатываются через защищенное HTTPS-соединение с использованием протокола TLS 1.2 и выше.</p>
<p>4.2. Маркетплейс не хранит полные данные банковских карт Покупателей. Обработка платежных данных осуществляется сертифицированными платежными агрегаторами.</p>
<p>4.3. Для защиты от мошенничества применяется технология 3D-Secure, требующая подтверждения платежа через SMS-код или push-уведомление от банка.</p>
<p>4.4. В случае подозрительной активности платежная система имеет право запросить дополнительную верификацию личности Покупателя.</p>
</section>
<section class="legal-section">
<h2>5. Подтверждение оплаты</h2>
<p>5.1. После успешной оплаты Покупатель получает подтверждение на указанный при оформлении заказа адрес электронной почты.</p>
<p>5.2. Подтверждение содержит следующую информацию:</p>
<ul>
<li>Номер заказа</li>
<li>Дата и время оплаты</li>
<li>Сумма платежа</li>
<li>Состав заказа</li>
<li>Контактные данные Продавца</li>
</ul>
<p>5.3. Информация о заказе также отображается в личном кабинете Покупателя на Маркетплейсе (при наличии регистрации).</p>
<p>5.4. Фискальный чек направляется Продавцом в соответствии с требованиями законодательства РФ.</p>
</section>
<section class="legal-section">
<h2>6. Возврат средств</h2>
<p>6.1. Порядок возврата денежных средств регулируется <a [routerLink]="'/return-policy' | langRoute">Политикой возврата</a> и зависит от типа приобретенного Товара/Услуги.</p>
<p>6.2. Возврат средств производится на тот же платежный инструмент, с которого была произведена оплата.</p>
<p>6.3. Срок возврата денежных средств составляет:</p>
<ul>
<li>На банковскую карту: от 3 до 30 банковских дней (в зависимости от банка-эмитента)</li>
<li>На электронный кошелек: от 1 до 5 рабочих дней</li>
<li>Через СБП: от 1 до 3 рабочих дней</li>
</ul>
<p>6.4. За обработку возврата средств Маркетплейс комиссию не взимает. Комиссии платежных систем и банков могут применяться в соответствии с их тарифами.</p>
</section>
<section class="legal-section">
<h2>7. Неуспешные платежи</h2>
<p>7.1. Платеж может быть отклонен по следующим причинам:</p>
<ul>
<li>Недостаточно средств на счете</li>
<li>Неверно введены платежные данные</li>
<li>Карта заблокирована или просрочена</li>
<li>Превышены лимиты на операции, установленные банком</li>
<li>Отказ в проведении транзакции системой безопасности</li>
</ul>
<p>7.2. В случае неуспешной оплаты Покупатель получает уведомление с указанием причины отказа.</p>
<p>7.3. При возникновении проблем с оплатой рекомендуется:</p>
<ul>
<li>Проверить правильность введенных данных</li>
<li>Связаться с банком-эмитентом карты для уточнения причины отказа</li>
<li>Попробовать альтернативный способ оплаты</li>
<li>Обратиться в службу поддержки: <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></li>
</ul>
</section>
<section class="legal-section">
<h2>8. Контакты для вопросов по оплате</h2>
<p>По вопросам, связанным с оплатой заказов, вы можете обратиться:</p>
<ul>
<li><strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></li>
<li><strong>Время работы:</strong> Круглосуточно (техническая поддержка)</li>
<li><strong>Среднее время ответа:</strong> До 24 часов в рабочие дни</li>
</ul>
<p>При обращении указывайте номер заказа и краткое описание проблемы для более быстрого решения вопроса.</p>
</section>
</div>
<ul>
<li><strong>Банковские карты:</strong> Visa, Mastercard, МИР</li>
<li><strong>Системы быстрых платежей (СБП):</strong> мгновенный перевод через мобильное приложение банка</li>
<li><strong>Электронные кошельки:</strong> ЮMoney, QIWI (при наличии)</li>
<li><strong>Оплата по ссылке:</strong> генерация уникальной платежной ссылки для каждого заказа</li>
</ul>
<p>2.2. Доступные способы оплаты могут различаться в зависимости от Продавца и типа Товара/Услуги.</p>
<p>2.3. Все платежи обрабатываются через сертифицированные платежные системы с соблюдением стандартов безопасности PCI DSS.</p>
</section>
<section class="legal-section">
<h2>3. Процесс оплаты</h2>
<p>3.1. Процедура оплаты заказа включает следующие этапы:</p>
<ol>
<li>Выбор Товаров/Услуг и добавление их в корзину</li>
<li>Оформление заказа с указанием контактных данных и способа доставки (при необходимости)</li>
<li>Выбор способа оплаты из доступных вариантов</li>
<li>Перенаправление на защищенную страницу платежной системы или получение платежной ссылки</li>
<li>Ввод платежных данных и подтверждение оплаты</li>
<li>Получение уведомления об успешной оплате</li>
</ol>
<p>3.2. При оплате банковской картой Покупатель может быть перенаправлен на страницу банка-эмитента для прохождения дополнительной аутентификации (3D-Secure).</p>
<p>3.3. Обязательство Покупателя по оплате считается исполненным с момента поступления денежных средств на счет платежной системы.</p>
</section>
<section class="legal-section">
<h2>4. Безопасность платежей</h2>
<p>4.1. Все платежи обрабатываются через защищенное HTTPS-соединение с использованием протокола TLS 1.2 и выше.</p>
<p>4.2. Маркетплейс не хранит полные данные банковских карт Покупателей. Обработка платежных данных осуществляется сертифицированными платежными агрегаторами.</p>
<p>4.3. Для защиты от мошенничества применяется технология 3D-Secure, требующая подтверждения платежа через SMS-код или push-уведомление от банка.</p>
<p>4.4. В случае подозрительной активности платежная система имеет право запросить дополнительную верификацию личности Покупателя.</p>
</section>
<section class="legal-section">
<h2>5. Подтверждение оплаты</h2>
<p>5.1. После успешной оплаты Покупатель получает подтверждение на указанный при оформлении заказа адрес электронной почты.</p>
<p>5.2. Подтверждение содержит следующую информацию:</p>
<ul>
<li>Номер заказа</li>
<li>Дата и время оплаты</li>
<li>Сумма платежа</li>
<li>Состав заказа</li>
<li>Контактные данные Продавца</li>
</ul>
<p>5.3. Информация о заказе также отображается в личном кабинете Покупателя на Маркетплейсе (при наличии регистрации).</p>
<p>5.4. Фискальный чек направляется Продавцом в соответствии с требованиями законодательства РФ.</p>
</section>
<section class="legal-section">
<h2>6. Возврат средств</h2>
<p>6.1. Порядок возврата денежных средств регулируется <a [routerLink]="'/return-policy' | langRoute">Политикой возврата</a> и зависит от типа приобретенного Товара/Услуги.</p>
<p>6.2. Возврат средств производится на тот же платежный инструмент, с которого была произведена оплата.</p>
<p>6.3. Срок возврата денежных средств составляет:</p>
<ul>
<li>На банковскую карту: от 3 до 30 банковских дней (в зависимости от банка-эмитента)</li>
<li>На электронный кошелек: от 1 до 5 рабочих дней</li>
<li>Через СБП: от 1 до 3 рабочих дней</li>
</ul>
<p>6.4. За обработку возврата средств Маркетплейс комиссию не взимает. Комиссии платежных систем и банков могут применяться в соответствии с их тарифами.</p>
</section>
<section class="legal-section">
<h2>7. Неуспешные платежи</h2>
<p>7.1. Платеж может быть отклонен по следующим причинам:</p>
<ul>
<li>Недостаточно средств на счете</li>
<li>Неверно введены платежные данные</li>
<li>Карта заблокирована или просрочена</li>
<li>Превышены лимиты на операции, установленные банком</li>
<li>Отказ в проведении транзакции системой безопасности</li>
</ul>
<p>7.2. В случае неуспешной оплаты Покупатель получает уведомление с указанием причины отказа.</p>
<p>7.3. При возникновении проблем с оплатой рекомендуется:</p>
<ul>
<li>Проверить правильность введенных данных</li>
<li>Связаться с банком-эмитентом карты для уточнения причины отказа</li>
<li>Попробовать альтернативный способ оплаты</li>
<li>Обратиться в службу поддержки: <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></li>
</ul>
</section>
<section class="legal-section">
<h2>8. Контакты для вопросов по оплате</h2>
<p>По вопросам, связанным с оплатой заказов, вы можете обратиться:</p>
<ul>
<li><strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></li>
<li><strong>Время работы:</strong> Круглосуточно (техническая поддержка)</li>
<li><strong>Среднее время ответа:</strong> До 24 часов в рабочие дни</li>
</ul>
<p>При обращении указывайте номер заказа и краткое описание проблемы для более быстрого решения вопроса.</p>
</section>
</div>

View File

@@ -1,3 +1,5 @@
<div class="legal-page">
<div class="legal-container">
<h1>PERSONAL DATA PROCESSING POLICY</h1>
<section class="legal-section">
@@ -361,3 +363,5 @@
<p>12.3. If the Operator can reasonably associate the information specified in this section with the personal account of a specific User, then such information may be processed together with the PD and other personal information of such User.</p>
</section>
</div>
</div>

View File

@@ -1,3 +1,5 @@
<div class="legal-page">
<div class="legal-container">
<h1>PERSONAL DATA PROCESSING POLICY</h1>
<section class="legal-section">
@@ -361,3 +363,5 @@
<p>12.3. If the Operator can reasonably associate the information specified in this section with the personal account of a specific User, then such information may be processed together with the PD and other personal information of such User.</p>
</section>
</div>
</div>

View File

@@ -1,3 +1,5 @@
<div class="legal-page">
<div class="legal-container">
<h1>ПОЛИТИКА В ОТНОШЕНИИ ОБРАБОТКИ ПЕРСОНАЛЬНЫХ ДАННЫХ</h1>
<section class="legal-section">
@@ -361,3 +363,5 @@
<p>12.3. Если Оператор может разумно соотнести указанные в настоящем разделе сведения с личным кабинетом конкретного Пользователя, то такие сведения могут обрабатываться совместно с ПДн и иной личной информацией такого Пользователя.</p>
</section>
</div>
</div>

View File

@@ -1,3 +1,5 @@
<div class="legal-page">
<div class="legal-container">
<h1>PUBLIC OFFER AGREEMENT</h1>
<section class="legal-section">
@@ -459,3 +461,5 @@
<p><strong>16.8. Response to Violations</strong></p>
<p>Non-intervention by the Site Owner in the event of violations of agreements by Users does not prevent subsequent measures to protect the Owner's interests at a later date.</p>
</section>
</div>
</div>

View File

@@ -1 +1,5 @@
<h1>Հdelays DELAYS ՀԱՄDELAYS</h1>
<div class="legal-page">
<div class="legal-container">
<h1>Հdelays DELAYS ՀԱՄDELAYS</h1>
</div>
</div>

View File

@@ -1,3 +1,5 @@
<div class="legal-page">
<div class="legal-container">
<h1>СОГЛАШЕНИЕ ПУБЛИЧНОЙ ОФЕРТЫ</h1>
<section class="legal-section">
@@ -459,3 +461,5 @@
<p><strong>16.8. Реакция на нарушения</strong></p>
<p>Невмешательство Владельца сайта в случае нарушений соглашений Пользователями не препятствует последующим мерам защиты интересов Владельца позже.</p>
</section>
</div>
</div>

View File

@@ -1,3 +1,5 @@
<div class="legal-page">
<div class="legal-container">
<h1>Return Policy</h1>
<section class="legal-section">
@@ -128,3 +130,5 @@
</ul>
<p>If the conflict cannot be resolved amicably, the Buyer has the right to file a complaint with Rospotrebnadzor or the court at the Seller's location.</p>
</section>
</div>
</div>

View File

@@ -1,3 +1,5 @@
<div class="legal-page">
<div class="legal-container">
<h1>Ապրանքների վերադարձի քաղաքականություն</h1>
<section class="legal-section">
@@ -127,4 +129,6 @@
<li><strong>Անհրաժեշտ նյութեր՝</strong> նամակների պատճեններ՝ ապրանքի լուսանկարներ՝ վճարման կտրոն։</li>
</ul>
<p>Եթե կոնֆլիկտը հնարավոր չէ լուծել խաղաղ ճանապարհով՝ Գնորդը իրավունք ունի բողոք ներկայացնելու Ռոսպոտրեբնաձոր կամ դատարան Վաճառողի գտնվելու վայրում։</p>
</section>
</section>
</div>
</div>

View File

@@ -1,3 +1,5 @@
<div class="legal-page">
<div class="legal-container">
<h1>Политика возврата товаров</h1>
<section class="legal-section">
@@ -128,3 +130,5 @@
</ul>
<p>Если конфликт невозможно разрешить мирно, Покупатель вправе подать жалобу в Роспотребнадзор или суд по месту расположения Продавца.</p>
</section>
</div>
</div>

View File

@@ -56,17 +56,28 @@
@if (items().length > 0) {
<div class="items-grid">
@for (item of items(); track trackByItemId($index, item)) {
<div class="item-card">
<div class="item-card" (mouseenter)="onItemHover(item.itemID)">
<a [routerLink]="['/item', item.itemID] | langRoute" class="item-link">
<div class="item-image">
<img [src]="getMainImage(item)" [alt]="item.name" loading="lazy" decoding="async" width="300" height="300" />
<img [src]="getMainImage(item)" [alt]="itemName(item)" loading="lazy" decoding="async" width="300" height="300" />
@if (item.discount > 0) {
<div class="discount-badge">-{{ item.discount }}%</div>
}
@if (item.badges && item.badges.length > 0) {
<div class="item-badges-overlay">
@for (badge of item.badges; track badge) {
<span class="item-badge" [class]="getBadgeClass(badge)">{{ badge }}</span>
}
</div>
}
</div>
<div class="item-details">
<h3 class="item-name">{{ item.name }}</h3>
<h3 class="item-name">{{ itemName(item) }}</h3>
@if (itemDesc(item)) {
<p class="item-simple-desc">{{ itemDesc(item) }}</p>
}
<div class="item-rating">
<span class="rating-stars">⭐ {{ item.rating }}</span>
@@ -94,19 +105,29 @@
</div>
</a>
<button class="add-to-cart-btn" (click)="addToCart(item.itemID, $event)">
<button class="add-to-cart-btn" (click)="addToCart(item.itemID, $event)" [attr.aria-label]="('search.addToCart' | translate) + ': ' + item.name">
{{ 'search.addToCart' | translate }}
</button>
</div>
}
</div>
@if (loading() && items().length > 0) {
<div class="loading-more">
<div class="spinner"></div>
<p>{{ 'search.loadingMore' | translate }}</p>
</div>
}
@if (loading() && items().length > 0) {
@for (i of skeletonSlots; track i) {
<div class="item-card skeleton-card">
<div class="item-link">
<div class="item-image skeleton-image"></div>
<div class="item-details">
<div class="skeleton-line skeleton-title"></div>
<div class="skeleton-line skeleton-rating"></div>
<div class="skeleton-line skeleton-price"></div>
<div class="skeleton-line skeleton-stock"></div>
</div>
</div>
<div class="skeleton-btn"></div>
</div>
}
}
@if (!hasMore() && items().length > 0) {
<div class="no-more">

View File

@@ -344,6 +344,59 @@
text-align: center;
}
// Skeleton loading cards
.skeleton-card {
pointer-events: none;
.skeleton-image {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.skeleton-line {
border-radius: 6px;
background: linear-gradient(90deg, #e8e8e8 25%, #d8d8d8 50%, #e8e8e8 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.skeleton-title {
height: 16px;
width: 80%;
}
.skeleton-rating {
height: 12px;
width: 50%;
}
.skeleton-price {
height: 18px;
width: 40%;
margin-top: auto;
}
.skeleton-stock {
height: 6px;
width: 60px;
}
.skeleton-btn {
height: 42px;
background: linear-gradient(90deg, #5a8a85 25%, #497671 50%, #5a8a85 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 0 0 13px 13px;
margin-top: -1px;
}
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
@media (max-width: 768px) {
.search-header h1 {
font-size: 1.5rem;

View File

@@ -3,13 +3,16 @@ import { DecimalPipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { ApiService, CartService } from '../../services';
import { PrefetchService } from '../../services/prefetch.service';
import { Item } from '../../models';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { getDiscountedPrice, getMainImage, trackByItemId } from '../../utils/item.utils';
import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass, getTranslatedField } from '../../utils/item.utils';
import { LanguageService } from '../../services/language.service';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service';
import { SEARCH_DEBOUNCE_MS, ITEMS_PER_PAGE, SCROLL_THRESHOLD_PX, SCROLL_DEBOUNCE_MS } from '../../config/constants';
@Component({
selector: 'app-search',
@@ -27,7 +30,7 @@ export class SearchComponent implements OnDestroy {
totalResults = signal<number>(0);
private skip = 0;
private readonly count = 20;
private readonly count = ITEMS_PER_PAGE;
private isLoadingMore = false;
private searchSubject = new Subject<string>();
private searchSubscription: Subscription;
@@ -35,11 +38,12 @@ export class SearchComponent implements OnDestroy {
constructor(
private apiService: ApiService,
private cartService: CartService
private cartService: CartService,
private prefetchService: PrefetchService
) {
this.searchSubscription = this.searchSubject
.pipe(
debounceTime(300),
debounceTime(SEARCH_DEBOUNCE_MS),
distinctUntilChanged()
)
.subscribe(query => {
@@ -63,7 +67,7 @@ export class SearchComponent implements OnDestroy {
performSearch(query: string): void {
if (!query.trim()) {
this.items.set([]);
this.hasMore.set(true);
this.hasMore.set(false);
this.totalResults.set(0);
return;
}
@@ -119,12 +123,12 @@ export class SearchComponent implements OnDestroy {
this.scrollTimeout = setTimeout(() => {
const scrollPosition = window.innerHeight + window.scrollY;
const bottomPosition = document.documentElement.scrollHeight - 500;
const bottomPosition = document.documentElement.scrollHeight - SCROLL_THRESHOLD_PX;
if (scrollPosition >= bottomPosition && !this.loading() && this.hasMore()) {
this.loadResults();
}
}, 100);
}, SCROLL_DEBOUNCE_MS);
}
addToCart(itemID: number, event: Event): void {
@@ -133,7 +137,17 @@ export class SearchComponent implements OnDestroy {
this.cartService.addItem(itemID);
}
onItemHover(itemID: number): void {
this.prefetchService.prefetchItem(itemID);
}
readonly skeletonSlots = Array.from({ length: 8 });
readonly getDiscountedPrice = getDiscountedPrice;
readonly getMainImage = getMainImage;
readonly trackByItemId = trackByItemId;
readonly getBadgeClass = getBadgeClass;
private langService = inject(LanguageService);
itemName(item: Item): string { return getTranslatedField(item, 'name', this.langService.currentLanguage()); }
itemDesc(item: Item): string { return getTranslatedField(item, 'simpleDescription', this.langService.currentLanguage()); }
}

View File

@@ -7,19 +7,30 @@ import { LanguageService } from '../services/language.service';
})
export class LangRoutePipe implements PipeTransform {
private langService = inject(LanguageService);
private lastLang = '';
private lastInput: unknown = null;
private lastResult: string | (string | number)[] = '';
transform(value: string | (string | number)[]): string | (string | number)[] {
const lang = this.langService.currentLanguage();
// Short-circuit if nothing changed
if (lang === this.lastLang && value === this.lastInput) {
return this.lastResult;
}
this.lastLang = lang;
this.lastInput = value;
if (typeof value === 'string') {
return value === '/' ? `/${lang}` : `/${lang}${value}`;
}
if (Array.isArray(value) && value.length > 0) {
this.lastResult = value === '/' ? `/${lang}` : `/${lang}${value}`;
} else if (Array.isArray(value) && value.length > 0) {
const [first, ...rest] = value;
return [`/${lang}${first}`, ...rest];
this.lastResult = [`/${lang}${first}`, ...rest];
} else {
this.lastResult = value;
}
return value;
return this.lastResult;
}
}

View File

@@ -1,72 +1,202 @@
import { Injectable, inject } from '@angular/core';
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Category, Item } from '../models';
import { Observable, timer } from 'rxjs';
import { map, retry } from 'rxjs/operators';
import { Category, Item, Subcategory } from '../models';
import { environment } from '../../environments/environment';
import { LocationService } from './location.service';
@Injectable({
providedIn: 'root'
})
export class ApiService {
private readonly baseUrl = environment.apiUrl;
private locationService = inject(LocationService);
private readonly retryConfig = {
count: 2,
delay: (error: unknown, retryCount: number) => timer(Math.pow(2, retryCount) * 500)
};
constructor(private http: HttpClient) {}
private normalizeItem(item: Item): Item {
return {
...item,
remainings: item.remainings || 'high'
};
/**
* Normalize an item from the API response — supports both
* legacy marketplace format and the new backOffice API format.
*/
private normalizeItem(raw: any): Item {
const { partnerID, ...rest } = raw;
const item: Item = { ...rest };
// Map backOffice string id → legacy numeric itemID
if (raw.id != null && raw.itemID == null) {
item.id = String(raw.id);
item.itemID = typeof raw.id === 'number' ? raw.id : 0;
}
// Map backOffice imgs[] → legacy photos[]
if (raw.imgs && (!raw.photos || raw.photos.length === 0)) {
item.photos = raw.imgs.map((url: string) => ({ url }));
}
// Normalize photo type: API sends type='video'|'photo', template checks .video
if (item.photos) {
item.photos = item.photos.map((p: any) => ({
...p,
video: p.video || (p.type === 'video' ? p.url : undefined),
}));
}
item.imgs = raw.imgs || raw.photos?.map((p: any) => p.url) || [];
// Map backOffice description (key-value array) → legacy description string
if (Array.isArray(raw.description)) {
item.descriptionFields = raw.description;
item.description = raw.description.map((d: any) => `${d.key}: ${d.value}`).join('\n');
} else {
item.description = raw.description || raw.simpleDescription || '';
}
// Map backend names[] → translations (multi-lang name support)
if (raw.names && Array.isArray(raw.names)) {
item.names = raw.names;
if (!item.translations) item.translations = {};
for (const entry of raw.names) {
if (!item.translations[entry.language]) item.translations[entry.language] = {};
item.translations[entry.language].name = entry.value;
}
}
// Map backend descriptions[] → translations (multi-lang descriptions)
if (raw.descriptions && Array.isArray(raw.descriptions)) {
item.descriptions = raw.descriptions;
if (!item.translations) item.translations = {};
for (const entry of raw.descriptions) {
if (!item.translations[entry.language]) item.translations[entry.language] = {};
item.translations[entry.language].simpleDescription = entry.value;
}
}
// Preserve attributes from backend
item.attributes = raw.attributes || [];
// Preserve colour & size
item.colour = raw.colour || '';
item.size = raw.size || '';
// Map backOffice comments → legacy callbacks
if (raw.comments && (!raw.callbacks || raw.callbacks.length === 0)) {
item.callbacks = raw.comments.map((c: any) => ({
rating: c.stars,
content: c.text,
userID: c.author,
timestamp: c.createdAt,
}));
}
item.comments = raw.comments || raw.callbacks?.map((c: any) => ({
id: c.userID,
text: c.content,
author: c.userID,
stars: c.rating,
createdAt: c.timestamp,
})) || [];
// Compute average rating from comments if not present
if (raw.rating == null && item.comments && item.comments.length > 0) {
const rated = item.comments.filter(c => c.stars != null);
item.rating = rated.length > 0
? rated.reduce((sum, c) => sum + (c.stars || 0), 0) / rated.length
: 0;
}
item.rating = item.rating || 0;
// Defaults
item.discount = item.discount || 0;
item.remainings = item.remainings || (raw.quantity != null
? (raw.quantity <= 0 ? 'out' : raw.quantity <= 5 ? 'low' : raw.quantity <= 20 ? 'medium' : 'high')
: 'high');
item.currency = item.currency || 'RUB';
// Preserve new backOffice fields
item.badges = raw.badges || [];
item.tags = raw.tags || [];
item.simpleDescription = raw.simpleDescription || '';
item.translations = item.translations || raw.translations || {};
item.visible = raw.visible ?? true;
item.priority = raw.priority ?? 0;
return item;
}
private normalizeItems(items: Item[] | null | undefined): Item[] {
private normalizeItems(items: any[] | null | undefined): Item[] {
if (!items || !Array.isArray(items)) {
return [];
}
return items.map(item => this.normalizeItem(item));
}
/** Append region query param if a region is selected */
private withRegion(params: HttpParams = new HttpParams()): HttpParams {
const regionId = this.locationService.regionId();
return regionId ? params.set('region', regionId) : params;
/**
* Normalize a category from the API response — supports both
* the flat legacy format and nested backOffice format.
*/
private normalizeCategory(raw: any): Category {
const cat: Category = { ...raw };
if (raw.id != null && raw.categoryID == null) {
cat.id = String(raw.id);
cat.categoryID = typeof raw.id === 'number' ? raw.id : 0;
}
// Map backOffice img → legacy icon
if (raw.img && !raw.icon) {
cat.icon = raw.img;
}
cat.img = raw.img || raw.icon;
cat.parentID = raw.parentID ?? 0;
cat.visible = raw.visible ?? true;
cat.priority = raw.priority ?? 0;
if (raw.subcategories && Array.isArray(raw.subcategories)) {
cat.subcategories = raw.subcategories;
}
return cat;
}
private normalizeCategories(cats: any[] | null | undefined): Category[] {
if (!cats || !Array.isArray(cats)) return [];
return cats.map(c => this.normalizeCategory(c));
}
// ─── Core Marketplace Endpoints ───────────────────────────
ping(): Observable<{ message: string }> {
return this.http.get<{ message: string }>(`${this.baseUrl}/ping`);
}
getCategories(): Observable<Category[]> {
return this.http.get<Category[]>(`${this.baseUrl}/category`, { params: this.withRegion() });
return this.http.get<any[]>(`${this.baseUrl}/category`)
.pipe(retry(this.retryConfig), map(cats => this.normalizeCategories(cats)));
}
getCategoryItems(categoryID: number, count: number = 50, skip: number = 0): Observable<Item[]> {
const params = this.withRegion(
new HttpParams()
.set('count', count.toString())
.set('skip', skip.toString())
);
return this.http.get<Item[]>(`${this.baseUrl}/category/${categoryID}`, { params })
.pipe(map(items => this.normalizeItems(items)));
const params = new HttpParams()
.set('count', count.toString())
.set('skip', skip.toString());
return this.http.get<any[]>(`${this.baseUrl}/category/${categoryID}`, { params })
.pipe(retry(this.retryConfig), map(items => this.normalizeItems(items)));
}
getItem(itemID: number): Observable<Item> {
return this.http.get<Item>(`${this.baseUrl}/item/${itemID}`, { params: this.withRegion() })
.pipe(map(item => this.normalizeItem(item)));
return this.http.get<any>(`${this.baseUrl}/item/${itemID}`)
.pipe(retry(this.retryConfig), map(item => this.normalizeItem(item)));
}
searchItems(search: string, count: number = 50, skip: number = 0): Observable<{ items: Item[], total: number }> {
const params = this.withRegion(
new HttpParams()
.set('search', search)
.set('count', count.toString())
.set('skip', skip.toString())
);
return this.http.get<{ items: Item[], total: number, count: number, skip: number }>(`${this.baseUrl}/searchitems`, { params })
const params = new HttpParams()
.set('search', search)
.set('count', count.toString())
.set('skip', skip.toString());
return this.http.get<any>(`${this.baseUrl}/searchitems`, { params })
.pipe(
retry(this.retryConfig),
map(response => ({
items: this.normalizeItems(response?.items || []),
total: response?.total || 0
@@ -87,7 +217,7 @@ export class ApiService {
}
getCart(): Observable<Item[]> {
return this.http.get<Item[]>(`${this.baseUrl}/cart`)
return this.http.get<any[]>(`${this.baseUrl}/cart`)
.pipe(map(items => this.normalizeItems(items)));
}
@@ -169,11 +299,11 @@ export class ApiService {
}
getRandomItems(count: number = 5, categoryID?: number): Observable<Item[]> {
let params = this.withRegion(new HttpParams().set('count', count.toString()));
let params = new HttpParams().set('count', count.toString());
if (categoryID) {
params = params.set('category', categoryID.toString());
}
return this.http.get<Item[]>(`${this.baseUrl}/randomitems`, { params })
.pipe(map(items => this.normalizeItems(items)));
return this.http.get<any[]>(`${this.baseUrl}/randomitems`, { params })
.pipe(retry(this.retryConfig), map(items => this.normalizeItems(items)));
}
}

View File

@@ -13,6 +13,7 @@ export class CartService {
private cartItems = signal<CartItem[]>([]);
private isTelegram = typeof window !== 'undefined' && !!window.Telegram?.WebApp;
private addingItems = new Set<number>();
private initialized = false;
items = this.cartItems.asReadonly();
itemCount = computed(() => {
@@ -31,10 +32,12 @@ export class CartService {
constructor(private apiService: ApiService) {
this.loadCart();
// Auto-save whenever cart changes
// Auto-save whenever cart changes (skip the initial empty state)
effect(() => {
const items = this.cartItems();
this.saveToStorage(items);
if (this.initialized) {
this.saveToStorage(items);
}
});
}
@@ -67,9 +70,11 @@ export class CartService {
// No data in CloudStorage, try localStorage
this.loadFromLocalStorage();
}
this.initialized = true;
});
} else {
this.loadFromLocalStorage();
this.initialized = true;
}
}

View File

@@ -9,19 +9,34 @@ export interface Language {
enabled: boolean;
}
export interface Currency {
code: string;
symbol: string;
name: string;
}
@Injectable({
providedIn: 'root'
})
export class LanguageService {
private currentLanguageSignal = signal<string>('ru');
private currentCurrencySignal = signal<string>('RUB');
languages: Language[] = [
{ code: 'ru', name: 'Русский', flag: '🇷🇺', flagSvg: '/flags/ru.svg', enabled: true },
{ code: 'en', name: 'English', flag: '🇬🇧', flagSvg: '/flags/en.svg', enabled: true },
{ code: 'hy', name: 'Հայերեն', flag: '🇦🇲', flagSvg: '/flags/arm.svg', enabled: true }
{ code: 'en', name: 'English', flag: '🇬🇧', flagSvg: '/flags/en.svg', enabled: false },
{ code: 'hy', name: 'Հայերեն', flag: '🇦🇲', flagSvg: '/flags/arm.svg', enabled: false }
];
currencies: Currency[] = [
{ code: 'RUB', symbol: '₽', name: 'Рубль' },
{ code: 'USD', symbol: '$', name: 'Dollar' },
{ code: 'EUR', symbol: '€', name: 'Euro' },
{ code: 'AMD', symbol: '֏', name: 'Դրամ' },
];
currentLanguage = this.currentLanguageSignal.asReadonly();
currentCurrency = this.currentCurrencySignal.asReadonly();
constructor(private router: Router) {
// Load saved language from localStorage
@@ -29,6 +44,11 @@ export class LanguageService {
if (savedLang && this.languages.find(l => l.code === savedLang && l.enabled)) {
this.currentLanguageSignal.set(savedLang);
}
const savedCurrency = localStorage.getItem('selectedCurrency');
if (savedCurrency && this.currencies.find(c => c.code === savedCurrency)) {
this.currentCurrencySignal.set(savedCurrency);
}
}
setLanguage(langCode: string): void {
@@ -39,6 +59,18 @@ export class LanguageService {
}
}
setCurrency(code: string): void {
const currency = this.currencies.find(c => c.code === code);
if (currency) {
this.currentCurrencySignal.set(code);
localStorage.setItem('selectedCurrency', code);
}
}
getCurrentCurrency(): Currency | undefined {
return this.currencies.find(c => c.code === this.currentCurrencySignal());
}
/** Change language and navigate to the same page with the new prefix */
switchLanguage(langCode: string): void {
const lang = this.languages.find(l => l.code === langCode);

View File

@@ -0,0 +1,15 @@
import { Injectable } from '@angular/core';
import { ApiService } from './api.service';
@Injectable({ providedIn: 'root' })
export class PrefetchService {
private prefetched = new Set<number>();
constructor(private api: ApiService) {}
prefetchItem(itemID: number): void {
if (this.prefetched.has(itemID)) return;
this.prefetched.add(itemID);
this.api.getItem(itemID).subscribe();
}
}

View File

@@ -1,4 +1,4 @@
import { Injectable, inject } from '@angular/core';
import { Injectable, inject, DOCUMENT } from '@angular/core';
import { Meta, Title } from '@angular/platform-browser';
import { environment } from '../../environments/environment';
import { Item } from '../models';
@@ -10,6 +10,7 @@ import { getDiscountedPrice, getMainImage } from '../utils/item.utils';
export class SeoService {
private meta = inject(Meta);
private title = inject(Title);
private doc = inject(DOCUMENT);
private readonly siteUrl = `https://${environment.domain}`;
private readonly siteName = environment.brandFullName;
@@ -25,6 +26,7 @@ export class SeoService {
const titleText = `${item.name}${this.siteName}`;
this.title.setTitle(titleText);
this.setCanonical(itemUrl);
this.setOrUpdate([
// Open Graph
@@ -81,6 +83,7 @@ export class SeoService {
// Remove product-specific tags
this.meta.removeTag("property='product:price:amount'");
this.meta.removeTag("property='product:price:currency'");
this.removeCanonical();
}
private setOrUpdate(tags: Array<{ property?: string; name?: string; content: string }>): void {
@@ -114,4 +117,19 @@ export class SeoService {
if (!text || text.length <= maxLength) return text || '';
return text.substring(0, maxLength - 1) + '…';
}
private setCanonical(url: string): void {
this.removeCanonical();
const link = this.doc.createElement('link');
link.setAttribute('rel', 'canonical');
link.setAttribute('href', url);
this.doc.head.appendChild(link);
}
private removeCanonical(): void {
const existing = this.doc.head.querySelector('link[rel="canonical"]');
if (existing) {
this.doc.head.removeChild(existing);
}
}
}

View File

@@ -1,13 +1,92 @@
import { Item } from '../models';
export function getDiscountedPrice(item: Item): number {
return item.price * (1 - item.discount / 100);
return item.price * (1 - (item.discount || 0) / 100);
}
export function getMainImage(item: Item): string {
// Support both backOffice format (imgs: string[]) and legacy (photos: Photo[])
if (item.imgs && item.imgs.length > 0) {
return item.imgs[0];
}
return item.photos?.[0]?.url || '/assets/images/placeholder.svg';
}
export function trackByItemId(index: number, item: Item): number {
return item.itemID;
export function getAllImages(item: Item): string[] {
if (item.imgs && item.imgs.length > 0) {
return item.imgs;
}
return item.photos?.map(p => p.url) || [];
}
export function trackByItemId(index: number, item: Item): number | string {
return item.id || item.itemID;
}
/**
* Get the display description — supports both legacy HTML string
* and structured key-value pairs from backOffice API.
*/
export function hasStructuredDescription(item: Item): boolean {
return Array.isArray(item.descriptionFields) && item.descriptionFields.length > 0;
}
/**
* Compute stock status from quantity if the legacy `remainings` field is absent.
*/
export function getStockStatus(item: Item): string {
if (item.remainings) return item.remainings;
if (item.quantity == null) return 'high';
if (item.quantity <= 0) return 'out';
if (item.quantity <= 5) return 'low';
if (item.quantity <= 20) return 'medium';
return 'high';
}
/**
* Map backOffice badge names to CSS color classes.
*/
export function getBadgeClass(badge: string): string {
const map: Record<string, string> = {
'new': 'badge-new',
'sale': 'badge-sale',
'exclusive': 'badge-exclusive',
'hot': 'badge-hot',
'limited': 'badge-limited',
'bestseller': 'badge-bestseller',
'featured': 'badge-featured',
};
return map[badge.toLowerCase()] || 'badge-custom';
}
/**
* Get the translated name/description for the current language.
* Checks translations map first, then names[]/descriptions[] arrays,
* then falls back to the default (base) field.
*/
export function getTranslatedField(
item: Item,
field: 'name' | 'simpleDescription',
lang: string
): string {
// 1. Check translations map (backOffice format)
const translation = item.translations?.[lang];
if (translation && translation[field]) {
return translation[field]!;
}
// 2. Check names[]/descriptions[] arrays (backend API format)
if (field === 'name' && item.names?.length) {
const entry = item.names.find(n => n.language === lang);
if (entry) return entry.value;
}
if (field === 'simpleDescription' && item.descriptions?.length) {
const entry = item.descriptions.find(d => d.language === lang);
if (entry) return entry.value;
}
// 3. Fallback to base field
if (field === 'name') return item.name;
if (field === 'simpleDescription') return item.simpleDescription || item.description || '';
return '';
}

View File

@@ -14,5 +14,6 @@ export const environment = {
phones: {
armenia: '+374 98 731231',
support: '+374 98 731231'
}
},
useMockData: false
};

View File

@@ -14,5 +14,6 @@ export const environment = {
phones: {
armenia: '+374 98 731231',
support: '+374 98 731231'
}
},
useMockData: true
};

View File

@@ -14,5 +14,6 @@ export const environment = {
phones: {
russia: '+7 (926) 459-31-57',
armenia: '+374 94 86 18 16'
}
},
useMockData: false
};

View File

@@ -1,10 +1,11 @@
// Dexar Market Configuration
export const environment = {
production: false,
useMockData: false, // Toggle to test with backOffice mock data
brandName: 'Dexarmarket',
brandFullName: 'Dexar Market',
theme: 'dexar',
apiUrl: 'https://api.dexarmarket.ru:445',
apiUrl: '/api',
logo: '/assets/images/dexar-logo.svg',
contactEmail: 'info@dexarmarket.ru',
supportEmail: 'info@dexarmarket.ru',

View File

@@ -140,3 +140,58 @@ a, button, input, textarea, select {
.p-3 { padding: 1.5rem; }
.p-4 { padding: 2rem; }
// ─── Shared Badge & Tag Styles (from backOffice integration) ───
.item-badges-overlay {
position: absolute;
top: 8px;
left: 8px;
display: flex;
flex-wrap: wrap;
gap: 4px;
z-index: 2;
}
.item-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.4px;
color: #fff;
line-height: 1.4;
&.badge-new { background: #4caf50; }
&.badge-sale { background: #f44336; }
&.badge-exclusive { background: #9c27b0; }
&.badge-hot { background: #ff5722; }
&.badge-limited { background: #ff9800; }
&.badge-bestseller { background: #2196f3; }
&.badge-featured { background: #607d8b; }
&.badge-custom { background: #78909c; }
}
.item-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.72rem;
color: var(--primary-color);
background: rgba(73, 118, 113, 0.08);
border: 1px solid rgba(73, 118, 113, 0.15);
}
.item-simple-desc {
font-size: 0.8rem;
color: var(--text-secondary);
line-height: 1.4;
margin: 2px 0 4px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}

4
start
View File

@@ -1,2 +1,2 @@
pm2 start "ng serve --configuration=novo --host 127.0.0.1 --port 4000" --name novo-market
pm2 start "ng serve --host 127.0.0.1 --port 3000" --name dexar-market
pm2 start "ng serve --configuration=novo --host 127.0.0.1 --port 4010" --name novo-market
pm2 start "ng serve --host 127.0.0.1 --port 4001" --name dexar-market