44 Commits

Author SHA1 Message Date
sdarbinyan
6de461473e added docs 2026-03-25 15:42:27 +04:00
sdarbinyan
db781fd871 qr login with telegram 2026-03-25 15:32:50 +04:00
sdarbinyan
ce301e9c70 translation into armenian 2026-03-25 14:52:26 +04:00
sdarbinyan
64288b5ce1 offer 2026-03-25 14:27:53 +04:00
sdarbinyan
a8bb725f78 Add ООО «ИНТ ФАКТОРИНГ» (ИНН 9909697635) as second company across all pages 2026-03-24 17:15:48 +04:00
tonoyan
df2208ab53 dexar.market 2026-03-24 10:55:29 +00:00
tonoyan
72deb8d5e3 add dexar.market 2026-03-24 10:53:03 +00:00
sdarbinyan
5566e011b7 fixed cart 2026-03-24 03:24:34 +04:00
sdarbinyan
ee23fd2d3c color 2026-03-24 03:12:04 +04:00
sdarbinyan
2a41062769 random 2026-03-24 02:58:51 +04:00
sdarbinyan
6624de7a32 random items 2026-03-24 02:52:39 +04:00
sdarbinyan
44553f5bd4 changes 2026-03-24 02:46:58 +04:00
sdarbinyan
5ed255dddb Merge branch 'main' of https://sources.vitanova.network/sdarbinyan/marketplaces 2026-03-24 02:27:59 +04:00
sdarbinyan
650bf137f2 fixes 2026-03-24 02:25:50 +04:00
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
98 changed files with 9326 additions and 2385 deletions

2
.gitignore vendored
View File

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

View File

@@ -154,7 +154,8 @@
}, },
"serve": { "serve": {
"options": { "options": {
"allowedHosts": ["novo.market", "dexarmarket.ru", "localhost"] "allowedHosts": ["novo.market", "dexarmarket.ru", "dexar.market","localhost"],
"proxyConfig": "proxy.conf.json"
}, },
"builder": "@angular/build:dev-server", "builder": "@angular/build:dev-server",
"configurations": { "configurations": {
@@ -175,28 +176,9 @@
}, },
"extract-i18n": { "extract-i18n": {
"builder": "@angular/build: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 > **Last updated:** February 2026
> **Frontend:** Angular 21 · Dual-brand (Dexar + Novo)
Frontend has been updated with two new features: > **Covers:** Catalog, Cart, Payments, Reviews, Regions, Auth, i18n, BackOffice
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`
--- ---
## 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 ```json
[ [
{ {
@@ -46,52 +493,44 @@ Returns the list of regions where the marketplace operates.
``` ```
**Region object:** **Region object:**
| Field | Type | Required | Description | | Field | Type | Required | Description |
|---------------|--------|----------|------------------------------| |---------------|--------|----------|--------------------------|
| `id` | string | yes | Unique region identifier | | `id` | string | yes | Unique region identifier |
| `city` | string | yes | City name (display) | | `city` | string | yes | City name (display) |
| `country` | string | yes | Country name (display) | | `country` | string | yes | Country name |
| `countryCode` | string | yes | ISO 3166-1 alpha-2 code | | `countryCode` | string | yes | ISO 3166-1 alpha-2 |
| `timezone` | string | no | IANA timezone string | | `timezone` | string | no | IANA timezone |
> If this endpoint is unavailable, the frontend falls back to 6 hardcoded regions (Moscow, SPB, Yerevan, Minsk, Almaty, Tbilisi). > **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 ## 8. Authentication (Telegram Login)
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
Authentication is **Telegram-based** with **cookie sessions** (HttpOnly, Secure, SameSite=None). 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 ```json
{ {
"sessionId": "sess_abc123", "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 ```json
{ {
"sessionId": "sess_abc123", "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 ```json
{ { "error": "No active session" }
"error": "No active session"
}
``` ```
**AuthSession object:** **AuthSession object:**
| Field | Type | Required | Description | | Field | Type | Required | Description |
|------------------|---------|----------|------------------------------------------| |------------------|---------|----------|--------------------------------------------|
| `sessionId` | string | yes | Unique session ID | | `sessionId` | string | yes | Unique session ID |
| `telegramUserId` | number | yes | Telegram user ID | | `telegramUserId` | number | yes | Telegram user ID |
| `username` | string? | no | Telegram @username (can be null) | | `username` | string? | no | Telegram @username (can be null) |
| `displayName` | string | yes | User display name (first_name + last_name) | | `displayName` | string | yes | User display name (first + last) |
| `active` | boolean | yes | Whether session is currently valid | | `active` | boolean | yes | Whether session is valid |
| `expiresAt` | string | yes | ISO 8601 expiration datetime | | `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:** **Request body (from bot):**
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):
```json ```json
{ {
"id": 123456789, "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 ```json
{ {
"sessionId": "sess_abc123", "sessionId": "sess_abc123",
@@ -167,93 +598,89 @@ This is the URL the Telegram bot redirects to after the user starts the bot.
``` ```
**Cookie requirements:** **Cookie requirements:**
| Attribute | Value | Notes | | Attribute | Value | Notes |
|------------|----------------|------------------------------------------| |------------|----------------|--------------------------------------------|
| `HttpOnly` | `true` | Not accessible via JS | | `HttpOnly` | `true` | Not accessible via JS |
| `Secure` | `true` | HTTPS only | | `Secure` | `true` | HTTPS only |
| `SameSite` | `None` | Required for cross-origin (API ≠ frontend) | | `SameSite` | `None` | Required for cross-origin (API ≠ frontend) |
| `Path` | `/` | | | `Path` | `/` | |
| `Max-Age` | `86400` (24h) | Or as needed | | `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.
--- ---
### 2.3 `POST /auth/logout` — End session ### `POST /auth/logout` — End session
**Request:** **Request:** Cookies only, empty body `{}`
- Cookies: session cookie
- CORS: `withCredentials: true`
- Body: `{}` (empty)
**Response `200 OK`:** **Response `200`:**
```json
{ "message": "Logged out" }
```
Must clear/invalidate the session cookie.
---
### Session refresh
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 ```json
{ {
"message": "Logged out" "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"
}
}
} }
``` ```
Should clear/invalidate the session cookie. 3. **Recommended approach**: Read `X-Language` header and return the `name`/`description` in that language directly. If no translation exists, return the Russian default.
--- ---
## 3. CORS Configuration ## 10. CORS Configuration
For auth cookies to work cross-origin, the backend CORS config must include: 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-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-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 ## 11. Telegram Bot Setup
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
Each brand needs its own bot: Each brand needs its own bot:
- **Dexar:** `@dexarmarket_bot` - **Dexar:** `@dexarmarket_bot`
@@ -262,5 +689,38 @@ Each brand needs its own bot:
The bot should: The bot should:
1. Listen for `/start auth_{callbackUrl}` command 1. Listen for `/start auth_{callbackUrl}` command
2. Extract the callback URL 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` 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

@@ -0,0 +1,824 @@
# Авторизация через Telegram — Backend & Bot
> Всё что нужно Go-разработчику для реализации авторизации.
> Фронтенд **полностью готов** и ждёт эти эндпоинты.
---
## Статус
| Компонент | Готов? |
|-----------|--------|
| Frontend (Angular) — диалог, QR, поллинг, корзина | ✅ Готов |
| Telegram бот (обработка `/start`) | ❌ Нужно |
| Backend — 6 HTTP-эндпоинтов | ❌ Нужно |
| Хранилище сессий + QR-токенов | ❌ Нужно |
| CORS для cookie-based запросов | ❌ Нужно |
---
## Архитектура
Два сценария авторизации:
### Сценарий 1: Прямой вход (кнопка "Войти через Telegram")
Пользователь нажимает кнопку → открывается Telegram → бот выдаёт кнопку "Войти на сайт" → callback ставит cookie → фронтенд поллит `/auth/session`.
### Сценарий 2: QR-логин с десктопа (основной)
```
ДЕСКТОП БРАУЗЕР СЕРВЕР (Go) TELEGRAM
│ │ │
│ 1. POST /auth/qr/create │ │
│ ─────────────────────────────> │ │
│ { token: "abc", url: "..." } │ │
│ <───────────────────────────── │ │
│ │ │
│ 2. Показать QR: │ │
│ t.me/Bot?start=login_abc │ │
│ │ │
│ ПОЛЬЗОВАТЕЛЬ СКАНИРУЕТ ТЕЛЕФОНОМ │
│ │ │
│ │ 3. /start login_abc │
│ │ <────────────────────────│
│ │ │
│ │ Бот → POST /auth/qr/confirm
│ │ Бот → "✅ Вы вошли!" │
│ │ ────────────────────────>│
│ │ │
│ 4. GET /auth/qr/poll?token=abc │ │
│ (каждые 3 сек) │ │
│ ─────────────────────────────> │ │
│ { status: "confirmed", │ │
│ session: {...} } │ │
│ + Set-Cookie: dx_session=... │ │
│ <───────────────────────────── │ │
│ │ │
│ 5. POST /websession/{sessionId} │ │
│ [{ itemID, quantity, ... }] │ ← корзина │
│ ─────────────────────────────> │ │
│ │ │
│ 6. Готово! Авторизован + корзина│ │
```
---
## Бренды и боты
| Бренд | Username бота | Домен фронтенда | API сервер | Cookie Domain |
|-------|---------------|------------------|------------|---------------|
| Dexar | `DexarSupport_bot` | `dexarmarket.ru` | `api.dexarmarket.ru:445` | `.dexarmarket.ru` |
| Novo | `novomarket_bot` | `novo.market` | `api.novo.market:444` | `.novo.market` |
Бот создаётся через https://t.me/BotFather → `/newbot`. Сохранить `BOT_TOKEN`.
---
## Хранилище
### Структура: Сессия
```go
type Session struct {
SessionID string `json:"sessionId"`
TelegramUserID int64 `json:"telegramUserId"`
Username *string `json:"username"` // может быть null
DisplayName string `json:"displayName"`
Active bool `json:"active"`
ExpiresAt time.Time `json:"expiresAt"`
}
```
**TTL:** 24 часа.
### Структура: QR-токен (одноразовый)
```go
type AuthToken struct {
Token string `json:"token"`
Status string `json:"status"` // "pending" | "confirmed" | "expired"
SessionID string `json:"sessionId"` // заполняется после подтверждения ботом
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
}
```
**TTL:** 5 минут.
### Варианты хранения
**Redis (рекомендуется):**
```go
// Сессия
redisClient.Set(ctx, "session:"+s.SessionID, json, 24*time.Hour)
// QR-токен
redisClient.Set(ctx, "auth_token:"+t.Token, json, 5*time.Minute)
```
**sync.Map (для MVP):**
```go
var sessions sync.Map
var authTokens sync.Map
// Очистка устаревших токенов — запустить горутину при старте
func cleanupExpiredTokens() {
ticker := time.NewTicker(1 * time.Minute)
for range ticker.C {
authTokens.Range(func(key, value any) bool {
t := value.(AuthToken)
if time.Now().After(t.ExpiresAt) {
authTokens.Delete(key)
}
return true
})
}
}
```
---
## HTTP-эндпоинты
### 1. `POST /auth/qr/create`
Фронтенд вызывает при открытии диалога логина. Создаёт одноразовый QR-токен.
```go
func handleQrCreate(w http.ResponseWriter, r *http.Request) {
// 1. Сгенерировать криптографически безопасный токен
tokenBytes := make([]byte, 32)
if _, err := rand.Read(tokenBytes); err != nil {
http.Error(w, "internal error", 500)
return
}
token := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(tokenBytes)
// 2. Определить бота по origin
botUsername := getBotForOrigin(r.Header.Get("Origin"))
// 3. Сохранить токен
authToken := AuthToken{
Token: token,
Status: "pending",
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(5 * time.Minute),
}
saveAuthToken(authToken)
// 4. Ответить
qrURL := fmt.Sprintf("https://t.me/%s?start=login_%s", botUsername, token)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"token": token,
"url": qrURL,
})
}
func getBotForOrigin(origin string) string {
if strings.Contains(origin, "novo.market") {
return "novomarket_bot"
}
return "DexarSupport_bot"
}
```
**Ответ:**
```json
{ "token": "dG9rZW4tYWJj....", "url": "https://t.me/DexarSupport_bot?start=login_dG9rZW4tYWJj...." }
```
> Telegram ограничивает `start` до 64 символов. `login_` (6) + base64url из 32 байт (43) = 49 ✅
---
### 2. `GET /auth/qr/poll?token={token}`
Фронтенд вызывает каждые 3 секунды. Когда бот подтвердил — возвращает сессию и ставит cookie.
```go
func handleQrPoll(w http.ResponseWriter, r *http.Request) {
tokenStr := r.URL.Query().Get("token")
if tokenStr == "" {
http.Error(w, "missing token", 400)
return
}
authToken, ok := getAuthToken(tokenStr)
if !ok {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "expired"})
return
}
switch authToken.Status {
case "pending":
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "pending"})
case "confirmed":
session, err := getSession(authToken.SessionID)
if err != nil {
http.Error(w, "session not found", 500)
return
}
// Cookie в ДЕСКТОПНЫЙ браузер
domain := getDomainForOrigin(r.Header.Get("Origin"))
http.SetCookie(w, &http.Cookie{
Name: "dx_session",
Value: session.SessionID,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteNoneMode,
MaxAge: 86400,
Domain: domain,
})
// Удалить использованный токен
deleteAuthToken(tokenStr)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "confirmed",
"session": session,
})
}
}
func getDomainForOrigin(origin string) string {
if strings.Contains(origin, "novo.market") {
return ".novo.market"
}
return ".dexarmarket.ru"
}
```
**Ответы:**
| Статус | JSON |
|--------|------|
| Ждём | `{ "status": "pending" }` |
| Подтверждено | `{ "status": "confirmed", "session": { sessionId, telegramUserId, username, displayName, active, expiresAt } }` + `Set-Cookie` |
| Истекло | `{ "status": "expired" }` |
---
### 3. `POST /auth/qr/confirm` (внутренний, для бота)
Бот вызывает когда пользователь отсканировал QR. Привязывает сессию к токену.
```go
func handleQrConfirm(w http.ResponseWriter, r *http.Request) {
// Проверить секрет бота
if r.Header.Get("X-Bot-Secret") != os.Getenv("BOT_INTERNAL_SECRET") {
http.Error(w, "forbidden", 403)
return
}
var req struct {
Token string `json:"token"`
User struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Username string `json:"username"`
} `json:"telegram_user"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad request", 400)
return
}
authToken, ok := getAuthToken(req.Token)
if !ok || authToken.Status != "pending" {
http.Error(w, "token not found or already used", 404)
return
}
// Создать сессию
displayName := req.User.FirstName
if req.User.LastName != "" {
displayName += " " + req.User.LastName
}
var username *string
if req.User.Username != "" {
username = &req.User.Username
}
session := Session{
SessionID: uuid.New().String(),
TelegramUserID: req.User.ID,
Username: username,
DisplayName: displayName,
Active: true,
ExpiresAt: time.Now().Add(24 * time.Hour),
}
saveSession(session)
// Привязать сессию к токену
authToken.Status = "confirmed"
authToken.SessionID = session.SessionID
saveAuthToken(*authToken)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
```
**Запрос от бота:**
```json
{
"token": "dG9rZW4tYWJj...",
"telegram_user": {
"id": 123456789,
"first_name": "Иван",
"last_name": "Петров",
"username": "ivan_petrov"
}
}
```
---
### 4. `GET /auth/session`
Фронтенд вызывает для проверки текущей сессии. Читает cookie.
```go
func handleGetSession(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("dx_session")
if err != nil {
http.Error(w, "unauthorized", 401)
return
}
session, err := getSession(cookie.Value)
if err != nil {
http.Error(w, "unauthorized", 401)
return
}
if time.Now().After(session.ExpiresAt) {
session.Active = false
saveSession(session)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(session)
}
```
**Формат ответа (200)** — фронтенд ожидает **точно эти поля**:
```json
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"telegramUserId": 123456789,
"username": "ivan_petrov",
"displayName": "Иван Петров",
"active": true,
"expiresAt": "2026-03-25T14:30:00Z"
}
```
| Поле | Тип | Обязательно | Примечание |
|------|-----|-------------|------------|
| `sessionId` | string (UUID) | да | Используется для `/websession/{sessionId}` |
| `telegramUserId` | number | да | Telegram user ID |
| `username` | string / null | нет | Telegram @username |
| `displayName` | string | да | "Имя Фамилия" — показывается в UI |
| `active` | boolean | да | `false` = истекла |
| `expiresAt` | string (ISO 8601) | да | Фронтенд перепроверяет за 60 сек до |
**Ошибка:** любой HTTP не-200 → фронтенд считает "не авторизован".
---
### 5. `GET /auth/telegram/callback`
Для прямого входа (по кнопке в Telegram, не через QR). Открывается в браузере.
```go
func handleTelegramCallback(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
if token == "" {
http.Error(w, "missing token", 400)
return
}
session, err := getSession(token)
if err != nil || !session.Active {
http.Error(w, "invalid or expired token", 401)
return
}
domain := getDomainForOrigin(r.Header.Get("Origin"))
if domain == "" {
domain = ".dexarmarket.ru" // fallback для прямого перехода
}
http.SetCookie(w, &http.Cookie{
Name: "dx_session",
Value: session.SessionID,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteNoneMode,
MaxAge: 86400,
Domain: domain,
})
// Редирект на сайт
http.Redirect(w, r, "https://dexarmarket.ru", http.StatusFound)
}
```
---
### 6. `POST /auth/logout`
```go
func handleLogout(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("dx_session")
if err == nil {
deleteSession(cookie.Value)
}
http.SetCookie(w, &http.Cookie{
Name: "dx_session",
Value: "",
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteNoneMode,
MaxAge: -1,
Domain: ".dexarmarket.ru",
})
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message":"ok"}`))
}
```
---
## Cookie-параметры
| Параметр | Значение | Почему |
|----------|----------|--------|
| `Name` | `dx_session` | |
| `SameSite` | `None` | Фронтенд на `dexarmarket.ru`, API на `api.dexarmarket.ru:445` — разные origins |
| `Secure` | `true` | Обязательно при `SameSite=None` |
| `Domain` | `.dexarmarket.ru` | Доступна и на `dexarmarket.ru` и на `api.dexarmarket.ru` |
| `HttpOnly` | `true` | Недоступна из JS — защита от XSS |
| `MaxAge` | `86400` | 24 часа |
---
## CORS
Фронтенд шлёт `withCredentials: true`. Бэкенд обязан вернуть правильные заголовки.
```go
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
allowed := map[string]bool{
"https://dexarmarket.ru": true,
"https://www.dexarmarket.ru": true,
"https://novo.market": true,
"https://www.novo.market": true,
"http://localhost:4200": true,
}
if allowed[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin) // НЕ "*"
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
}
if r.Method == "OPTIONS" {
w.WriteHeader(200)
return
}
next.ServeHTTP(w, r)
})
}
```
> **Критично:** `Access-Control-Allow-Origin` не может быть `"*"` при `withCredentials`. Вернуть конкретный origin.
---
## Роутинг
```go
mux := http.NewServeMux()
// Существующие
mux.HandleFunc("GET /items/{id}", handleGetItem)
mux.HandleFunc("GET /category", handleGetCategories)
mux.HandleFunc("POST /websession/{id}", handleWebSession)
mux.HandleFunc("POST /websession/{id}/qr", handleCreateQR)
mux.HandleFunc("GET /websession/{id}/{qrId}", handleCheckPayment)
// Auth — прямой вход
mux.HandleFunc("GET /auth/session", handleGetSession)
mux.HandleFunc("GET /auth/telegram/callback", handleTelegramCallback)
mux.HandleFunc("POST /auth/logout", handleLogout)
// Auth — QR-логин
mux.HandleFunc("POST /auth/qr/create", handleQrCreate)
mux.HandleFunc("GET /auth/qr/poll", handleQrPoll)
mux.HandleFunc("POST /auth/qr/confirm", handleQrConfirm)
handler := corsMiddleware(mux)
http.ListenAndServeTLS(":445", "cert.pem", "key.pem", handler)
```
---
## Telegram-бот
### Обработчик `/start`
```go
const (
confirmURL = "http://localhost:8080/auth/qr/confirm"
botInternalSecret = os.Getenv("BOT_INTERNAL_SECRET")
)
func handleStart(update tgbotapi.Update) {
text := update.Message.Text
user := update.Message.From
switch {
case strings.HasPrefix(text, "/start login_"):
handleQrLogin(update, user, strings.TrimPrefix(text, "/start login_"))
case strings.HasPrefix(text, "/start auth"):
handleDirectAuth(update, user)
default:
sendWelcome(update)
}
}
```
### QR-логин (основной)
```go
func handleQrLogin(update tgbotapi.Update, user *tgbotapi.User, token string) {
reqBody := map[string]interface{}{
"token": token,
"telegram_user": map[string]interface{}{
"id": user.ID,
"first_name": user.FirstName,
"last_name": user.LastName,
"username": user.UserName,
},
}
bodyBytes, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", confirmURL, bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Bot-Secret", botInternalSecret)
resp, err := http.DefaultClient.Do(req)
if err != nil || resp.StatusCode != 200 {
msg := tgbotapi.NewMessage(update.Message.Chat.ID,
"❌ Не удалось войти. QR-код мог устареть. Попробуйте обновить страницу и отсканировать новый QR.")
bot.Send(msg)
return
}
defer resp.Body.Close()
displayName := buildDisplayName(user)
msg := tgbotapi.NewMessage(update.Message.Chat.ID,
fmt.Sprintf("✅ Вы вошли на сайт как %s!\n\nМожете вернуться в браузер — страница обновится автоматически.", displayName))
bot.Send(msg)
}
func buildDisplayName(user *tgbotapi.User) string {
name := user.FirstName
if user.LastName != "" {
name += " " + user.LastName
}
return name
}
```
### Прямой вход (кнопка, для обратной совместимости)
```go
func handleDirectAuth(update tgbotapi.Update, user *tgbotapi.User) {
session := Session{
SessionID: uuid.New().String(),
TelegramUserID: user.ID,
Username: stringPtr(user.UserName),
DisplayName: buildDisplayName(user),
Active: true,
ExpiresAt: time.Now().Add(24 * time.Hour),
}
saveSession(session)
callbackURL := "https://api.dexarmarket.ru:445/auth/telegram/callback"
loginURL := callbackURL + "?token=" + session.SessionID
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Нажмите кнопку чтобы войти:")
msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonURL("🔐 Войти на сайт", loginURL),
),
)
bot.Send(msg)
}
```
### Запуск бота (long polling)
```go
func main() {
bot, _ := tgbotapi.NewBotAPI(os.Getenv("BOT_TOKEN"))
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message != nil && strings.HasPrefix(update.Message.Text, "/start") {
handleStart(update)
}
}
}
```
---
## Синхронизация корзины
Сразу после QR-логина фронтенд автоматически отправляет корзину:
```
POST /websession/{sessionId}
```
Тело — массив:
```json
[
{
"itemID": 123,
"quantity": 2,
"colour": "#ff0000",
"size": "XL",
"price": 1500
}
]
```
| Поле | Тип | Примечание |
|------|-----|------------|
| `itemID` | number | ID товара |
| `quantity` | number | Количество |
| `colour` | string | CSS hex (`#ff0000`). Бэкенд отдаёт `0xff0000`, фронтенд конвертирует |
| `size` | string | `"default"` если размер один |
| `price` | number | Финальная цена **с учётом скидки** |
> Этот эндпоинт (`POST /websession/{id}`) уже существует. Ничего менять не нужно, просто учитывать что он вызывается сразу после успешного логина.
---
## Безопасность
### Криптографический токен
```go
tokenBytes := make([]byte, 32) // 256 бит
crypto/rand.Read(tokenBytes)
token := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(tokenBytes)
```
**НЕ использовать:** `math/rand`, UUID, timestamp.
### Токен одноразовый
- После `confirmed` → удалить при первом успешном `poll`
- После 5 минут → автоудаление (TTL)
- Повторный `poll``"expired"`
### Защита `/auth/qr/confirm`
```go
if r.Header.Get("X-Bot-Secret") != os.Getenv("BOT_INTERNAL_SECRET") {
http.Error(w, "forbidden", 403)
return
}
```
Дополнительно: можно ограничить по IP (`127.0.0.1`) если бот на том же сервере.
### Rate limiting для `/auth/qr/create`
Не более **5 токенов в минуту** с одного IP:
```go
var ipCounts sync.Map
func rateLimitQrCreate(ip string) bool {
key := ip + ":" + time.Now().Format("2006-01-02T15:04")
val, _ := ipCounts.LoadOrStore(key, new(int32))
count := atomic.AddInt32(val.(*int32), 1)
return count <= 5
}
```
---
## Переменные окружения
```env
BOT_TOKEN=123456:ABC-DEF...
BOT_INTERNAL_SECRET=случайная-строка-минимум-32-символа
FRONTEND_URL=https://dexarmarket.ru
SESSION_TTL=24h
REDIS_URL=localhost:6379
```
`BOT_INTERNAL_SECRET` должен совпадать в env сервера и env бота.
---
## Тестирование
### curl-тесты
**1. Создание токена:**
```bash
curl -X POST https://api.dexarmarket.ru:445/auth/qr/create \
-H "Origin: https://dexarmarket.ru"
# → { "token": "dG9r...", "url": "https://t.me/DexarSupport_bot?start=login_dG9r..." }
```
**2. Поллинг (до подтверждения):**
```bash
curl "https://api.dexarmarket.ru:445/auth/qr/poll?token=dG9r..."
# → { "status": "pending" }
```
**3. Подтверждение (имитация бота):**
```bash
curl -X POST https://api.dexarmarket.ru:445/auth/qr/confirm \
-H "Content-Type: application/json" \
-H "X-Bot-Secret: ваш-секрет" \
-d '{"token":"dG9r...","telegram_user":{"id":123,"first_name":"Тест","last_name":"","username":"testuser"}}'
# → { "status": "ok" }
```
**4. Поллинг (после подтверждения):**
```bash
curl -v "https://api.dexarmarket.ru:445/auth/qr/poll?token=dG9r..."
# → { "status": "confirmed", "session": {...} } + Set-Cookie: dx_session=...
```
**5. E2E:**
1. Открыть маркетплейс → добавить товар в корзину
2. Нажать "Оформить заказ" → появляется диалог с QR
3. Отсканировать QR телефоном → Telegram → бот: "✅ Вы вошли!"
4. Через 3 сек диалог закрывается → авторизован
5. Корзина синхронизирована (`POST /websession/{sessionId}`)
### Отладка
| Проблема | Где смотреть |
|----------|-------------|
| QR не показывается | `POST /auth/qr/create` — ошибка? CORS? |
| QR отсканирован, ничего не происходит | Бот получил `/start login_...`? Бот вызвал `confirm`? |
| Бот пишет "❌ QR устарел" | Токен expired? 5 минут прошло? |
| Поллинг "pending" бесконечно | Бот не вызвал `confirm`. Логи бота |
| Поллинг "confirmed" но cookie нет | `SameSite`, `Secure`, `Domain`, CORS |
---
## Чеклист
### Бэкенд (Go)
- [ ] Структура `Session` + `AuthToken`, функции save/get/delete
- [ ] `POST /auth/qr/create` — генерация токена
- [ ] `GET /auth/qr/poll?token=...` — статус + cookie при confirmed
- [ ] `POST /auth/qr/confirm` — приём от бота с `X-Bot-Secret`
- [ ] `GET /auth/session` — чтение cookie, JSON сессии
- [ ] `GET /auth/telegram/callback?token=...` — cookie + редирект
- [ ] `POST /auth/logout` — удаление сессии и cookie
- [ ] TTL 5 мин для токенов, 24ч для сессий
- [ ] Rate limiting `/auth/qr/create` (5/мин/IP)
- [ ] Очистка устаревших токенов
- [ ] CORS middleware
- [ ] `BOT_INTERNAL_SECRET` в env
### Telegram бот
- [ ] Обработка `/start login_{token}``POST /auth/qr/confirm`
- [ ] Обработка `/start auth` → создание сессии + кнопка "Войти"
- [ ] Сообщения: "✅ Вы вошли" / "❌ QR устарел"
- [ ] `BOT_INTERNAL_SECRET` в env (совпадает с сервером)
- [ ] `BOT_TOKEN` в env

View File

@@ -1,11 +1,748 @@
bro we need to do changes, that client required General Information
1. we need to add location logic Information exchange with the SBP server is realized via RESTful API. All requests to the server must be executed via HTTPS using GET||POST||PUT||DELETE requests to the given ROOT address. Body of requests must be in JSON format. All not public requests must be signed by the client and the public key must be sent to the server for client identification and sign checking.
1.1 the catalogs will come or for global or for exact region Header:
1.2 need to add a place where the user can choose his region like city if choosed moscow the country is set russian “Authorization”: {JSON WITH KEY AND PARTNERID}
1.3 can we try to understand what country is user logged or whach city by global ip and set it? “X-Region” : Moscow | Yerevan | ST. Petersburg
2. we need to add somekind of user login logic “X-Language” : RU | AM | EN
2.1 user can add to cart, look the items and etc without logged in, but when he is going to buy/pay -> “WebSessionID” : f02fe5d6-c6ae-4b2e-9b4d-687534e11b01
at first he have to login with telegram, i will send you the bots adress. “Currency” :RUB | AMD | USD
2.1.1 if is not logged -> will see the QR or link for logging via telegram Root:
2.1.2 if logged we need to ping server to check if he is active user. the expiration date (like day or 5 days) we will get from bakcend with session id API.dexarmarket.ru
2.2 and when user is logged, that time he can do a payment
General Information
Check if server is available
Get Marketplaces
Set Marketplaces
Get Item
Delete Item
New Item
New Callback
New Question
Get random Items
Get items in category
Get searched items
Get Categories
Delete Category
New Category
Create new websession
Check websession status
Delete websession status
Add to cart
Create New QR code for cart checkout
Check QR code
item structure
category structure
Check if server is available
Client needs to periodically check if the server is available by sending “ping” to the client. On error corresponding message must be shown.
Protocol: https
Type: GET
Path: /ping
Request Parameters:
{
}
Response (Error):
{
"message": "pong",
"status": "Wrong Header"
}
Response (OK):
{
"message": "pong",
"status": "Correct Header"
}
________________
Get Marketplaces
Get Available Marketplaces
Protocol: https
Type: GET
Path: /marketplaces
Request Parameters:
{
}
Response !=200(Error):
{
"error": "wrong header"
}
Response =200(OK):
{
[{“brand” : “dexar”,
“api”:”dexar.market”,
“bot”:”dexarmarket_bot”,
“languagies”:[“”am,”ru”,”en”],
“regions”:[“Mosocw - Russia”, ”St Petersburg - Russia”, ”Yerevan - Armenia”]
“currency”:[“RUB, ”AMD”, ”USD”]
“icon”:”./dexar.market.png”},
{“brand” : “store”,
“api”:”dexarmarket.store”,
“bot”:”dexarstore_bot”,
“languagies”:[“”am,”ru”,”en”],
“regions”:[“Mosocw - Russia”,”St Petersburg - Russia”,”Yerevan - Armenia”]
“currency”:[“”RUB,”AMD”,”USD”]
“icon”:”./dexarmarket.store.png”},
{“brand” : “Novo”,
“api”:”novo.market”,
“bot”:”novomarket_bot”,
“languagies”:[“”am,”ru”,”en”],
“regions”:[“Mosocw - Russia”, ”St Petersburg - Russia”,”Yerevan - Armenia”]
“currency”:[“”RUB,”AMD”,”USD”]
“icon”:”./novo.market.png”}]
}
________________
Set Marketplaces
Get Available Marketplaces
Protocol: https
Type: PUT
Path: /marketplaces
Request Parameters:
{
[{“brand” : “dexar”,
“api”:”dexar.market”,
“languagies”:[“”am,”ru”,”en”],
“regions”:[“Mosocw - Russia”,”St Petersburg - Russia”,”Yerevan - Armenia”]
“currency”:[“”RUB,”AMD”,”USD”]
“icon”:”./dexar.market.png”},
{“brand” : “store”,
“api”:”dexarmarket.store”,
“languagies”:[“”am,”ru”,”en”],
“regions”:[“Mosocw - Russia”,”St Petersburg - Russia”,”Yerevan - Armenia”]
“currency”:[“”RUB,”AMD”,”USD”]
“icon”:”./dexarmarket.store.png”},
{“brand” : “Novo”,
“api”:”novo.market”,
“languagies”:[“”am,”ru”,”en”],
“regions”:[“Mosocw - Russia”, ”St Petersburg - Russia”,”Yerevan - Armenia”]
“currency”:[“”RUB,”AMD”,”USD”]
“icon”:”./novo.market.png”}]
}
Response !=200(Error):
{
"error": "wrong header"
}
Response =200(OK):
{
“status”:”Marketplace updated”
}
________________
Get Item
Get Item by ID
Protocol: https
Type: GET
Path: /items/:itemID
Request Parameters:
{
}
Response !=200(Error):
{
"error": "wrong header"
}
Response =200(OK):
{
“itemID”:...
}
________________
Delete Item
Delete the item
Protocol: https
Type: Delete
Path: /items/:itemID
Request Parameters:
{
}
Response !=200(Error):
{
"error": "wrong header"
}
Response =200(OK):
{
“status”:”Item was deleted”
}
________________
New Item
Create new Item
Protocol: https
Type: POST
Path: /items/:itemID
Request Parameters:
{
“itemID”:...
}
Response !=200(Error):
{
"error": "wrong header"
}
Response =200(OK):
{
“itemID”:...
}
________________
Update Item
Update the item
Protocol: https
Type: PUT
Path: /items/:itemID
Request Parameters:
{
“itemID”:...
}
Response !=200(Error):
{
"error": "wrong header"
}
Response =200(OK):
{
“status”:”Item updated”
}
________________
New Callback
Update the item
Protocol: https
Type: POST
Path: /items/:itemID/callback
Request Parameters:
{
"rating": 5,
"comment": "Отличный товар!",
"sessionID": “ f02fe5d6-c6ae-4b2e-9b4d-687534e11b01”
"timestamp": "2026-02-28T12:00:00Z"
}
Response !=200(Error):
{
"error": "wrong item"
}
Response =200(OK):
{
“status”:”Callback added”
}
________________
New Question
Update the item
Protocol: https
Type: POST
Path: /items/:itemID/questiion
Request Parameters:
{
"question": "some question!",
"sessionID": “ f02fe5d6-c6ae-4b2e-9b4d-687534e11b01”
"timestamp": "2026-02-28T12:00:00Z"
}
Response !=200(Error):
{
"error": "wrong item"
}
Response =200(OK):
{
“status”:”Questiion added”
}
________________
Get random Items
Get given number of items from random categorues
Protocol: https
Type: GET
Path: /items/randomitems?count=15 // 20 is the default
Request Parameters:
{
}
Response !=200(Error):
{
"error": "wrong header"
}
Response =200(OK):
{
[“itemID”:...]
}
________________
Get items in category
Get all items in category and in all subcategories inside the category
Protocol: https
Type: GET
Path: /category/:categoryID?count=30, skip=60 // default skip=0, default count=20
Request Parameters:
{
}
Response !=200(Error):
{
"error": "wrong header"
}
Response =200(OK):
{
[“itemID”:...]
}
________________
Get searched items
Get all items in category and in all subcategories inside the category
Protocol: https
Type: GET
Path: /searchitems
Parameters:
{
search (string) — query text
categoryIDs (string) — e.g., 1,2,5 (includes all subcategories)
minPrice / maxPrice (float) — price range
tag (string) — e.g., sale
sort (string) — relevance (default), price_asc, price_desc, popular, rating
skip / count — default 0 / 20
}
Examples:
* ?search=iphone&sort=popular
* ?categoryIDs=1,5&minPrice=100&maxPrice=500
* ?tag=new&sort=price_asc&count=10
Request Parameters:
{
}
Response !=200(Error):
{
"error": "wrong header"
}
Response =200(OK):
{
"total": 12,
"skip": 0,
"count": 12,
"isGlobal": false,
"items": [
{ "itemID": 101, "name": "..." }
]
}
________________
Get Categories
Get all available categories
Protocol: https
Type: GET
Path: /category
Request Parameters:
{
}
Response !=200(Error):
{
"error": "wrong header"
}
Response =200(OK):
{
“categoryID”:...
}
________________
Delete Category
Delete EMPTY category, no items and no subcategories must present
Protocol: https
Type: Delete
Path: /category/:categoryID
Request Parameters:
{
}
Response !=200(Error):
{
"error": "wrong header"
}
Response =200(OK):
{
“status”:”Category was deleted”
}
________________
New Category
Create new category
Protocol: https
Type: POST
Path: /category/
Request Parameters:
{
“CategoryID”:...
}
Response !=200(Error):
{
"error": "wrong header"
}
Response =200(OK):
{
“CategoryID”:...
}
________________
Update Category
Update existing category
Protocol: https
Type: PUT
Path: /category/:categoryID
Request Parameters:
{
“itemID”:...
}
Response !=200(Error):
{
"error": "wrong header"
}
Response =200(OK):
{
“status”:”Category was updated”
}
________________
Create new websession
Creates a new websession for qr generation. By timeout a new websession must be requested, after the user shows some activity (click on qr).
Protocol: https
Type POST
Path /websession
Request Parameters:
{
}
Response (OK):
{
"sessionId": “1AF3781BF6B94604B771AEA1D44FA63A”,
"userId" : "",
"expires" : "sessionId",
"userSessionId": "",
"status": false
}
________________
Check websession status
Check if the user is already logged in. a new websession for qr generation. By timeout a new websession must be requested, after the user shows some activity (click on qr).
Protocol: https
Type GET
Path /websession/:webSessionID
Request Parameters:
{
}
Response (OK):
{
"sessionId": “1AF3781BF6B94604B771AEA1D44FA63A”,
"userId" : "kHaAe9roaC2uq63AKGE/8+Ti/t/iFro68QhEZ1dRGLo",
"expires" : "sessionId",
"userSessionId": "8A94EFEFD003426A9B456C48CAC99BE6",
"x-Region" : "Moscow",
"x-Language" : "RU",
"currency" : "RUB",
"Status": true,
"cart": [
{ "itemID": 12, "quantity": 1, “colour”:”black”, “size”:”42”,"priice":230.50 },
{ "itemID": 13, "quantity": 2, “colour”:”dark”, “size”:”L”,"priice":250.50 },
{ "itemID": 14, "quantity": 3, “colour”:”blue”, “size”:”50”,"priice":290.50 },
]
}
________________
Delete websession status
Delete the session to log out from the system.
Protocol: https
Type DELETE
Path /websession/:webSessionID
Request Parameters:
{
}
Response (OK):
{
“status”:”User logged out”
}
________________
Add to cart
Add a all item to users (session) cart
Protocol: https
Type Post
Path /websession/:webSessionID
Request Parameters:
{
[
{ "itemID": 12, "quantity": 1, “colour”:”black”, “size”:”42”,"priice":230.50 },
{ "itemID": 13, "quantity": 2, “colour”:”dark”, “size”:”L”,"priice":250.50 },
{ "itemID": 14, "quantity": 3, “colour”:”blue”, “size”:”50”,"priice":290.50 },
]
}
Response (OK):
{
"sessionId": “1AF3781BF6B94604B771AEA1D44FA63A”,
"userId" : "kHaAe9roaC2uq63AKGE/8+Ti/t/iFro68QhEZ1dRGLo",
"expires" : "sessionId",
"userSessionId": "8A94EFEFD003426A9B456C48CAC99BE6",
"Status": true,
"cart": [
{ "itemID": 12, "quantity": 1, “colour”:”black”, “size”:”42”,"priice":230.50 },
{ "itemID": 13, "quantity": 2, “colour”:”dark”, “size”:”L”,"priice":250.50 },
{ "itemID": 14, "quantity": 3, “colour”:”blue”, “size”:”50”,"priice":290.50 },
]
}
________________
Create New QR code for cart checkout
Create New QR for payment via SBP
Protocol: https
Type POST
Path /websession/:webSessionID/qr
Request Parameters:
{
}
Response !=200(Error):
{
"error": "wrong key"
}
Response =200(OK):
{
"qrId": "BD10002CI1V3JP1T8QR8TIQ8K35RBVQB",
"qrStatus": "NEW",
"qrExpirationDate": "2025-11-20T10:10:44Z",
"Payload": "https://qr.nspk.ru/BD10002CI1V3JP1T8QR8TIQ8K35RBVQB?type=02&bank=100000000007&sum=1000&cur=RUB&crc=8ACC",
"qrUrl": "https://e-commerce.raiffeisen.ru/api/sbp/v1/qr/BD10002CI1V3JP1T8QR8TIQ8K35RBVQB/image"
}
________________
Check QR code
Check QR status
Protocol: https
Type GET
Path /websession/:webSessionID/:qrID
Request Parameters:
{
}
Response !=200(Error):
{
"error": "Error from the bank "
}
Response =200(OK):
{
"additionalInfo": "",
"paymentPurpose": "",
"amount": 10,
"code": "SUCCESS",
"createDate": "2025-11-20T13:17:20.453884+03:00",
"currency": "RUB",
"order": "102_540",
"paymentStatus": "NO_INFO", //check for SUCCESS
"qrId": "BD1000263VS7G81D8JCP5FHFTFEH38MT",
"transactionDate": "",
"transactionId": 0,
"qrExpirationDate": "2025-11-20T13:32:20+03:00"
}
## 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..."
}
```
Бот должен:
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 |
________________
item structure
CategoryID uint64 `json:"categoryID" binding:"required"`
ItemID uint64 `json:"itemID" binding:"required"`
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Discount float32 `json:"discount" `
Rating float32 `json:"rating" binding:"required"`
Visible bool `json:"rating"`
Priority uint64 `json:"priority"`
Tags []string `json:"tags"`
Badges []string `json:"badges"`
Details []itemdetail `json:"itemdetails"`
Colour string `json:"colour" binding:"required"`
Size string `json:"size" binding:"required"`
Price float32 `json:"price" binding:"required"`
Currency string `json:"currency" binding:"required"`
Remaining uint64 `json:"remaining" binding:"required"`
Names []itemname `json:"names"`
Language string `json:"language"`
Value string `json:"value"`
Descriptions []itemdescription `json:"descriptions" `
Language string `json:"language"`
Value string `json:"value"`
Attributes []attribute `json:"attributes" binding:"required"`
Key string `json:"key"`
Value string `json:"value"`
Photos []photo `json:"photos"`
Type string `json:"type" binding:"required"` //video || photo
URL string `json:"url" binding:"required"`
Questions []question `json:"questions"`
Question string `json:"question" `
Answer string `json:"answer" `
Like uint64 `json:"like" `
Dislike uint64 `json:"dislike" `
Visits uint64 `json:"visits"`
Callbacks []callback `json:"callbacks" binding:"required"`
Rating float32 `json:"rating,omitempty"`
Content string `json:"content,omitempty"`
Userid string `json:"userID"`
Answer string `json:"answer"`
Timestamp string `json:"timestamp"`
PartnerID []string `json:"partnerID" binding:"required"`
category structure
CategoryID uint64 `json:"categoryID" binding:"required"`
ParentID uint64 `json:"parentID" binding:"required"`
Name string `json:"name" binding:"required"`
Visible bool `json:"visible" `
Priority uint64 `json:"priority" `
Icon string `json:"icon"`
WideIcon string `json:"wideicon"`
ItemsCount uint64
CategoriesCount uint64
Names []itemname `json:"names"`
Language string `json:"language"`
Value string `json:"value"`

View File

@@ -36,6 +36,9 @@ server {
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" 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 compression (if available)
# brotli on; # brotli on;

View File

@@ -30,7 +30,9 @@
{ {
"name": "api-cache", "name": "api-cache",
"urls": [ "urls": [
"/api/**" "/api/**",
"https://api.dexarmarket.ru:445/**",
"https://api.novo.market:444/**"
], ],
"cacheConfig": { "cacheConfig": {
"maxSize": 100, "maxSize": 100,

73
package-lock.json generated
View File

@@ -8,11 +8,15 @@
"name": "dexarmarket", "name": "dexarmarket",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@angular/animations": "^21.1.5",
"@angular/cdk": "^21.1.5",
"@angular/common": "^21.0.6", "@angular/common": "^21.0.6",
"@angular/compiler": "^21.0.6", "@angular/compiler": "^21.0.6",
"@angular/core": "^21.0.6", "@angular/core": "^21.0.6",
"@angular/forms": "^21.0.6", "@angular/forms": "^21.0.6",
"@angular/material": "^21.1.5",
"@angular/platform-browser": "^21.0.6", "@angular/platform-browser": "^21.0.6",
"@angular/platform-browser-dynamic": "^21.1.5",
"@angular/router": "^21.0.6", "@angular/router": "^21.0.6",
"@angular/service-worker": "^21.0.6", "@angular/service-worker": "^21.0.6",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
@@ -324,6 +328,21 @@
"yarn": ">= 1.13.0" "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": { "node_modules/@angular/build": {
"version": "21.1.0", "version": "21.1.0",
"resolved": "https://registry.npmjs.org/@angular/build/-/build-21.1.0.tgz", "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.1.0.tgz",
@@ -472,6 +491,22 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@angular/cli": {
"version": "21.1.0", "version": "21.1.0",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.1.0.tgz", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.1.0.tgz",
@@ -613,6 +648,23 @@
"rxjs": "^6.5.3 || ^7.4.0" "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": { "node_modules/@angular/platform-browser": {
"version": "21.0.6", "version": "21.0.6",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.6.tgz", "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": { "node_modules/@angular/router": {
"version": "21.0.6", "version": "21.0.6",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-21.0.6.tgz", "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.0.6.tgz",
@@ -7687,7 +7757,6 @@
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"entities": "^6.0.0" "entities": "^6.0.0"
@@ -7741,7 +7810,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"
@@ -9512,3 +9580,4 @@
} }
} }
} }

View File

@@ -16,11 +16,15 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^21.1.5",
"@angular/cdk": "^21.1.5",
"@angular/common": "^21.0.6", "@angular/common": "^21.0.6",
"@angular/compiler": "^21.0.6", "@angular/compiler": "^21.0.6",
"@angular/core": "^21.0.6", "@angular/core": "^21.0.6",
"@angular/forms": "^21.0.6", "@angular/forms": "^21.0.6",
"@angular/material": "^21.1.5",
"@angular/platform-browser": "^21.0.6", "@angular/platform-browser": "^21.0.6",
"@angular/platform-browser-dynamic": "^21.1.5",
"@angular/router": "^21.0.6", "@angular/router": "^21.0.6",
"@angular/service-worker": "^21.0.6", "@angular/service-worker": "^21.0.6",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
@@ -43,3 +47,4 @@
"typescript": "~5.9.3" "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 - Интернет-магазин", "name": "Novo Market - Интернет-магазин",
"short_name": "Novo", "short_name": "Novo",
"description": "Novo Market - ваш онлайн магазин качественных товаров с доставкой", "description": "Novo Market - ваш онлайн магазин качественных товаров с доставкой",
@@ -12,34 +11,10 @@
"categories": ["shopping", "lifestyle"], "categories": ["shopping", "lifestyle"],
"icons": [ "icons": [
{ {
"src": "icons/icon-72x72.png", "src": "assets/images/novo-favicon.svg",
"sizes": "72x72", "sizes": "any",
"type": "image/png", "type": "image/svg+xml",
"purpose": "maskable any" "purpose": "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": "icons/icon-192x192.png", "src": "icons/icon-192x192.png",
@@ -47,12 +22,6 @@
"type": "image/png", "type": "image/png",
"purpose": "maskable any" "purpose": "maskable any"
}, },
{
"src": "icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "maskable any"
},
{ {
"src": "icons/icon-512x512.png", "src": "icons/icon-512x512.png",
"sizes": "512x512", "sizes": "512x512",

View File

@@ -11,34 +11,10 @@
"categories": ["shopping", "marketplace"], "categories": ["shopping", "marketplace"],
"icons": [ "icons": [
{ {
"src": "icons/icon-72x72.png", "src": "assets/images/dexar-favicon.svg",
"sizes": "72x72", "sizes": "any",
"type": "image/png", "type": "image/svg+xml",
"purpose": "maskable any" "purpose": "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": "icons/icon-192x192.png", "src": "icons/icon-192x192.png",
@@ -46,12 +22,6 @@
"type": "image/png", "type": "image/png",
"purpose": "maskable any" "purpose": "maskable any"
}, },
{
"src": "icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "maskable any"
},
{ {
"src": "icons/icon-512x512.png", "src": "icons/icon-512x512.png",
"sizes": "512x512", "sizes": "512x512",

View File

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

View File

@@ -12,10 +12,10 @@
</div> </div>
} @else { } @else {
<app-header></app-header> <app-header></app-header>
<main class="main-content">
@if (!isHomePage()) { @if (!isHomePage()) {
<app-back-button /> <app-back-button />
} }
<main class="main-content">
<router-outlet></router-outlet> <router-outlet></router-outlet>
</main> </main>
<app-footer></app-footer> <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: [` styles: [`
.dexar-back-btn { .dexar-back-btn {
position: fixed; position: sticky;
top: 76px; top: 72px;
left: 20px; left: 20px;
z-index: 100; z-index: 100;
background: none; background: none;
border: none; border: none;
cursor: pointer; cursor: pointer;
padding: 4px; padding: 8px 4px;
margin-bottom: -40px;
width: fit-content;
transition: transform 0.2s ease; transition: transform 0.2s ease;
svg path { svg path {
@@ -47,7 +49,7 @@ import { TranslateService } from '../../i18n/translate.service';
@media (max-width: 768px) { @media (max-width: 768px) {
.dexar-back-btn { .dexar-back-btn {
top: 68px; top: 64px;
left: 12px; left: 12px;
svg { svg {

View File

@@ -30,8 +30,8 @@
<app-region-selector /> <app-region-selector />
<app-language-selector /> <app-language-selector />
<a [routerLink]="'/cart' | langRoute" routerLinkActive="novo-cart-active" class="novo-cart" (click)="closeMenu()"> <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"> <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="9" cy="21" r="1"></circle>
<circle cx="20" 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> <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> </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> <span></span>
<span></span> <span></span>
@@ -118,7 +118,7 @@
</div> </div>
<!-- Mobile Menu Toggle --> <!-- 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> <span></span>
<span></span> <span></span>

View File

@@ -18,14 +18,21 @@
<div class="item-card"> <div class="item-card">
<a [routerLink]="['/item', product.itemID] | langRoute" class="item-link"> <a [routerLink]="['/item', product.itemID] | langRoute" class="item-link">
<div class="item-image"> <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) { @if (product.discount > 0) {
<span class="discount-badge">-{{ product.discount }}%</span> <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>
<div class="item-details"> <div class="item-details">
<h3 class="item-name">{{ product.name }}</h3> <h3 class="item-name">{{ itemName(product) }}</h3>
@if (product.rating) { @if (product.rating) {
<div class="item-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 { DecimalPipe } from '@angular/common';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { CarouselModule } from 'primeng/carousel'; import { CarouselModule } from 'primeng/carousel';
@@ -7,7 +7,8 @@ import { TagModule } from 'primeng/tag';
import { ApiService, CartService } from '../../services'; import { ApiService, CartService } from '../../services';
import { Item } from '../../models'; import { Item } from '../../models';
import { environment } from '../../../environments/environment'; 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 { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe'; import { TranslatePipe } from '../../i18n/translate.pipe';
@@ -98,6 +99,10 @@ export class ItemsCarouselComponent implements OnInit {
readonly getItemImage = getMainImage; readonly getItemImage = getMainImage;
readonly getDiscountedPrice = getDiscountedPrice; 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 { addToCart(event: Event, item: Item): void {
event.preventDefault(); event.preventDefault();

View File

@@ -1,5 +1,5 @@
<div class="language-selector"> <div class="language-selector" role="listbox">
<button class="language-button" (click)="toggleDropdown()"> <button class="language-button" (click)="toggleDropdown()" (keydown)="onKeyDown($event)" aria-haspopup="listbox" [attr.aria-expanded]="dropdownOpen">
<img [src]="languageService.getCurrentLanguage()?.flagSvg" <img [src]="languageService.getCurrentLanguage()?.flagSvg"
[alt]="languageService.getCurrentLanguage()?.name" [alt]="languageService.getCurrentLanguage()?.name"
class="language-flag"> class="language-flag">
@@ -13,6 +13,8 @@
@for (lang of languageService.languages; track lang.code) { @for (lang of languageService.languages; track lang.code) {
<button <button
class="language-option" class="language-option"
role="option"
[attr.aria-selected]="languageService.currentLanguage() === lang.code"
[class.active]="languageService.currentLanguage() === lang.code" [class.active]="languageService.currentLanguage() === lang.code"
[class.disabled]="!lang.enabled" [class.disabled]="!lang.enabled"
[disabled]="!lang.enabled" [disabled]="!lang.enabled"
@@ -22,4 +24,25 @@
</button> </button>
} }
</div> </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> </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 { Component, HostListener, ElementRef, ChangeDetectionStrategy } from '@angular/core';
import { LanguageService, Language } from '../../services/language.service'; import { LanguageService, Language, Currency } from '../../services/language.service';
@Component({ @Component({
selector: 'app-language-selector', selector: 'app-language-selector',
@@ -10,6 +10,7 @@ import { LanguageService, Language } from '../../services/language.service';
}) })
export class LanguageSelectorComponent { export class LanguageSelectorComponent {
dropdownOpen = false; dropdownOpen = false;
currencyOpen = false;
constructor( constructor(
public languageService: LanguageService, public languageService: LanguageService,
@@ -18,6 +19,12 @@ export class LanguageSelectorComponent {
toggleDropdown(): void { toggleDropdown(): void {
this.dropdownOpen = !this.dropdownOpen; this.dropdownOpen = !this.dropdownOpen;
this.currencyOpen = false;
}
toggleCurrency(): void {
this.currencyOpen = !this.currencyOpen;
this.dropdownOpen = false;
} }
selectLanguage(lang: Language): void { 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 { closeDropdown(): void {
this.dropdownOpen = false; 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']) @HostListener('document:click', ['$event'])
onClickOutside(event: Event): void { onClickOutside(event: Event): void {
if (!this.elementRef.nativeElement.contains(event.target)) { if (!this.elementRef.nativeElement.contains(event.target)) {
this.dropdownOpen = false; this.dropdownOpen = false;
this.currencyOpen = false;
} }
} }
} }

View File

@@ -31,13 +31,41 @@
<div class="qr-section"> <div class="qr-section">
<p class="qr-hint">{{ 'auth.orScanQr' | translate }}</p> <p class="qr-hint">{{ 'auth.orScanQr' | translate }}</p>
@switch (qrStatus()) {
@case ('loading') {
<div class="qr-container qr-loading">
<div class="spinner"></div>
</div>
}
@case ('ready') {
<div class="qr-container"> <div class="qr-container">
<img [src]="'https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=' + loginUrl()" <img [src]="'https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=' + encodedQrUrl()"
alt="QR Code"
width="180"
height="180"
loading="eager" />
</div>
}
@case ('expired') {
<div class="qr-container qr-expired" (click)="refreshQr()">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 4v6h6M23 20v-6h-6"/>
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/>
</svg>
<span>{{ 'auth.qrExpired' | translate }}</span>
</div>
}
@case ('error') {
<div class="qr-container">
<img [src]="'https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=' + encodedQrUrl()"
alt="QR Code" alt="QR Code"
width="180" width="180"
height="180" height="180"
loading="lazy" /> loading="lazy" />
</div> </div>
}
}
</div> </div>
<p class="login-note">{{ 'auth.loginNote' | translate }}</p> <p class="login-note">{{ 'auth.loginNote' | translate }}</p>

View File

@@ -122,6 +122,42 @@ h2 {
display: block; display: block;
border-radius: 4px; border-radius: 4px;
} }
&.qr-loading {
align-items: center;
justify-content: center;
width: 204px;
height: 204px;
.spinner {
width: 32px;
height: 32px;
border: 3px solid #e0e0e0;
border-top-color: var(--accent-color, #497671);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
}
&.qr-expired {
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
width: 204px;
height: 204px;
cursor: pointer;
color: var(--text-secondary, #999);
transition: color 0.2s ease;
&:hover {
color: var(--accent-color, #497671);
}
span {
font-size: 13px;
}
}
} }
} }

View File

@@ -1,6 +1,8 @@
import { Component, ChangeDetectionStrategy, inject, signal, OnInit, OnDestroy } from '@angular/core'; import { Component, ChangeDetectionStrategy, inject, signal, computed, effect, OnDestroy } from '@angular/core';
import { AuthService } from '../../services/auth.service'; import { AuthService } from '../../services/auth.service';
import { CartService } from '../../services/cart.service';
import { TranslatePipe } from '../../i18n/translate.pipe'; import { TranslatePipe } from '../../i18n/translate.pipe';
import { getDiscountedPrice } from '../../utils/item.utils';
@Component({ @Component({
selector: 'app-telegram-login', selector: 'app-telegram-login',
@@ -9,17 +11,28 @@ import { TranslatePipe } from '../../i18n/translate.pipe';
styleUrls: ['./telegram-login.component.scss'], styleUrls: ['./telegram-login.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class TelegramLoginComponent implements OnInit, OnDestroy { export class TelegramLoginComponent implements OnDestroy {
private authService = inject(AuthService); private authService = inject(AuthService);
private cartService = inject(CartService);
showDialog = this.authService.showLoginDialog; showDialog = this.authService.showLoginDialog;
status = this.authService.status; status = this.authService.status;
loginUrl = signal(''); loginUrl = signal('');
qrToken = signal('');
qrStatus = signal<'loading' | 'ready' | 'expired' | 'error'>('loading');
encodedQrUrl = computed(() => encodeURIComponent(this.loginUrl()));
private pollTimer?: ReturnType<typeof setInterval>; private pollTimer?: ReturnType<typeof setInterval>;
ngOnInit(): void { constructor() {
this.loginUrl.set(this.authService.getTelegramLoginUrl()); effect(() => {
if (this.showDialog()) {
this.initQrLogin();
} else {
this.stopPolling();
}
});
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@@ -31,32 +44,87 @@ export class TelegramLoginComponent implements OnInit, OnDestroy {
this.stopPolling(); this.stopPolling();
} }
/** Open Telegram login link and start polling for session */
openTelegramLogin(): void { openTelegramLogin(): void {
window.open(this.loginUrl(), '_blank'); window.open(this.loginUrl(), '_blank');
this.startPolling(); if (!this.pollTimer) {
this.startPolling(this.qrToken());
}
} }
/** Start polling the backend to detect when user completes Telegram auth */ refreshQr(): void {
private startPolling(): void {
this.stopPolling(); this.stopPolling();
// Check every 3 seconds for up to 5 minutes this.initQrLogin();
}
private initQrLogin(): void {
this.qrStatus.set('loading');
this.authService.createQrToken().subscribe({
next: (res) => {
this.loginUrl.set(res.url);
this.qrToken.set(res.token);
this.qrStatus.set('ready');
this.startPolling(res.token);
},
error: () => {
this.loginUrl.set(this.authService.getTelegramLoginUrl());
this.qrStatus.set('error');
}
});
}
private startPolling(token: string): void {
this.stopPolling();
if (!token) return;
let checks = 0; let checks = 0;
this.pollTimer = setInterval(() => { this.pollTimer = setInterval(() => {
checks++; checks++;
if (checks > 100) { // 100 * 3s = 5 min if (checks > 100) {
this.stopPolling(); this.stopPolling();
this.qrStatus.set('expired');
return; return;
} }
this.authService.checkSession();
// If authenticated, stop polling and close dialog this.authService.pollQrToken(token).subscribe({
if (this.authService.isAuthenticated()) { next: (res) => {
switch (res.status) {
case 'confirmed':
this.stopPolling(); this.stopPolling();
this.authService.hideLogin(); if (res.session) {
this.syncCartAndComplete(res.session.sessionId);
} else {
this.authService.onTelegramLoginComplete();
} }
break;
case 'expired':
this.stopPolling();
this.qrStatus.set('expired');
break;
}
},
error: () => {
// Network error — keep polling
}
});
}, 3000); }, 3000);
} }
private syncCartAndComplete(sessionId: string): void {
const cartItems = this.cartService.items().map(item => ({
itemID: item.itemID,
quantity: item.quantity,
colour: item.colour || '',
size: item.size || '',
price: item.discount > 0
? item.price * (1 - item.discount / 100)
: item.price,
}));
this.authService.syncCart(sessionId, cartItems).subscribe(() => {
this.authService.onTelegramLoginComplete();
});
}
private stopPolling(): void { private stopPolling(): void {
if (this.pollTimer) { if (this.pollTimer) {
clearInterval(this.pollTimer); clearInterval(this.pollTimer);

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 @', emailNeedsAt: 'Email must contain @',
emailNeedsDomain: 'Email must contain a domain (.com, .ru, etc.)', emailNeedsDomain: 'Email must contain a domain (.com, .ru, etc.)',
emailInvalid: 'Invalid email format', 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: { search: {
title: 'Product search', title: 'Product search',
@@ -134,6 +138,7 @@ export const en: Translations = {
emptyTitle: 'Oops! No subcategories yet', emptyTitle: 'Oops! No subcategories yet',
emptyDesc: 'There are no subcategories in this section yet, but they will appear soon', emptyDesc: 'There are no subcategories in this section yet, but they will appear soon',
goHome: 'Go home', goHome: 'Go home',
itemsInCategory: 'Items in this category',
}, },
itemDetail: { itemDetail: {
loading: 'Loading...', loading: 'Loading...',
@@ -148,6 +153,7 @@ export const en: Translations = {
mediumStock: 'Running low', mediumStock: 'Running low',
addToCart: 'Add to cart', addToCart: 'Add to cart',
description: 'Description', description: 'Description',
specifications: 'Specifications',
reviews: 'Reviews', reviews: 'Reviews',
yourReview: 'Your review', yourReview: 'Your review',
leaveReview: 'Leave a review', leaveReview: 'Leave a review',
@@ -169,6 +175,8 @@ export const en: Translations = {
yesterday: 'Yesterday', yesterday: 'Yesterday',
daysAgo: 'd. ago', daysAgo: 'd. ago',
weeksAgo: 'w. ago', weeksAgo: 'w. ago',
colour: 'Colour',
size: 'Size',
}, },
app: { app: {
connecting: 'Connecting to server...', connecting: 'Connecting to server...',
@@ -197,5 +205,6 @@ export const en: Translations = {
loginWithTelegram: 'Log in with Telegram', loginWithTelegram: 'Log in with Telegram',
orScanQr: 'Or scan the QR code', orScanQr: 'Or scan the QR code',
loginNote: 'You will be redirected back after login', loginNote: 'You will be redirected back after login',
qrExpired: 'QR code expired. Click to refresh',
}, },
}; };

View File

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

View File

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

View File

@@ -100,6 +100,10 @@ export interface Translations {
emailNeedsAt: string; emailNeedsAt: string;
emailNeedsDomain: string; emailNeedsDomain: string;
emailInvalid: string; emailInvalid: string;
loginRequired: string;
loginRequiredDesc: string;
loginWithTelegram: string;
orScanQr: string;
}; };
search: { search: {
title: string; title: string;
@@ -132,6 +136,7 @@ export interface Translations {
emptyTitle: string; emptyTitle: string;
emptyDesc: string; emptyDesc: string;
goHome: string; goHome: string;
itemsInCategory: string;
}; };
itemDetail: { itemDetail: {
loading: string; loading: string;
@@ -146,6 +151,7 @@ export interface Translations {
mediumStock: string; mediumStock: string;
addToCart: string; addToCart: string;
description: string; description: string;
specifications: string;
reviews: string; reviews: string;
yourReview: string; yourReview: string;
leaveReview: string; leaveReview: string;
@@ -167,6 +173,8 @@ export interface Translations {
yesterday: string; yesterday: string;
daysAgo: string; daysAgo: string;
weeksAgo: string; weeksAgo: string;
colour: string;
size: string;
}; };
app: { app: {
connecting: string; connecting: string;
@@ -195,5 +203,6 @@ export interface Translations {
loginWithTelegram: string; loginWithTelegram: string;
orScanQr: string; orScanQr: string;
loginNote: string; loginNote: string;
qrExpired: 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 { of } from 'rxjs';
import { tap } from 'rxjs/operators'; 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 = new Map<string, { response: HttpResponse<unknown>, timestamp: number }>();
const CACHE_DURATION = 5 * 60 * 1000; // 5 минут
export const cacheInterceptor: HttpInterceptorFn = (req, next) => { export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
// Кэшируем только GET запросы // Кэшируем только GET запросы
@@ -11,12 +12,16 @@ export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
return next(req); return next(req);
} }
// Кэшируем только запросы списка категорий (не товары категорий) // Кэшируем списки категорий, товары категорий и отдельные товары
const shouldCache = req.url.match(/\/category$/) !== null; const isCategoryList = /\/category$/.test(req.url);
if (!shouldCache) { const isCategoryItems = /\/category\/\d+/.test(req.url);
const isItem = /\/items\/\d+/.test(req.url);
if (!isCategoryList && !isCategoryItems && !isItem) {
return next(req); return next(req);
} }
const ttl = isCategoryList ? CACHE_DURATION_MS : CATEGORY_CACHE_DURATION_MS;
// Cleanup expired entries before checking // Cleanup expired entries before checking
cleanupExpiredCache(); cleanupExpiredCache();
@@ -25,7 +30,7 @@ export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
// Проверяем наличие и актуальность кэша // Проверяем наличие и актуальность кэша
if (cachedResponse) { if (cachedResponse) {
const age = Date.now() - cachedResponse.timestamp; const age = Date.now() - cachedResponse.timestamp;
if (age < CACHE_DURATION) { if (age < ttl) {
return of(cachedResponse.response.clone()); return of(cachedResponse.response.clone());
} else { } else {
cache.delete(req.url); cache.delete(req.url);
@@ -53,7 +58,7 @@ export function clearCache(): void {
function cleanupExpiredCache(): void { function cleanupExpiredCache(): void {
const now = Date.now(); const now = Date.now();
for (const [url, data] of cache.entries()) { for (const [url, data] of cache.entries()) {
if (now - data.timestamp >= CACHE_DURATION) { if (now - data.timestamp >= CACHE_DURATION_MS) {
cache.delete(url); cache.delete(url);
} }
} }

View File

@@ -0,0 +1,788 @@
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 /websession/:id (add to cart)
if (url.match(/\/websession\/[^/]+$/) && req.method === 'POST') {
return respond({
sessionId: 'mock-session',
Status: true,
cart: req.body
});
}
// ── POST /websession/:id/qr (create payment QR)
if (url.match(/\/websession\/[^/]+\/qr$/) && req.method === 'POST') {
return respond({
qrId: 'mock-qr-' + Date.now(),
qrStatus: 'NEW',
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);
}
// ── POST /items/:id/callback (review)
if (url.match(/\/items\/\d+\/callback$/) && 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 /websession/:id/:qrId (check QR payment status)
if (url.match(/\/websession\/[^/]+\/[^/]+$/) && !url.match(/\/websession\/[^/]+\/qr$/) && 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()
}, 500);
}
// Fallback — pass through
return next(req);
};

View File

@@ -17,4 +17,9 @@ export interface TelegramAuthData {
hash: string; hash: string;
} }
export interface QrPollResponse {
status: 'pending' | 'confirmed' | 'expired';
session?: AuthSession;
}
export type AuthStatus = 'unknown' | 'checking' | 'authenticated' | 'expired' | 'unauthenticated'; export type AuthStatus = 'unknown' | 'checking' | 'authenticated' | 'expired' | 'unauthenticated';

View File

@@ -1,3 +1,5 @@
import { ItemName } from './item.model';
export interface Category { export interface Category {
categoryID: number; categoryID: number;
name: string; name: string;
@@ -5,5 +7,32 @@ export interface Category {
icon?: string; icon?: string;
wideBanner?: string; wideBanner?: string;
itemCount?: number; itemCount?: number;
categoriesCount?: number;
priority?: number; priority?: number;
names?: ItemName[];
translations?: Record<string, CategoryTranslation>;
// 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; 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 { export interface Review {
rating?: number; rating?: number;
content?: string; content?: string;
@@ -21,6 +40,36 @@ export interface Question {
answer: string; answer: string;
upvotes: number; upvotes: number;
downvotes: number; downvotes: number;
like?: number;
dislike?: 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;
}
/** Item variant detail (price, size, colour per variant) */
export interface ItemDetail {
color?: string;
colour?: string;
size?: string;
price: number;
currency: string;
remaining: number;
} }
export interface Item { export interface Item {
@@ -36,7 +85,30 @@ export interface Item {
rating: number; rating: number;
callbacks: Review[] | null; callbacks: Review[] | null;
questions: Question[] | 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[];
visits?: number;
itemDetails?: ItemDetail[];
} }
export interface CartItem extends Item { export interface CartItem extends Item {

View File

@@ -31,12 +31,12 @@
(touchstart)="onSwipeStart(item.itemID, $event)"> (touchstart)="onSwipeStart(item.itemID, $event)">
<div class="cart-item"> <div class="cart-item">
<a [routerLink]="['/item', item.itemID] | langRoute" class="item-image"> <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> </a>
<div class="item-info"> <div class="item-info">
<div class="item-header"> <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"> <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"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12"/> <path d="M18 6L6 18M6 6l12 12"/>
@@ -44,17 +44,39 @@
</button> </button>
</div> </div>
<p class="item-description">{{ item.description.substring(0, 100) }}...</p> <p class="item-description">{{ itemDesc(item) || '' }}...</p>
@if (item.colour || (item.size && item.size.toLowerCase() !== 'default')) {
<div class="cart-item-variants">
@if (item.colour) {
<span class="cart-variant cart-variant-colour">
{{ 'itemDetail.colour' | translate }}:
<span class="cart-colour-swatch" [style.background-color]="item.colour" [title]="item.colour"></span>
</span>
}
@if (item.size && item.size.toLowerCase() !== 'default') {
<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-footer">
<div class="item-pricing"> <div class="item-pricing">
@if (item.discount > 0) { @if (item.discount > 0) {
<div class="price-with-discount"> <div class="price-with-discount">
<span class="original-price">{{ item.price }} </span> <span class="original-price">{{ item.price }} {{ item.currency }}</span>
<span class="current-price">{{ getDiscountedPrice(item) | number:'1.2-2' }} </span> <span class="current-price">{{ getDiscountedPrice(item) | number:'1.2-2' }} {{ item.currency }}</span>
</div> </div>
} @else { } @else {
<span class="current-price">{{ item.price }} </span> <span class="current-price">{{ item.price }} {{ item.currency }}</span>
} }
</div> </div>
@@ -91,17 +113,17 @@
<div class="summary-row"> <div class="summary-row">
<span>{{ 'cart.items' | translate }} ({{ itemCount() }})</span> <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>
<div class="summary-row delivery"> <div class="summary-row delivery">
<span>{{ 'cart.deliveryLabel' | translate }}</span> <span>{{ 'cart.deliveryLabel' | translate }}</span>
<span>0 </span> <span>0 {{ currentCurrency }}</span>
</div> </div>
<div class="summary-row total"> <div class="summary-row total">
<span>{{ 'cart.toPay' | translate }}</span> <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>
<div class="terms-agreement"> <div class="terms-agreement">
@@ -130,6 +152,36 @@
> >
{{ 'cart.checkout' | translate }} {{ 'cart.checkout' | translate }}
</button> </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>
</div> </div>
} }
@@ -166,7 +218,7 @@
<div class="payment-info"> <div class="payment-info">
<div class="payment-amount"> <div class="payment-amount">
<span class="label">{{ 'cart.amountToPay' | translate }}</span> <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>
<div class="waiting-indicator"> <div class="waiting-indicator">
@@ -256,3 +308,5 @@
</div> </div>
</div> </div>
} }
<app-telegram-login />

View File

@@ -364,6 +364,35 @@
line-height: 1.5; line-height: 1.5;
} }
.cart-item-variants {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
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;
display: inline-flex;
align-items: center;
gap: 6px;
}
.cart-colour-swatch {
display: inline-block;
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.15);
vertical-align: middle;
}
}
.item-footer { .item-footer {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -464,6 +493,35 @@
line-height: 1.6; line-height: 1.6;
} }
.cart-item-variants {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
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;
display: inline-flex;
align-items: center;
gap: 6px;
}
.cart-colour-swatch {
display: inline-block;
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.15);
vertical-align: middle;
}
}
.item-footer { .item-footer {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -689,6 +747,85 @@
cursor: not-allowed; 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 // Novo Cart Summary - Green Modern

View File

@@ -7,15 +7,17 @@ import { Item, CartItem } from '../../models';
import { interval, Subscription } from 'rxjs'; import { interval, Subscription } from 'rxjs';
import { switchMap, take } from 'rxjs/operators'; import { switchMap, take } from 'rxjs/operators';
import { EmptyCartIconComponent } from '../../components/empty-cart-icon/empty-cart-icon.component'; 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 { 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 { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe'; import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service'; 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({ @Component({
selector: 'app-cart', selector: 'app-cart',
imports: [DecimalPipe, RouterLink, FormsModule, EmptyCartIconComponent, LangRoutePipe, TranslatePipe], imports: [DecimalPipe, RouterLink, FormsModule, EmptyCartIconComponent, TelegramLoginComponent, LangRoutePipe, TranslatePipe],
templateUrl: './cart.component.html', templateUrl: './cart.component.html',
styleUrls: ['./cart.component.scss'], styleUrls: ['./cart.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
@@ -30,6 +32,9 @@ export class CartComponent implements OnDestroy {
private i18n = inject(TranslateService); private i18n = inject(TranslateService);
private authService = inject(AuthService); private authService = inject(AuthService);
isAuthenticated = this.authService.isAuthenticated;
loginUrl = signal('');
// Swipe state // Swipe state
swipedItemId = signal<number | null>(null); swipedItemId = signal<number | null>(null);
@@ -51,7 +56,7 @@ export class CartComponent implements OnDestroy {
emailSubmitting = signal<boolean>(false); emailSubmitting = signal<boolean>(false);
paidItems: CartItem[] = []; paidItems: CartItem[] = [];
maxChecks = 36; // 36 checks * 5 seconds = 180 seconds (3 minutes) maxChecks = PAYMENT_MAX_CHECKS;
private pollingSubscription?: Subscription; private pollingSubscription?: Subscription;
private closeTimeout?: ReturnType<typeof setTimeout>; private closeTimeout?: ReturnType<typeof setTimeout>;
@@ -64,6 +69,11 @@ export class CartComponent implements OnDestroy {
this.items = this.cartService.items; this.items = this.cartService.items;
this.itemCount = this.cartService.itemCount; this.itemCount = this.cartService.itemCount;
this.totalPrice = this.cartService.totalPrice; this.totalPrice = this.cartService.totalPrice;
this.loginUrl.set(this.authService.getTelegramLoginUrl());
}
requestLogin(): void {
this.authService.requestLogin();
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@@ -129,6 +139,11 @@ export class CartComponent implements OnDestroy {
readonly getMainImage = getMainImage; readonly getMainImage = getMainImage;
readonly trackByItemId = trackByItemId; readonly trackByItemId = trackByItemId;
readonly getDiscountedPrice = getDiscountedPrice; 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 { checkout(): void {
if (!this.termsAccepted) { if (!this.termsAccepted) {
@@ -167,51 +182,62 @@ export class CartComponent implements OnDestroy {
} }
createPayment(): void { createPayment(): void {
const telegramUsername = this.getTelegramUsername(); const sessionId = this.authService.session()?.sessionId || '';
const userId = this.getUserId(); if (!sessionId) {
const orderId = this.generateOrderId(); this.paymentStatus.set('timeout');
return;
}
const paymentData = { // First sync cart items to server via websession, then create QR
amount: this.totalPrice(), const cartItems = this.items().map((item: CartItem) => ({
currency: 'RUB',
siteuserID: userId,
siteorderID: orderId,
redirectUrl: '',
telegramUsername: telegramUsername,
items: this.items().map((item: CartItem) => ({
itemID: item.itemID, itemID: item.itemID,
quantity: item.quantity,
colour: item.colour || '',
size: item.size || '',
price: item.discount > 0 price: item.discount > 0
? item.price * (1 - item.discount / 100) ? item.price * (1 - item.discount / 100)
: item.price, : item.price,
name: item.name, }));
quantity: item.quantity
}))
};
this.apiService.createPayment(paymentData).subscribe({ this.apiService.addToCart(sessionId, cartItems).subscribe({
next: () => {
this.apiService.createPayment(sessionId).subscribe({
next: (response) => { next: (response) => {
this.paymentId.set(response.qrId); this.paymentId.set(response.qrId);
this.qrCodeUrl.set(response.qrUrl); this.qrCodeUrl.set(response.qrUrl);
this.paymentUrl.set(response.payload); this.paymentUrl.set(response.Payload);
this.paymentStatus.set('waiting'); this.paymentStatus.set('waiting');
this.startPolling(); this.startPolling();
}, },
error: (err) => { error: (err) => {
console.error('Error creating payment:', err); console.error('Error creating payment:', err);
this.paymentStatus.set('timeout'); this.paymentStatus.set('timeout');
if (this.closeTimeout) clearTimeout(this.closeTimeout);
this.closeTimeout = setTimeout(() => { this.closeTimeout = setTimeout(() => {
this.closePaymentPopup(); this.closePaymentPopup();
}, 4000); }, PAYMENT_ERROR_CLOSE_MS);
}
});
},
error: (err) => {
console.error('Error syncing cart:', err);
this.paymentStatus.set('timeout');
if (this.closeTimeout) clearTimeout(this.closeTimeout);
this.closeTimeout = setTimeout(() => {
this.closePaymentPopup();
}, PAYMENT_ERROR_CLOSE_MS);
} }
}); });
} }
startPolling(): void { startPolling(): void {
this.pollingSubscription = interval(5000) // every 5 seconds this.stopPolling();
this.pollingSubscription = interval(PAYMENT_POLL_INTERVAL_MS)
.pipe( .pipe(
take(this.maxChecks), // maximum 36 checks (3 minutes) take(this.maxChecks), // maximum 36 checks (3 minutes)
switchMap(() => { switchMap(() => {
return this.apiService.checkPaymentStatus(this.paymentId()); const sessionId = this.authService.session()?.sessionId || '';
return this.apiService.checkPaymentStatus(sessionId, this.paymentId());
}) })
) )
.subscribe({ .subscribe({
@@ -231,17 +257,19 @@ export class CartComponent implements OnDestroy {
if (this.paymentStatus() === 'waiting') { if (this.paymentStatus() === 'waiting') {
this.paymentStatus.set('timeout'); this.paymentStatus.set('timeout');
// Close popup after showing timeout message // Close popup after showing timeout message
if (this.closeTimeout) clearTimeout(this.closeTimeout);
this.closeTimeout = setTimeout(() => { this.closeTimeout = setTimeout(() => {
this.closePaymentPopup(); this.closePaymentPopup();
}, 3000); }, PAYMENT_TIMEOUT_CLOSE_MS);
} }
}, },
error: (err) => { error: (err) => {
console.error('Error checking payment status:', err); console.error('Error checking payment status:', err);
// Continue checking even on error until time runs out // Continue checking even on error until time runs out
if (this.closeTimeout) clearTimeout(this.closeTimeout);
this.closeTimeout = setTimeout(() => { this.closeTimeout = setTimeout(() => {
this.closePaymentPopup(); this.closePaymentPopup();
}, 3000); }, PAYMENT_TIMEOUT_CLOSE_MS);
} }
}); });
} }
@@ -257,34 +285,13 @@ export class CartComponent implements OnDestroy {
if (url) { if (url) {
navigator.clipboard.writeText(url).then(() => { navigator.clipboard.writeText(url).then(() => {
this.linkCopied.set(true); this.linkCopied.set(true);
setTimeout(() => this.linkCopied.set(false), 2000); setTimeout(() => this.linkCopied.set(false), LINK_COPIED_DURATION_MS);
}).catch(err => { }).catch(err => {
console.error(this.i18n.t('cart.copyError'), err); console.error(this.i18n.t('cart.copyError'), err);
}); });
} }
} }
private getTelegramUsername(): string {
if (typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.user) {
const user = window.Telegram.WebApp.initDataUnsafe.user;
return user.username || 'nontelegram';
}
return 'nontelegram';
}
private getUserId(): string {
if (typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.user) {
return window.Telegram.WebApp.initDataUnsafe.user.id.toString();
}
return `web_${Date.now()}`;
}
private generateOrderId(): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
return `order_${timestamp}_${random}`;
}
submitEmail(): void { submitEmail(): void {
// Mark both fields as touched // Mark both fields as touched
this.emailTouched.set(true); this.emailTouched.set(true);

View File

@@ -9,17 +9,24 @@
@if (!error()) { @if (!error()) {
<div class="items-grid"> <div class="items-grid">
@for (item of items(); track trackByItemId($index, item)) { @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"> <a [routerLink]="['/item', item.itemID] | langRoute" class="item-link">
<div class="item-image"> <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) { @if (item.discount > 0) {
<div class="discount-badge">-{{ item.discount }}%</div> <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>
<div class="item-details"> <div class="item-details">
<h3 class="item-name">{{ item.name }}</h3> <h3 class="item-name">{{ itemName(item) }}</h3>
<div class="item-rating"> <div class="item-rating">
<span class="rating-stars">⭐ {{ item.rating }}</span> <span class="rating-stars">⭐ {{ item.rating }}</span>
@@ -45,19 +52,29 @@
</div> </div>
</a> </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 }} {{ 'category.addToCart' | translate }}
</button> </button>
</div> </div>
} }
</div>
@if (loading() && items().length > 0) { @if (loading() && items().length > 0) {
<div class="loading-more"> @for (i of skeletonSlots; track i) {
<div class="spinner"></div> <div class="item-card skeleton-card">
<p>{{ 'category.loadingMore' | translate }}</p> <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>
} }
}
</div>
@if (!hasMore() && items().length > 0) { @if (!hasMore() && items().length > 0) {
<div class="no-more"> <div class="no-more">

View File

@@ -95,7 +95,7 @@
.items-grid { .items-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 30px; gap: 30px;
margin-bottom: 40px; margin-bottom: 40px;
width: 100%; width: 100%;
@@ -103,8 +103,10 @@
.item-card { .item-card {
width: 100%; width: 100%;
min-width: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover { &:hover {
@@ -139,7 +141,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: #f5f5f5; background: #f0f0f0;
img { img {
width: 100%; width: 100%;
@@ -147,7 +149,7 @@
object-fit: contain; object-fit: contain;
background: white; background: white;
padding: 12px; padding: 12px;
transition: transform 0.3s ease; transition: transform 0.3s ease, opacity 0.3s ease;
} }
&:hover img { &:hover img {
@@ -192,6 +194,7 @@
margin: 0; margin: 0;
line-height: 1.3; line-height: 1.3;
display: -webkit-box; display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
@@ -287,11 +290,6 @@
} }
} }
.loading-more {
text-align: center;
padding: 40px 20px;
}
.spinner { .spinner {
width: 40px; width: 40px;
height: 40px; height: 40px;
@@ -312,24 +310,77 @@
padding: 40px 20px; 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 // Responsive
@media (max-width: 1200px) { @media (max-width: 1200px) {
.items-grid { .items-grid {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 24px; gap: 24px;
} }
} }
@media (max-width: 992px) { @media (max-width: 992px) {
.items-grid { .items-grid {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 20px; gap: 20px;
} }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.items-grid { .items-grid {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px; gap: 16px;
} }
@@ -353,7 +404,7 @@
} }
.items-grid { .items-grid {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px; 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 { DecimalPipe } from '@angular/common';
import { ActivatedRoute, RouterLink } from '@angular/router'; import { ActivatedRoute, RouterLink } from '@angular/router';
import { ApiService, CartService } from '../../services'; import { ApiService, CartService } from '../../services';
import { PrefetchService } from '../../services/prefetch.service';
import { Item } from '../../models'; import { Item } from '../../models';
import { Subscription } from 'rxjs'; 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 { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe'; import { TranslatePipe } from '../../i18n/translate.pipe';
import { SCROLL_THRESHOLD_PX, SCROLL_DEBOUNCE_MS, ITEMS_PER_PAGE } from '../../config/constants';
@Component({ @Component({
selector: 'app-category', selector: 'app-category',
@@ -23,7 +26,7 @@ export class CategoryComponent implements OnInit, OnDestroy {
hasMore = signal(true); hasMore = signal(true);
private skip = 0; private skip = 0;
private readonly count = 20; private readonly count = ITEMS_PER_PAGE;
private isLoadingMore = false; private isLoadingMore = false;
private routeSubscription?: Subscription; private routeSubscription?: Subscription;
private scrollTimeout?: ReturnType<typeof setTimeout>; private scrollTimeout?: ReturnType<typeof setTimeout>;
@@ -31,7 +34,8 @@ export class CategoryComponent implements OnInit, OnDestroy {
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private apiService: ApiService, private apiService: ApiService,
private cartService: CartService private cartService: CartService,
private prefetchService: PrefetchService
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@@ -90,12 +94,12 @@ export class CategoryComponent implements OnInit, OnDestroy {
this.scrollTimeout = setTimeout(() => { this.scrollTimeout = setTimeout(() => {
const scrollPosition = window.innerHeight + window.scrollY; 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) { if (scrollPosition >= bottomPosition && !this.loading() && this.hasMore() && !this.isLoadingMore) {
this.loadItems(); this.loadItems();
} }
}, 100); }, SCROLL_DEBOUNCE_MS);
} }
addToCart(itemID: number, event: Event): void { addToCart(itemID: number, event: Event): void {
@@ -104,7 +108,16 @@ export class CategoryComponent implements OnInit, OnDestroy {
this.cartService.addItem(itemID); this.cartService.addItem(itemID);
} }
onItemHover(itemID: number): void {
this.prefetchService.prefetchItem(itemID);
}
readonly skeletonSlots = Array.from({ length: 8 });
readonly getDiscountedPrice = getDiscountedPrice; readonly getDiscountedPrice = getDiscountedPrice;
readonly getMainImage = getMainImage; readonly getMainImage = getMainImage;
readonly trackByItemId = trackByItemId; readonly trackByItemId = trackByItemId;
readonly getBadgeClass = getBadgeClass;
private langService = inject(LanguageService);
itemName(item: Item): string { return getTranslatedField(item, 'name', this.langService.currentLanguage()); }
} }

View File

@@ -18,24 +18,94 @@
<h2>{{ parentName() }}</h2> <h2>{{ parentName() }}</h2>
</header> </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) { @if (subcategories().length > 0) {
<div class="categories-grid"> <div class="categories-grid">
@for (cat of subcategories(); track trackByCategoryId($index, cat)) { @for (cat of subcategories(); track trackByCategoryId($index, cat)) {
<a [routerLink]="['/category', cat.categoryID] | langRoute" class="category-card"> <a [routerLink]="['/category', cat.categoryID] | langRoute" class="category-card">
<div class="category-image"> <div class="category-image">
@if (cat.icon) { @if (cat.icon) {
<img [src]="cat.icon" [alt]="cat.name" loading="lazy" decoding="async" /> <img [src]="cat.icon" [alt]="categoryName(cat)" loading="lazy" decoding="async" />
} @else { } @else {
<div class="category-fallback">{{ cat.name.charAt(0) }}</div> <div class="category-fallback">{{ categoryName(cat).charAt(0) }}</div>
} }
</div> </div>
<div class="category-info"> <div class="category-info">
<h3 class="category-name">{{ cat.name }}</h3> <h3 class="category-name">{{ categoryName(cat) }}</h3>
</div> </div>
</a> </a>
} }
</div> </div>
}
<!-- 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 { } @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">
<div class="no-subcats-icon"> <div class="no-subcats-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <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); 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
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
@@ -248,6 +391,11 @@
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 24px; gap: 24px;
} }
.items-grid {
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
} }
@media (max-width: 992px) { @media (max-width: 992px) {
@@ -273,6 +421,11 @@
gap: 16px; gap: 16px;
} }
.items-grid {
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.category-info { .category-info {
padding: 10px 12px; padding: 10px 12px;
} }
@@ -294,6 +447,11 @@
gap: 12px; gap: 12px;
} }
.items-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.category-info { .category-info {
padding: 8px 10px; padding: 8px 10px;
} }

View File

@@ -1,15 +1,17 @@
import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy, inject } from '@angular/core'; import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy, inject } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { ApiService, LanguageService } from '../../services'; import { ApiService, CartService, LanguageService } from '../../services';
import { Category } from '../../models'; import { Category, Item, Subcategory } from '../../models';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { LangRoutePipe } from '../../pipes/lang-route.pipe'; import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe'; import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service'; import { TranslateService } from '../../i18n/translate.service';
import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass, getTranslatedField, getTranslatedCategoryName } from '../../utils/item.utils';
@Component({ @Component({
selector: 'app-subcategories', selector: 'app-subcategories',
imports: [RouterLink, LangRoutePipe, TranslatePipe], imports: [DecimalPipe, RouterLink, LangRoutePipe, TranslatePipe],
templateUrl: './subcategories.component.html', templateUrl: './subcategories.component.html',
styleUrls: ['./subcategories.component.scss'], styleUrls: ['./subcategories.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
@@ -17,6 +19,10 @@ import { TranslateService } from '../../i18n/translate.service';
export class SubcategoriesComponent implements OnInit, OnDestroy { export class SubcategoriesComponent implements OnInit, OnDestroy {
categories = signal<Category[]>([]); categories = signal<Category[]>([]);
subcategories = 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); loading = signal(true);
error = signal<string | null>(null); error = signal<string | null>(null);
@@ -29,7 +35,8 @@ export class SubcategoriesComponent implements OnInit, OnDestroy {
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private apiService: ApiService, private apiService: ApiService,
private langService: LanguageService private langService: LanguageService,
private cartService: CartService
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@@ -45,19 +52,40 @@ export class SubcategoriesComponent implements OnInit, OnDestroy {
private loadForParent(parentID: number): void { private loadForParent(parentID: number): void {
this.loading.set(true); this.loading.set(true);
this.categoryItems.set([]);
this.nestedSubcategories.set([]);
this.apiService.getCategories().subscribe({ this.apiService.getCategories().subscribe({
next: (cats) => { next: (cats) => {
this.categories.set(cats); this.categories.set(cats);
const subs = cats.filter(c => c.parentID === parentID);
const parent = cats.find(c => c.categoryID === parentID); const parent = cats.find(c => c.categoryID === parentID);
this.parentName.set(parent ? parent.name : this.i18n.t('home.categoriesTitle')); this.parentName.set(parent ? getTranslatedCategoryName(parent, this.langService.currentLanguage()) : 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 // No subcategories: redirect to items list for this category
const lang = this.langService.currentLanguage(); const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}/category`, parentID, 'items'], { replaceUrl: true }); this.router.navigate([`/${lang}/category`, parentID, 'items'], { replaceUrl: true });
} else {
this.subcategories.set(subs);
} }
this.loading.set(false); this.loading.set(false);
@@ -70,8 +98,43 @@ 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 // TrackBy function for performance optimization
trackByCategoryId(index: number, category: Category): number { trackByCategoryId(index: number, category: Category): number {
return category.categoryID; 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()); }
categoryName(cat: Category): string { return getTranslatedCategoryName(cat, this.langService.currentLanguage()); }
} }

View File

@@ -19,10 +19,23 @@
<app-items-carousel /> <app-items-carousel />
@if (loading()) { @if (loading()) {
<div class="novo-loading"> <section class="novo-categories">
<div class="novo-spinner"></div> <div class="novo-section-header">
<p>{{ 'home.loading' | translate }}</p> <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>
<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()) { @if (error()) {
@@ -53,15 +66,15 @@
<a [routerLink]="['/category', category.categoryID] | langRoute" class="novo-category-card"> <a [routerLink]="['/category', category.categoryID] | langRoute" class="novo-category-card">
<div class="novo-category-image"> <div class="novo-category-image">
@if (category.icon) { @if (category.icon) {
<img [src]="category.icon" [alt]="category.name" loading="lazy" /> <img [src]="category.icon" [alt]="categoryName(category)" loading="lazy" />
} @else { } @else {
<div class="novo-category-placeholder"> <div class="novo-category-placeholder">
<span>{{ category.name.charAt(0) }}</span> <span>{{ categoryName(category).charAt(0) }}</span>
</div> </div>
} }
</div> </div>
<div class="novo-category-info"> <div class="novo-category-info">
<h3>{{ category.name }}</h3> <h3>{{ categoryName(category) }}</h3>
<span class="novo-category-arrow"></span> <span class="novo-category-arrow"></span>
</div> </div>
</a> </a>
@@ -101,10 +114,20 @@
<app-items-carousel /> <app-items-carousel />
@if (loading()) { @if (loading()) {
<div class="dexar-loading"> <section class="dexar-categories">
<div class="dexar-spinner"></div> <div class="skeleton-line" style="height: 36px; width: 220px; margin-bottom: 40px;"></div>
<p>{{ 'home.loadingDexar' | translate }}</p> <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>
}
</div>
</section>
} }
@if (error()) { @if (error()) {
@@ -131,15 +154,15 @@
[class.dexar-category-card--wide]="isWideCategory(category.categoryID)"> [class.dexar-category-card--wide]="isWideCategory(category.categoryID)">
<div class="dexar-category-image"> <div class="dexar-category-image">
@if (isWideCategory(category.categoryID) && category.wideBanner) { @if (isWideCategory(category.categoryID) && category.wideBanner) {
<img [src]="category.wideBanner" [alt]="category.name" loading="lazy" decoding="async" /> <img [src]="category.wideBanner" [alt]="categoryName(category)" loading="lazy" decoding="async" />
} @else if (category.icon) { } @else if (category.icon) {
<img [src]="category.icon" [alt]="category.name" loading="lazy" decoding="async" /> <img [src]="category.icon" [alt]="categoryName(category)" loading="lazy" decoding="async" />
} @else { } @else {
<div class="dexar-category-fallback">{{ category.name.charAt(0) }}</div> <div class="dexar-category-fallback">{{ categoryName(category).charAt(0) }}</div>
} }
</div> </div>
<div class="dexar-category-info"> <div class="dexar-category-info">
<h3 class="dexar-category-name">{{ category.name }}</h3> <h3 class="dexar-category-name">{{ categoryName(category) }}</h3>
<p class="dexar-category-count">{{ 'home.itemsCount' | translate:{ count: getItemCount(category.categoryID) } }}</p> <p class="dexar-category-count">{{ 'home.itemsCount' | translate:{ count: getItemCount(category.categoryID) } }}</p>
</div> </div>
</a> </a>

View File

@@ -896,3 +896,26 @@
transform: translateY(-2px); 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,8 +1,9 @@
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 { Router, RouterLink } from '@angular/router';
import { ApiService, LanguageService } from '../../services'; import { ApiService, LanguageService } from '../../services';
import { Category } from '../../models'; import { Category } from '../../models';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { getTranslatedCategoryName } from '../../utils/item.utils';
import { ItemsCarouselComponent } from '../../components/items-carousel/items-carousel.component'; import { ItemsCarouselComponent } from '../../components/items-carousel/items-carousel.component';
import { LangRoutePipe } from '../../pipes/lang-route.pipe'; import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe'; import { TranslatePipe } from '../../i18n/translate.pipe';
@@ -14,13 +15,14 @@ import { TranslatePipe } from '../../i18n/translate.pipe';
styleUrls: ['./home.component.scss'], styleUrls: ['./home.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class HomeComponent implements OnInit { export class HomeComponent implements OnInit, OnDestroy {
brandName = environment.brandFullName; brandName = environment.brandFullName;
isnovo = environment.theme === 'novo'; isnovo = environment.theme === 'novo';
categories = signal<Category[]>([]); categories = signal<Category[]>([]);
wideCategories = signal<Set<number>>(new Set()); wideCategories = signal<Set<number>>(new Set());
loading = signal(true); loading = signal(true);
error = signal<string | null>(null); error = signal<string | null>(null);
readonly skeletonSlots = Array.from({ length: 6 });
// Memoized computed values for performance // Memoized computed values for performance
topLevelCategories = computed(() => { topLevelCategories = computed(() => {
@@ -56,6 +58,14 @@ export class HomeComponent implements OnInit {
this.loadCategories(); this.loadCategories();
} }
ngOnDestroy(): void {
this.pendingImages.forEach(img => {
img.onload = null;
img.onerror = null;
});
this.pendingImages.clear();
}
loadCategories(): void { loadCategories(): void {
this.loading.set(true); this.loading.set(true);
this.apiService.getCategories().subscribe({ this.apiService.getCategories().subscribe({
@@ -84,13 +94,17 @@ export class HomeComponent implements OnInit {
return this.wideCategories().has(categoryID); return this.wideCategories().has(categoryID);
} }
private pendingImages = new Set<HTMLImageElement>();
private detectWideImages(categories: Category[]): void { private detectWideImages(categories: Category[]): void {
const topLevel = categories.filter(c => c.parentID === 0); const topLevel = categories.filter(c => c.parentID === 0);
topLevel.forEach(cat => { topLevel.forEach(cat => {
if (!cat.wideBanner) return; if (!cat.wideBanner) return;
const img = new Image(); const img = new Image();
this.pendingImages.add(img);
img.onload = () => { img.onload = () => {
this.pendingImages.delete(img);
const ratio = img.naturalWidth / img.naturalHeight; const ratio = img.naturalWidth / img.naturalHeight;
if (ratio > 2) { if (ratio > 2) {
this.wideCategories.update(set => { this.wideCategories.update(set => {
@@ -100,6 +114,7 @@ export class HomeComponent implements OnInit {
}); });
} }
}; };
img.onerror = () => this.pendingImages.delete(img);
img.src = cat.wideBanner; img.src = cat.wideBanner;
}); });
} }
@@ -109,6 +124,10 @@ export class HomeComponent implements OnInit {
this.router.navigate([`/${lang}/search`]); this.router.navigate([`/${lang}/search`]);
} }
categoryName(cat: Category): string {
return getTranslatedCategoryName(cat, this.langService.currentLanguage());
}
scrollToCatalog(): void { scrollToCatalog(): void {
const target = document.getElementById('catalog'); const target = document.getElementById('catalog');
if (!target) return; if (!target) return;

View File

@@ -1,10 +1,10 @@
<div class="legal-page"> <div class="legal-page">
<div class="legal-container"> <div class="legal-container">
<h1>About the company LLC «INT FIN LOGISTIC»</h1> <h1>About the companies LLC «INT FIN LOGISTIC» and LLC «INT FACTORING»</h1>
<section class="legal-section"> <section class="legal-section">
<h2>About us</h2> <h2>About us</h2>
<p>DexarMarket is a rapidly growing marketplace actively operating in the trade of various goods and services. The registration of the legal entity ООО "ИНТ ФИН ЛОГИСТИК" in accordance with Armenian legislation underscores the international focus of the business, as the company also successfully operates in the Russian market with all necessary credentials for legal activity in the Russian Federation.</p> <p>DexarMarket is a rapidly growing marketplace actively operating in the trade of various goods and services. The registration of the legal entities ООО "ИНТ ФИН ЛОГИСТИК" and ООО "ИНТ ФАКТОРИНГ" in accordance with Armenian legislation underscores the international focus of the business, as the companies also successfully operate in the Russian market with all necessary credentials for legal activity in the Russian Federation.</p>
<p>DEXARMARKET began its operations in Armenia, but expansion happened swiftly. By the summer of 2025, the platform had entered the Russian market, showing significant growth in popularity among partners and buyers from various regions of the world, including Russia, the United Arab Emirates, Turkey, China, Armenia, Kazakhstan, Kyrgyzstan, and other countries.</p> <p>DEXARMARKET began its operations in Armenia, but expansion happened swiftly. By the summer of 2025, the platform had entered the Russian market, showing significant growth in popularity among partners and buyers from various regions of the world, including Russia, the United Arab Emirates, Turkey, China, Armenia, Kazakhstan, Kyrgyzstan, and other countries.</p>
@@ -65,8 +65,11 @@
<p><strong>Director:</strong> Оганнисян Ашот Рафикович</p> <p><strong>Director:</strong> Оганнисян Ашот Рафикович</p>
<p><strong>Legal address:</strong><br>АРМЕНИЯ, 2301, КОТАЙКСКАЯ ОБЛАСТЬ, РАЗДАН, ХАЧАТРЯНА ул, 31, 4</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 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>Key details:</strong><br>ИНН (RF): 9909697628<br>ИНН (Armenia): 03033502<br>КПП: 770287001<br>ОГРН: 85.110.1408711</p>
<br>
<p><strong>Full company name:</strong><br>ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ИНТ ФАКТОРИНГ»</p>
<p><strong>ИНН:</strong> 9909697635</p>
<p><strong>Banking details:</strong><br>Bank: АО "Райффайзенбанк"<br>Settlement account: 40807810500000002376<br>Correspondent account: 30101810200000000700<br>БИК: 044525700</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> <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>
</section> </section>

View File

@@ -1,54 +1,54 @@
<div class="legal-page"> <div class="legal-page">
<div class="legal-container"> <div class="legal-container">
<h1>«ИНТ ФИН ЛОГИСТИК» ՍՊԸ ընկերության մասին</h1> <h1>«ԻՆՏ ՖԻՆ ԼՈԳԻՍՏԻԿ» և «ԻՆՏ ՖԱԿՏՈՐԻՆԳ» ՍՊԸ ընկերությունների մասին</h1>
<section class="legal-section"> <section class="legal-section">
<h2>Մեր մասին</h2> <h2>Մեր մասին</h2>
<p>DexarMarket-ը արագորեն զարգացող մարկեթփլեյս է, որն ակտիվորեն գործում է տարբեր ապրանքների և ծառայությունների առևտրի ոլորտում։ «ИНТ ФИН ЛОГИСТИК» ՍՊԸ իրավաբանական անձի գրանցումը, իրականացված Հայաստանի օրենսդրությանը համաձայն, ընդգծում է բիզնեսի միջազգային ուղղվածությունը, քանի որ ընկերությունը նաև հաջողությամբ գործում է ռուսական շուկայում։</p> <p>DexarMarket-ը արագ զարգացող առցանց առևտրային հարթակ է, որը գործում է տարբեր ապրանքների և ծառայությունների վաճառքի ոլորտում։ «ԻՆՏ ՖԻՆ ԼՈԳԻՍՏԻԿ» և «ԻՆՏ ՖԱԿՏՈՐԻՆԳ» ՍՊԸ-ների գրանցումը Հայաստանի օրենսդրությանը համապատասխան ընդգծում է բիզնեսի միջազգային ուղղվածությունը, քանի որ ընկերությունները հաջողությամբ աշխատում են նաև ռուսական շուկայում։</p>
<p>DEXARMARKET-ը իր գործունեությունը սկսել է հենց Հայաստանում, սակայն ընդլայնումը տեղի ունեցավ արագ։ 2025 թվականի ամռանը հարթակը մուտք գործեց ռուսական շուկա, ցույց տալով ժողովրդականության զգալի աճ գործընկերների և գնորդների շրջանում, ներառյալ Րուսաստանը, Միացյալ Արաբական Էմիրությունները, Թուրքիան, Չինաստանը, Հայաստանը, Ղազախստանը, Ղրղզստանը և այլ երկրները։</p> <p>DEXARMARKET-ը իր գործունեությունը սկսել է Հայաստանում, իսկ հետագա ընդլայնումը տեղի է ունեցել արագ տեմպերով։ 2025 թվականի ամռանը հարթակը մուտք գործեց ռուսական շուկա և կարճ ժամանակում զգալի ճանաչում ձեռք բերեց գործընկերների և գնորդների շրջանում Ռուսաստանում, Միացյալ Արաբական Էմիրություններում, Թուրքիայում, Չինաստանում, Հայաստանում, Ղազախստանում, Ղրղզստանում և այլ երկրներում։</p>
<p>Ընկերության հիմնական նպատակը որակյալ ծառայություններ մատուցելն է իր գործընկերներին և գնորդներին, ապահովելով գործարքների հարմարությունը և հուսալիությունը, ընդլայնելով ապրանքների և ծառայությունների տեսականիքը, ինչպես նաև պահպանելով հաճախորդների սպասարկման բարձր չափանիշները։</p> <p>Ընկերության հիմնական նպատակը գործընկերներին և գնորդներին որակյալ ծառայություններ մատուցելն է, գործարքների հարմարությունն ու հուսալիությունն ապահովելը, ապրանքների և ծառայությունների տեսականու ընդլայնումը, ինչպես նաև հաճախորդների սպասարկման բարձր չափանիշների պահպանումը։</p>
<p>Այսպիսով, DEXARMARKET-ը հաջող միջազգային նախագծի վառք օրինակ է, որը ցույց է տալիս հաջող զարգացում և ինտեգրացիա համաշխարհային մակարդակով։</p> <p>Այսպիսով, DEXARMARKET-ը հաջող միջազգային նախագծի օրինակ է, որը ցույց է տալիս կայուն զարգացում և արդյունավետ ինտեգրում միջազգային մակարդակում։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>Մեր առաքելությունը</h2> <h2>Մեր առաքելությունը</h2>
<p>Մենք ձգտում ենք ստեղծել յուրահատուկ էկոհամակարգ, որը ապահովում է առավելագույն հարմարություն և օգուտ մեր գործընկերներին DEXARMARKET հարթակում ապրանքներ տեղադրելիս։ Մենք առաջարկում ենք լայն շրջանակի լրացուցիչ ծառայություններ, որոնք օգնում են օպտիմալացնել մեր վաճառողների բիզնես գործընթացները։</p> <p>Մենք ձգտում ենք ստեղծել այնպիսի էկոհամակարգ, որը մեր գործընկերներին կապահովի առավելագույն հարմարություն և օգուտ DEXARMARKET հարթակում ապրանքներ տեղադրելիս։ Մենք առաջարկում ենք լրացուցիչ ծառայությունների լայն շրջանակ, որոնք օգնում են օպտիմալացնել վաճառողների բիզնես գործընթացները։</p>
<p>Գնորդների համար մենք ապահովում ենք հարմար մուտք դեպի որակյալ ապրանքների բազմազան տեսականիք գրավիչ գներով անմիջապես արտադրողներից և մատակարարներից։ Սա թույլ է տալիս հաճախորդներին խնայել ժամանակ և միջոցներ, ընտրելով շուկայի լավագույն առաջարկները։</p> <p>Գնորդներին մենք ապահովում ենք հարմար հասանելիություն դեպի որակյալ ապրանքների լայն տեսականի` գրավիչ գներով, անմիջապես արտադրողներից և մատակարարներից։ Սա հաճախորդներին թույլ է տալիս խնայել ժամանակ և միջոցներ` ընտրելով շուկայի լավագույն առաջարկները։</p>
<h3>Մեր հիմնական առաջնահերթություններն են՝</h3> <h3>Մեր հիմնական առաջնահերթություններն են՝</h3>
<ul> <ul>
<li>Ստեղծել թափանցիկ և արդյունավետ համակարգ վաճառողների և գնորդների միջև։</li> <li>Ստեղծել թափանցիկ և արդյունավետ համագործակցության համակարգ վաճառողների և գնորդների միջև։</li>
<li>Տրամադրել նորարարական լուծումներ՝ մեր գործընկերների մրցունակությունը բարձրացնելու և վաճառքները ավելացնելու համար։</li> <li>Տրամադրել նորարարական լուծումներ` գործընկերների մրցունակությունը բարձրացնելու և վաճառքներն ավելացնելու համար։</li>
<li>Ապահովել հաճախորդների սպասարկման և օգտատերների աջակցության բարձր մակարդակ համագործակցության բոլոր փուլերում։</li> <li>Ապահովել հաճախորդների սպասարկման և օգտատերերի աջակցության բարձր մակարդակ համագործակցության բոլոր փուլերում։</li>
</ul> </ul>
<p>Մենք մշտապես աշխատում ենք մեր հարթակի ֆունկցիոնալության բարելավման վրա, ներդրելով նոր տեխնոլոգիաներ և գործիքներ, որպեսզի գնում և վաճառքի գործընթացը ավելի հարմար և շահավետ դարձնենք շուկայի բոլոր մասնակիցների համար։</p> <p>Մենք մշտապես աշխատում ենք հարթակի ֆունկցիոնալության բարելավման վրա` ներդնելով նոր տեխնոլոգիաներ և գործիքներ, որպեսզի գնումների և վաճառքի գործընթացը դարձնենք ավելի հարմար և շահավետ շուկայի բոլոր մասնակիցների համար։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>DEXARMARKET ընկերության պատմությունը</h2> <h2>DEXARMARKET ընկերության պատմությունը</h2>
<p>DEXARMARKET ընկերությունը ստեղծվել է մասնագետների թիմի ջանքերի շնորհիվ, որոնք միավորվել էին ընդհանուր նպատակով և փորձով հիմնական ոլորտներում՝ տեղեկատվական-հաղորդակցական տեխնոլոգիաների (IT), տնտեսագիտության և առևտրային ոլորտի։ Հիմնադիրները հասկանում էին առաջատար տեխնոլոգիական լուծումների ինտեգրման և շուկայական գործընթացների խորը ըմբռնման կարևորությունը, ինչը հնարավորություն տվեց ձևավորել մարկեթփլեյսի զարգացման արդյունավետ հայեցակարգ։</p> <p>DEXARMARKET-ը ստեղծվել է մասնագետների թիմի ջանքերով, որոնք միավորվել էին ընդհանուր տեսլականի շուրջ և ունեին փորձ տեղեկատվական տեխնոլոգիաների, տնտեսագիտության և առևտրի ոլորտներում։ Հիմնադիրները հասկանում էին առաջատար տեխնոլոգիական լուծումների ինտեգրման և շուկայական գործընթացների խորքային ըմբռնման կարևորությունը, ինչը հնարավորություն տվեց ձևավորել հարթակի զարգացման արդյունավետ հայեցակարգ։</p>
<h3>Հիմնական փուլերը՝</h3> <h3>Հիմնական փուլերը՝</h3>
<h4>Գաղափարի ծնունդը</h4> <h4>Գաղափարի ծնունդը</h4>
<p>Թիմը, որը ուներ խորը գիտելիքներ թվային բիզնեսի տրանսֆորմացիայի և տնտեսական գործընթացների ոլորտում, միավորվեց հավակնորդ նպատակի շուրջ՝ ստեղծել հարթակ, որը նպաստում է ձեռներկատիրության և առևտրի զարգացմանը միջազգային մակարդակով։</p> <p>Թիմը, որն ուներ խոր գիտելիքներ թվային բիզնեսի վերափոխման և տնտեսական գործընթացների ոլորտում, միավորվեց հավակնոտ նպատակի շուրջ` ստեղծել հարթակ, որը կնպաստի ձեռնարկատիրության և առևտրի զարգացմանը միջազգային մակարդակում։</p>
<h4>Հայեցակարգի մշակումը</h4> <h4>Հայեցակարգի մշակումը</h4>
<p>Գիտակցելով, որ անհրաժեշտ է տրամադրել հարմար գործիքներ օնլայն բիզնեսի արդյունավետ կառավարման համար, թիմը սկսեց մարկեթփլեյսի յուրահատուկ մոդելի մշակում, հիմնված էլեկտրոնային առևտրի և մեծ տվյալների վերլուծության վերջին նվաճումների վրա։</p> <p>Գիտակցելով, որ առցանց բիզնեսի արդյունավետ կառավարման համար անհրաժեշտ են հարմար գործիքներ, թիմը սկսեց մշակել հարթակի յուրահատուկ մոդելը` հիմնված էլեկտրոնային առևտրի և մեծ տվյալների վերլուծության արդի հնարավորությունների վրա։</p>
<h4>Հարթակի գործարկումը</h4> <h4>Հարթակի գործարկումը</h4>
<p>Օգտագործելով նորարարական լուծումներ տեղեկատվական ենթակառուցվածքում և տվյալների պաշտպանության հուսալի մեխանիզմներով, թիմը ստեղծեց կայուն թվային միջավայր, գրավիչ ձեռներկատերների և վերջնական սպառողների համար։</p> <p>Տեղեկատվական ենթակառուցվածքում կիրառելով նորարարական լուծումներ և տվյալների պաշտպանության հուսալի մեխանիզմներ` թիմը ստեղծեց կայուն թվային միջավայր, որը գրավիչ է ինչպես ձեռնարկատերերի, այնպես էլ վերջնական սպառողների համար։</p>
<h4>Միջազգային մակարդակ դուրս գալը</h4> <h4>Միջազգային մակարդակ դուրս գալը</h4>
<p>Ազգային շուկայում ճանաչում ստանալով՝ DEXARMARKET-ը սկսեց իր ակտիվ առաջմումը միջազգային շուկաներում, աստիճանաբար ընդգրկելով տարբեր տարածաշրջաններ և երկրներ, ստեղծելով պայմաններ փոքր և միջին բիզնեսի, ինչպես նաև խոշոր առևտրային ընկերությունների աճի համար։</p> <p>Տեղական շուկայում ճանաչում ձեռք բերելուց հետո DEXARMARKET-ը սկսեց ակտիվորեն ընդլայնվել միջազգային շուկաներում` աստիճանաբար ընդգրկելով տարբեր տարածաշրջաններ և երկրներ ու ստեղծելով աճի պայմաններ փոքր և միջին բիզնեսի, ինչպես նաև խոշոր առևտրային ընկերությունների համար։</p>
<p><strong>Այսօր DEXARMARKET-ը շարունակում է զարգանալ, կատարելագործելով իր տեխնոլոգիական հնարավորությունները և օգտատերներին առաջարկելով ավելի շատ առավելություններ և հնարավորություններ հաջող առևտրային գործունեության համար։</strong></p> <p><strong>Այսօր DEXARMARKET-ը շարունակում է զարգանալ, կատարելագործել իր տեխնոլոգիական հնարավորությունները և օգտատերերին առաջարկել ավելի շատ առավելություններ ու հնարավորություններ հաջող առևտրային գործունեության համար։</strong></p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
@@ -65,8 +65,10 @@
<p><strong>Տնօրեն՝</strong> Оганнисян Ашот Рафикович</p> <p><strong>Տնօրեն՝</strong> Оганнисян Ашот Рафикович</p>
<p><strong>Իրավաբանական հասցե՝</strong><br>АРМЕНИЯ, 2301, КОТАЙКСКАЯ ОБЛАСТЬ, РАЗДАН, ХАЧАТРЯНА ул, 31, 4</p> <p><strong>Իրավաբանական հասցե՝</strong><br>АРМЕНИЯ, 2301, КОТАЙКСКАЯ ОБЛАСТЬ, РАЗДАН, ХАЧАТРЯНА ул, 31, 4</p>
<p><strong>Գրասենյակ Հայաստանում՝</strong><br>0033, Երևան, Եղբայրներ Օրբելի փողոց, 47</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>ՀՍՀ (ՌՄ)՝ 9909697628<br>ՀՍՀ (Հայաստան)՝ 03033502<br>ԿՊՊ՝ 770287001<br>ՕԳՌՆ՝ 85.110.1408711</p>
<br>
<p><strong>Կազմակերպության լիարժեք անվանումը՝</strong><br>ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ИНТ ФАКТОРИНГ»</p>
<p><strong>ИНН:</strong> 9909697635</p>
<p><strong>Բանկային վավերապայմանները՝</strong><br>Բանկ՝ АО "Райффайзенбанк"<br>Հաշվարկային հաշիվ՝ 40807810500000002376<br>Թղթակցային հաշիվ՝ 30101810200000000700<br>ԲԻԿ՝ 044525700</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> <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>
</section> </section>

View File

@@ -1,10 +1,10 @@
<div class="legal-page"> <div class="legal-page">
<div class="legal-container"> <div class="legal-container">
<h1>О компании ООО «ИНТ ФИН ЛОГИСТИК»</h1> <h1>О компаниях ООО «ИНТ ФИН ЛОГИСТИК» и ООО «ИНТ ФАКТОРИНГ»</h1>
<section class="legal-section"> <section class="legal-section">
<h2>О нас</h2> <h2>О нас</h2>
<p>Компания DexarMarket действительно представляет собой быстроразвивающийся маркетплейс, активно функционирующий в области торговли различными товарами и услугами. Регистрация юридического лица ООО "ИНТ ФИН ЛОГИСТИК", осуществленная согласно законодательству Армении, подчеркивает международную направленность бизнеса, поскольку компания также успешно работает на российском рынке, имея необходимые реквизиты для легальной деятельности в РФ.</p> <p>Компания DexarMarket действительно представляет собой быстроразвивающийся маркетплейс, активно функционирующий в области торговли различными товарами и услугами. Регистрация юридических лиц ООО "ИНТ ФИН ЛОГИСТИК" и ООО "ИНТ ФАКТОРИНГ", осуществленная согласно законодательству Армении, подчеркивает международную направленность бизнеса, поскольку компании также успешно работают на российском рынке, имея необходимые реквизиты для легальной деятельности в РФ.</p>
<p>Начало своей деятельности DEXARMARKET положил именно в Армении, однако расширение произошло стремительно. Уже летом 2025 года площадка вышла на российский рынок, показывая значительный рост популярности среди партнеров и покупателей из разных регионов мира, включая Россию, Объединённые Арабские Эмираты, Турцию, Китай, Армению, Казахстан, Кыргызстан и другие государства.</p> <p>Начало своей деятельности DEXARMARKET положил именно в Армении, однако расширение произошло стремительно. Уже летом 2025 года площадка вышла на российский рынок, показывая значительный рост популярности среди партнеров и покупателей из разных регионов мира, включая Россию, Объединённые Арабские Эмираты, Турцию, Китай, Армению, Казахстан, Кыргызстан и другие государства.</p>
@@ -65,8 +65,11 @@
<p><strong>Директор:</strong> Оганнисян Ашот Рафикович</p> <p><strong>Директор:</strong> Оганнисян Ашот Рафикович</p>
<p><strong>Юридический адрес:</strong><br>АРМЕНИЯ, 2301, КОТАЙКСКАЯ ОБЛАСТЬ, РАЗДАН, ХАЧАТРЯНА ул, 31, 4</p> <p><strong>Юридический адрес:</strong><br>АРМЕНИЯ, 2301, КОТАЙКСКАЯ ОБЛАСТЬ, РАЗДАН, ХАЧАТРЯНА ул, 31, 4</p>
<p><strong>Офис в Армении:</strong><br>0033, Ереван, улица Братьев Орбели, 47</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>ИНН (РФ): 9909697628<br>ИНН (Армения): 03033502<br>КПП: 770287001<br>ОГРН: 85.110.1408711</p>
<br>
<p><strong>Полное наименование организации:</strong><br>ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ИНТ ФАКТОРИНГ»</p>
<p><strong>ИНН:</strong> 9909697635</p>
<p><strong>Банковские реквизиты:</strong><br>Банк: АО "Райффайзенбанк"<br>Расчетный счет: 40807810500000002376<br>Корр. счет: 30101810200000000700<br>БИК: 044525700</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> <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>
</section> </section>

View File

@@ -1,4 +1,4 @@
<div class="legal-page"> <div class="legal-page">
<div class="legal-container"> <div class="legal-container">
<h1>Contacts</h1> <h1>Contacts</h1>
@@ -11,6 +11,9 @@
<h2>Company Details</h2> <h2>Company Details</h2>
<p><strong>Company name:</strong> INT FIN LOGISTIC LLC</p> <p><strong>Company name:</strong> INT FIN LOGISTIC LLC</p>
<p><strong>TIN:</strong> 9909697628</p> <p><strong>TIN:</strong> 9909697628</p>
<br>
<p><strong>Company name:</strong> INT FACTORING LLC</p>
<p><strong>TIN:</strong> 9909697635</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
@@ -35,7 +38,7 @@
<section class="legal-section"> <section class="legal-section">
<h2>Office Addresses</h2> <h2>Office Addresses</h2>
<p><strong>Office in Armenia:</strong> 0033, Yerevan, Orbeli Brothers Street, 47</p> <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>
<section class="legal-section"> <section class="legal-section">

View File

@@ -1,4 +1,4 @@
<div class="legal-page"> <div class="legal-page">
<div class="legal-container"> <div class="legal-container">
<h1>Կապ</h1> <h1>Կապ</h1>
@@ -11,6 +11,9 @@
<h2>Կազմակերպության տվյալներ</h2> <h2>Կազմակերպության տվյալներ</h2>
<p><strong>Անվանումը՝</strong> ՍՊԸ «ԻՆՏ ՖԻՆ ԼՈԳԻՍՏԻԿ»</p> <p><strong>Անվանումը՝</strong> ՍՊԸ «ԻՆՏ ՖԻՆ ԼՈԳԻՍՏԻԿ»</p>
<p><strong>ՀՍՀ՝</strong> 9909697628</p> <p><strong>ՀՍՀ՝</strong> 9909697628</p>
<br>
<p><strong>Անվանումը՝</strong> ՍՊԸ «ԻՆՏ ՖԱԿՏՈՌԻՆԳ»</p>
<p><strong>ՀՍՀ՝</strong> 9909697635</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
@@ -35,7 +38,6 @@
<section class="legal-section"> <section class="legal-section">
<h2>Գրասենյակների հասցեներ</h2> <h2>Գրասենյակների հասցեներ</h2>
<p><strong>Գրասենյակ Հայաստանում՝</strong> 0033, Երևան, Եղբայրներ Օրբելի փողոց, 47</p> <p><strong>Գրասենյակ Հայաստանում՝</strong> 0033, Երևան, Եղբայրներ Օրբելի փողոց, 47</p>
<p><strong>Գրասենյակ Ռուսաստանում՝</strong> 121059, Մոսկվա, Տարաս Շևչենկոի փողոց, 3կ2</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">

View File

@@ -1,4 +1,4 @@
<div class="legal-page"> <div class="legal-page">
<div class="legal-container"> <div class="legal-container">
<h1>Контакты</h1> <h1>Контакты</h1>
@@ -11,6 +11,9 @@
<h2>Реквизиты организации</h2> <h2>Реквизиты организации</h2>
<p><strong>Наименование:</strong> ООО «ИНТ ФИН ЛОГИСТИК»</p> <p><strong>Наименование:</strong> ООО «ИНТ ФИН ЛОГИСТИК»</p>
<p><strong>ИНН:</strong> 9909697628</p> <p><strong>ИНН:</strong> 9909697628</p>
<br>
<p><strong>Наименование:</strong> ООО «ИНТ ФАКТОРИНГ»</p>
<p><strong>ИНН:</strong> 9909697635</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
@@ -35,7 +38,7 @@
<section class="legal-section"> <section class="legal-section">
<h2>Адреса офисов</h2> <h2>Адреса офисов</h2>
<p><strong>Офис в Армении:</strong> 0033, Ереван, улица Братьев Орбели, 47</p> <p><strong>Офис в Армении:</strong> 0033, Ереван, улица Братьев Орбели, 47</p>
<p><strong>Офис в России:</strong> 121059, Москва, наб. Тараса Шевченко, 3к2</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">

View File

@@ -4,52 +4,52 @@
<section class="legal-section"> <section class="legal-section">
<h2>1. Առաքման եղանակներ</h2> <h2>1. Առաքման եղանակներ</h2>
<p>1.1. <strong>Թվային ապրանքներ.</strong> DexarMarket հարթակն իրականացնում է թվային ապրանքների (լիցենզիային բանալիներ, էլեկտրոնային վկայականներ, թվային բովանդակություն և այլն) առաքումը Գնորդի կողմից նշված էլեկտրոնային փոստի հասցեին կամ հարթակում անձնական հաշվին։ վճարման հաստատմանից անմիջապես հետո։</p> <p>1.1. <strong>Թվային ապրանքներ.</strong> DexarMarket հարթակը թվային ապրանքները (լիցենզիայի բանալիներ, էլեկտրոնային վկայականներ, թվային բովանդակություն և այլն) ուղարկում է Գնորդի նշած էլեկտրոնային փոստին կամ անձնական հաշվին` վճարումը հաստատվելուց անմիջապես հետո։</p>
<p>1.2. <strong>Նյութական ապրանքներ.</strong> Վաճառողն ինքնուրույն իրականացնում է նյութական ապրանքների առաքումը՝ ներգրավելով տրանսպորտային ընկերություններ և փոստային օպերատորներ՝ ներառյալ, բայց չսահմանափակվաթ՝ <strong>ՍԴԷԿ</strong>, <strong>Ռուսաստանի փոստ</strong>, <strong>Boxberry</strong>, <strong>DPD</strong>, <strong>Yandex.Առաքում</strong> և այլ փոխադրողներ՝ Գնորդի հետ համաձայնությամբ։</p> <p>1.2. <strong>Նյութական ապրանքներ.</strong> Նյութական ապրանքների առաքումը Վաճառողն իրականացնում է ինքնուրույն` ներգրավելով տրանսպորտային ընկերություններ և փոստային օպերատորներ, այդ թվում` բայց չսահմանափակվելով <strong>СДЭК</strong>, <strong>Почта России</strong>, <strong>Boxberry</strong>, <strong>DPD</strong>, <strong>Yandex.Доставка</strong> և այլ փոխադրողներով` Գնորդի հետ համաձայնեցմամբ։</p>
<p>1.3. Նյութական ապրանքների առաքման եղանակները, հասանելի կոնկրետ Պատվերի համար, որոշվում են Վաճառողի կողմից Պատվերի ձևակերպման պահին և կարող են ներառել՝</p> <p>1.3. Կոնկրետ պատվերի համար հասանելի առաքման եղանակները որոշվում են Վաճառողի կողմից պատվերի ձևակերպման պահին և կարող են ներառել`</p>
<ul> <ul>
<li>սուրհանդարակային առաքում (ՍԴԷԿ, DPD, Yandex.Առաքում, այլ սուրհանդարակային ծառայություններ)։</li> <li>սուրհանդակային առաքում (СДЭК, DPD, Yandex.Доставка և այլ սուրհանդակային ծառայություններ),</li>
<li>առաքում ստացման կետ (Boxberry, ՍԴԷԿ, Հայփոստ)։</li> <li>առաքում ստացման կետ (Boxberry, СДЭК, Почта России),</li>
<li>փոստային առաքում (Հայփոստ)։</li> <li>փոստային առաքում (Почта России),</li>
<li>ինքնավերցնել (եթե կիրառելի է տվյալ ապրանքի համար)։</li> <li>ինքնուրույն ստացում, եթե դա կիրառելի է տվյալ ապրանքի համար։</li>
</ul> </ul>
<p>1.4. Առաքման կոնկրետ եղանակի ընտրությունը կատարվում է Գնորդի կողմից Պատվերի ձևակերպման գործընթացում՝ Վաճառողի առաիադրած հասանելի տարբերակների շրիանակում։</p> <p>1.4. Առաքման կոնկրետ եղանակը Գնորդն ընտրում է պատվերի ձևակերպման ընթացքում` Վաճառողի առաջարկած հասանելի տարբերակներից։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>2. Առաքման արժեքը</h2> <h2>2. Առաքման արժեքը</h2>
<p>2.1. <strong>Թվային ապրանքներ.</strong> Թվային ապրանքների առաքումն անվճար է։</p> <p>2.1. <strong>Թվային ապրանքներ.</strong> Թվային ապրանքների առաքումն անվճար է։</p>
<p>2.2. <strong>Նյութական ապրանքներ.</strong> Նյութական ապրանքների առաքման արժեքը որոշվում է Վաճառողի կողմից՝ կախված առաքման եղանակից, առաքման քաշից և չափսերից, առաքման հասցեից և փոխադրողի գործող սակագներից՝ Պատվերի ձևակերպման պահին։</p> <p>2.2. <strong>Նյութական ապրանքներ.</strong> Նյութական ապրանքների առաքման արժեքը որոշվում է Վաճառողի կողմից` կախված ընտրված եղանակից, ծանրոցի քաշից ու չափերից, առաքման հասցեից և փոխադրողի գործող սակագներից պատվերի ձևակերպման պահին։</p>
<p>2.3. Նյութական ապրանքների առաքման վերջնական արժեքը ցուցադրվում է Գնորդին մինչև Պատվերի հաստատումը և ներառվում է վճարման ընդհանուր գումարում։</p> <p>2.3. Նյութական ապրանքների առաքման վերջնական արժեքը ցուցադրվում է Գնորդին մինչև պատվերի հաստատումը և ներառվում է վճարման ընդհանուր գումարում։</p>
<p>2.4. Ակցիաների և հատուկ առածների դեպքում առաքումը կարող է իրականացվել անվճար՝ ակցիայի նկարգրում նշված պայմաններին համապատասխան։</p> <p>2.4. Ակցիաների և հատուկ առաջարկների դեպքում առաքումը կարող է իրականացվել անվճար` ակցիայի նկարագրության մեջ նշված պայմաններին համապատասխան։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>3. Առաքման ժամկետները</h2> <h2>3. Առաքման ժամկետները</h2>
<p>3.1. <strong>Թվային ապրանքներ.</strong> Թվային ապրանքների առաքումն իրականացվում է անհապաղ (մի քանի րոպեի ընթացքում) վճարման հաստատմանից հետո։ Առանձին դեպքերում ժամկետը կարող է կազմել մինչև 24 ժամ։</p> <p>3.1. <strong>Թվային ապրանքներ.</strong> Թվային ապրանքների առաքումն իրականացվում է անհապաղ (մի քանի րոպեի ընթացքում) վճարման հաստատումից հետո։ Առանձին դեպքերում ժամկետը կարող է հասնել մինչև 24 ժամի։</p>
<p>3.2. <strong>Նյութական ապրանքներ.</strong> Նյութական ապրանքների առաքման մոտավոր ժամկետները Ռուսաստանի Դաշնության տարածքում կազմում են 1 (մեկ) մինչև 14 (տասնչորս) աշխատանքային օր՝ առաքմանը փոխադրողին հանձնելու պահից։</p> <p>3.2. <strong>Նյութական ապրանքներ.</strong> Նյութական ապրանքների առաքման մոտավոր ժամկետը Ռուսաստանի Դաշնության տարածքում կազմում է 1-ից 14 աշխատանքային օր` ապրանքը փոխադրողին հանձնելու պահից։</p>
<p>3.3. Նշված ժամկետները մոտավոր են և կարող են փոխվել՝ կախված առաքման տարածաշրից, փոխադրողի աշխատանքից, եղանակի պայմաններից, հանգստյան և տոնական օրերից։</p> <p>3.3. Նշված ժամկետները մոտավոր են և կարող են փոխվել` կախված առաքման տարածաշրջանից, փոխադրողի աշխատանքից, եղանակային պայմաններից, հանգստյան և տոնական օրերից։</p>
<p>3.4. Նյութական ապրանքների առաքման ճշգրիտ ժամկետը հաշվարկվում է ավտոմատ կերպով Պատվերի ձևակերպման ժամանակ՝ ընտրված փոխադրողի տվյալների հիման վրա և հաղորդվում է Գնորդին։</p> <p>3.4. Նյութական ապրանքների առաքման ճշգրիտ ժամկետը հաշվարկվում է ավտոմատ կերպով պատվերի ձևակերպման ընթացքում` ընտրված փոխադրողի տվյալների հիման վրա և հաղորդվում է Գնորդին։</p>
<p>3.5. Վաճառողը պատասխանատվություն չի կրում առաքման ժամկետների խախտման համար՝ պատճառված փոխադրողի գործողությամբ կամ անգործությամբ, անհաղթահարելի ուժի հանգամանքներով կամ Վաճառողից անկախ այլ պատճառներով։</p> <p>3.5. Վաճառողը պատասխանատվություն չի կրում առաքման ժամկետների խախտման համար, եթե դա պայմանավորված է փոխադրողի գործողություններով կամ անգործությամբ, ֆորս-մաժորային հանգամանքներով կամ Վաճառողից անկախ այլ պատճառներով։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>4. Ապրանքի հանձնման և ընդունման պայմանները</h2> <h2>4. Ապրանքի հանձնման և ընդունման պայմանները</h2>
<p>4.1. Ապրանքը հանձնվում է Գնորդին կամ նրա նշանակած անձին՝ փոխադրողի կողմից՝ ընտրված առաքման եղանակի պայմաններին համապատասխան։</p> <p>4.1. Ապրանքը փոխադրողի կողմից հանձնվում է Գնորդին կամ նրա կողմից նշանակված անձին` ընտրված առաքման եղանակի պայմաններին համապատասխան։</p>
<p>4.2. Ապրանքի պատահական կորստի կամ վնասվելու ռիսկը անցնում է Գնորդին՝ Ապրանքի փաստացի հանձնման պահից։</p> <p>4.2. Ապրանքի պատահական կորստի կամ վնասման ռիսկը անցնում է Գնորդին` ապրանքի փաստացի հանձնման պահից։</p>
<p>4.3. Ապրանքը ստանալիս Գնորդը պարտավոր է՝</p> <p>4.3. Ապրանքը ստանալիս Գնորդը պարտավոր է`</p>
<ul> <ul>
<li>ստուգել փաթեթավորման ամբողինությունը և Ապրանքի անվանման ու քանակի համապատասխանությունը ուղեկցող փաստաթղթերին։</li> <li>ստուգել փաթեթավորման ամբողջականությունը և ապրանքի անվանման ու քանակի համապատասխանությունը ուղեկցող փաստաթղթերին,</li>
<li>փաթեթավորման վնասվելու, պակասի կամ ապրանքի անհամապատասխանության դեպքում՝ կազմել ակտ փոխադրողի ներկայացուցչի ներկայությամբ։</li> <li>փաթեթավորման վնասման, պակասի կամ ապրանքի անհամապատասխանության դեպքում` կազմել ակտ փոխադրողի ներկայացուցչի ներկայությամբ,</li>
<li>պրետենզիաների բացակայության դեպքում՝ ստորագրել փոխադրողի փաստաթղթերում՝ ծանոթացնելով, որ Ապրանքը ստացվել է պատշած վիժակում։</li> <li>եթե պահանջներ չկան` ստորագրել փոխադրողի փաստաթղթերը` հաստատելով, որ ապրանքը ստացվել է պատշաճ վիճակում։</li>
</ul> </ul>
<p>4.4. Ապրանքի առաքումը իրականացվում է Վաճառողի կողմից Պատվերի հաստատմանից և վճարման ստացմանից (լիարժեք կամ մասնակի) հետո՝ կոնկրետ Պատվերի պայմաններին համապատասխան։</p> <p>4.4. Ապրանքի առաքումը կատարվում է Վաճառողի կողմից պատվերի հաստատումից և վճարման ստացումից (ամբողջությամբ կամ մասնակի) հետո` տվյալ պատվերի պայմաններին համապատասխան։</p>
<p>4.5. Վաճառողը պատասխանատվություն չի կրում Ապրանքի կորստի, վնասվելու կամ ուշացման համար փոխադրման ընթացքում՝ եթե դրանք պատճառված են փոխադրողի գործողությամբ կամ անգործությամբ։ Այդ դեպքերում Գնորդը իրավունք ունի պրետենզիա ներկայացնել անմիիապես փոխադրողին՝ օրենքով սահմանված կարգով, իսկ Վաճառողը պարտավորվում է աջակցել վեճի կարգավորմանը ողի սահմաններում։</p> <p>4.5. Վաճառողը պատասխանատվություն չի կրում ապրանքի կորստի, վնասման կամ ուշացման համար փոխադրման ընթացքում, եթե դրանք պատճառվել են փոխադրողի գործողությամբ կամ անգործությամբ։ Այդ դեպքերում Գնորդն իրավունք ունի օրենքով սահմանված կարգով պահանջ ներկայացնել անմիջապես փոխադրողին, իսկ Վաճառողը պարտավորվում է ողջամիտ սահմաններում աջակցել վեճի կարգավորմանը։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>5. Կապի տեղեկատվություն</h2> <h2>5. Կոնտակտային տվյալներ</h2>
<p>Առաքման հարցերով դիմեք՝</p> <p>Առաքման հետ կապված հարցերով կարող եք դիմել`</p>
<ul> <ul>
<li><strong>Էլ. փոստ՝</strong> <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></li> <li><strong>Էլ. փոստ՝</strong> <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a></li>
<li><strong>Հեռախոս (Ռուսաստան)՝</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a></li> <li><strong>Հեռախոս (Ռուսաստան)՝</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a></li>

View File

@@ -1,3 +1,5 @@
<div class="legal-page">
<div class="legal-container">
<h1>Frequently Asked Questions (FAQ) 📌</h1> <h1>Frequently Asked Questions (FAQ) 📌</h1>
<section class="legal-section"> <section class="legal-section">
@@ -238,3 +240,5 @@
<h2>Need Help?</h2> <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> <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> </section>
</div>
</div>

View File

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

View File

@@ -1,3 +1,5 @@
<div class="legal-page">
<div class="legal-container">
<h1>Часто задаваемые вопросы (FAQ) 📌</h1> <h1>Часто задаваемые вопросы (FAQ) 📌</h1>
<section class="legal-section"> <section class="legal-section">
@@ -238,3 +240,5 @@
<h2>Нужна помощь?</h2> <h2>Нужна помощь?</h2>
<p>Если возникнут дополнительные вопросы, обращайтесь на <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a> — мы оперативно решим любые ваши вопросы!</p> <p>Если возникнут дополнительные вопросы, обращайтесь на <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a> — мы оперативно решим любые ваши вопросы!</p>
</section> </section>
</div>
</div>

View File

@@ -1,158 +1,158 @@
<div class="legal-page"> <div class="legal-page">
<div class="legal-container"> <div class="legal-container">
<h1>Երաdelays 🔨</h1> <h1>Երաշխիք 🔨</h1>
<section class="legal-section"> <section class="legal-section">
<h2>1. Երdelays հիմնական դրdelays</h2> <h2>1. Երաշխիքի հիմնական դրույթներ</h2>
<p>Սuis բadger սahmanumé DexarMarket մarketplace-ում գnumva apa&shy;ranq&shy;ne&shy;ri eradiqayin caragayutyanneridek kargy.</p> <p>Սույն բաժինը սահմանում է DexarMarket մարկետփլեյսում ձեռք բերված ապրանքների երաշխիքային սպասարկման կարգը։</p>
<ul> <ul>
<li>Eradiqayin partavorutyunnery katarumé apranqi Vacharoghn é, Rusastani orensdrutyun hamadzayn.</li> <li>Երաշխիքային պարտավորությունները կատարում է հենց ապրանքի Վաճառողը` Ռուսաստանի Դաշնության օրենսդրությանը խիստ համապատասխան։</li>
<li>DexarMarket hartakumé gortsumé miay orpes teghekatvakan mijnourd yev chi masnaktsumé eradiqayin paymanneri katarman.</li> <li>DexarMarket հարթակը հանդես է գալիս միայն որպես տեղեկատվական միջնորդ և չի մասնակցում երաշխիքային պայմանների կատարմանը։</li>
<li>Eradiqayin baxverum é bazaraparén gortsaranayingortsaragnerov arajavordvats terutyyunnerin, vorvoné gnoghimatsu é.</li> <li>Երաշխիքը տարածվում է բացառապես գործարանային թերությունների և այնպիսի անսարքությունների վրա, որոնք առաջացել չեն գնորդի մեղքով։</li>
</ul> </ul>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>2. Երашխdelays jamkety 🏷</h2> <h2>2. Երաշխիքային ժամկետ 🏷</h2>
<p>Eradiqayin jamkety sahmanumy Vacharoghny kam artadroghny yev hratarakumé:</p> <p>Երաշխիքային ժամկետը սահմանվում է Վաճառողի կամ ապրանքի արտադրողի կողմից և հրապարակվում է.</p>
<ul> <ul>
<li>Mer kayqum apranqneri ejererum.</li> <li>Մեր կայքի ապրանքների էջերում։</li>
<li>Pakovkayin nerdy eradiqayin taloni mej.</li> <li>Փաթեթավորման մեջ ներառված երաշխիքային կտրոնում։</li>
<li>Apranqi uugheknogakan phastatgterum.</li> <li>Ապրանքին կցվող փաստաթղթերում։</li>
</ul> </ul>
<p><strong>Apranqneri kategorianeri tipekan eradiqayin jamketneré:</strong></p> <p><strong>Երաշխիքային ժամկետների բնորոշ տևողությունը ըստ ապրանքային կատեգորիաների.</strong></p>
<ul> <ul>
<li><strong>Elektronika yev kentzaghayan tekhnika.</strong> 12-itz 24 amis.</li> <li><strong>Էլեկտրոնիկա և կենցաղային տեխնիկա.</strong> 12-ից 24 ամիս։</li>
<li><strong>Hamakargchayin tekhnika yev bakhichner.</strong> 12-itz 36 amis.</li> <li><strong>Համակարգչային տեխնիկա և բաղադրիչներ.</strong> 12-ից 36 ամիս։</li>
<li><strong>Hagust yev koshik.</strong> 30 or-itz 6 amis (kakhvats é seasonaynutyunitz).</li> <li><strong>Հագուստ և կոշիկ.</strong> 30 օրից մինչև 6 ամիս` կախված սեզոնայնությունից։</li>
<li><strong>Kahuyq.</strong> 12-itz 18 amis.</li> <li><strong>Կահույք.</strong> 12-ից 18 ամիս։</li>
<li><strong>Tvayin artadrutyun.</strong> Ajakatsutyuny voroshum é Vacharoghny.</li> <li><strong>Թվային արտադրանք.</strong> սպասարկման պայմանները սահմանում է Վաճառողը։</li>
</ul> </ul>
<p>Eradiqayin jamkety sksbumé apranqy gnordi handzelou pahitz.</p> <p>Երաշխիքային ժամկետը հաշվարկվում է ապրանքը գնորդին հանձնելու պահից։</p>
<p>Apranqi poxarinumy noratsutsumé eradiqayin jamkety poxarinmana tarmana pahitz.</p> <p>Ապրանքի փոխարինման դեպքում երաշխիքային ժամկետը սկսվում է նորից` փոխարինման օրվանից։</p>
<p>Yete eradiqayin jamkety nshanvats che Vacharoghkoghmitz, gnoghé iravunq uné nerkayayatsnel pahanjner 2 tari yntatsqum apranqi dzerkberumitz (RF «Sparoghnerineri iravunqneri pashtpanutyun» orenqi 19 hodvatsi hamalyatzq).</p> <p>Եթե երաշխիքային ժամկետը Վաճառողի կողմից նշված չէ, գնորդն իրավունք ունի պահանջ ներկայացնել ապրանքը ձեռք բերելու օրվանից 2 տարվա ընթացքում` համաձայն ՌԴ «Սպառողների իրավունքների պաշտպանության մասին» օրենքի 19-րդ հոդվածի։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>3. Երashkhdelays paymanneré 📝</h2> <h2>3. Երաշխիքի տրամադրման պայմաններ 📝</h2>
<p>Eradiqayin gortsumé hetevyal pahanjneri katarman depqum:</p> <p>Երաշխիքը գործում է հետևյալ պահանջների պահպանման դեպքում.</p>
<ul> <ul>
<li>Apranqi ogtagortsumy khist hamalyatzq tsutsumy.</li> <li>Ապրանքը օգտագործվել է խստորեն ըստ հրահանգի։</li>
<li>Inqnuruyn qandumé, norogy kam pokhakerpumé chi eghel.</li> <li>Չի կատարվել սարքի ինքնուրույն ապամոնտաժում, վերանորոգում կամ ձևափոխում։</li>
<li>Artaqin irany yev nerqin tarrerri mekhanikan vnasvatsqner chkan.</li> <li>Բացակայում են արտաքին կորպուսի և ներքին տարրերի մեխանիկական վնասվածքները։</li>
<li>Bntakan plombnery yev sertiakany hamarneré pahhapanvats en (yete nakhatesvats en).</li> <li>Պահպանված են գործարանային պլոմբները և սերիական համարները (եթե դրանք նախատեսված են)։</li>
<li>Sarqy chi entarkvel bardzr jermasdijani, khonutyuny kam qimiakan niwterov.</li> <li>Սարքը չի ենթարկվել բարձր ջերմաստիճանի, խոնավության կամ քիմիական նյութերի ազդեցության։</li>
<li>Eradiqayin talony yev gnman hastavatory (statsakan, hashiv) kan.</li> <li>Առկա են երաշխիքային կտրոնը և գնման փաստը հաստատող փաստաթուղթը (կտրոն, անդորրագիր)։</li>
</ul> </ul>
<p><strong>Eradiqayin dimumy anverhjar phastatgteré:</strong></p> <p><strong>Երաշխիքային դիմում ներկայացնելու համար անհրաժեշտ փաստաթղթեր.</strong></p>
<ul> <ul>
<li>Apranqn ir amboghakan komplektatsyayov.</li> <li>Ապրանքը` ամբողջական կոմպլեկտացիայով։</li>
<li>Eradiqayin talony (yete nerkayayatsvié).</li> <li>Երաշխիքային կտրոնը (եթե առկա է)։</li>
<li>Gnman hasadatory phastatgter (statzakan, drantayin order).</li> <li>Գնումը հաստատող փաստաթուղթ (կտրոն, դրամարկղային օրդեր)։</li>
<li>Apranqi tirapanji inqnuruthy havastichny phastatgter (orinaky antsnagiry).</li> <li>Ապրանքի սեփականատիրոջ ինքնությունը հաստատող փաստաթուղթ (օրինակ` անձնագիր)։</li>
</ul> </ul>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>4. Երashkhdelays nórogy yev poxarinumy 🛠</h2> <h2>4. Երաշխիքային վերանորոգում և փոխարինում 🛠</h2>
<h3>Gnordi iravunqneré terutyun haytnaberelou depqum:</h3> <h3>Գնորդի իրավունքները թերության հայտնաբերման դեպքում.</h3>
<p>Yete terutyun haytnaberelié eradiqayin jamketum, duq iravunq unéq:</p> <p>Եթե թերությունը հայտնաբերվել է երաշխիքային ժամկետի ընթացքում, դուք իրավունք ունեք.</p>
<ul> <ul>
<li>Andzradzchar veratsnogel ansparkutyuny.</li> <li>Անվճար վերացնել անսարքությունը։</li>
<li>Stanalou nvazé apranq poxarinvoghiy.</li> <li>Ստանալ անսարք ապրանքին համարժեք փոխարինող ապրանք։</li>
<li>Pahanjel poxarinumy ayl apranqov arzhéqi verahashvarqov.</li> <li>Պահանջել փոխարինում այլ ապրանքով` արժեքի համապատասխան վերահաշվարկով։</li>
<li>Apranqi giny nshanatsel hamalyatsqy terutyuny.</li> <li>Պահանջել ապրանքի գնի նվազեցում` թերությանը համամասնորեն։</li>
<li>Veratartznel amboghakan gumaré apranqi hamar.</li> <li>Ստանալ ապրանքի համար վճարված ամբողջ գումարի վերադարձ։</li>
</ul> </ul>
<h3>Norogi jamketneré:</h3> <h3>Վերանորոգման ժամկետներ.</h3>
<p>Norogy katarvumé arag, bayts aravelin jamketé kazmumé 45 or (RF «Sparoghnerineri iravunqneri pashtpanutyun» orenqi 20 hodvats). Yete jamketé khakhtvel é, karogh eq poxarinumy pahanjel kam gumaré veratartznel.</p> <p>Վերանորոգումն իրականացվում է հնարավորինս արագ, սակայն առավելագույն ժամկետը 45 օր է` համաձայն ՌԴ «Սպառողների իրավունքների պաշտպանության մասին» օրենքի 20-րդ հոդվածի։ Եթե այդ ժամկետը խախտվում է, դուք կարող եք պահանջել ապրանքի փոխարինում կամ գումարի վերադարձ։</p>
<h3>Zhamanakavory apranqi poxarinumy:</h3> <h3>Ապրանքի ժամանակավոր փոխարինում.</h3>
<p>Yete norogman jamketé gerazantsum é mek shabatitz, vacharoghny partavory é trakan poxarinoghy tekhnikanatspes barbd apranqneri hamar.</p> <p>Եթե վերանորոգման ժամկետը գերազանցում է մեկ շաբաթը, վաճառողը պարտավոր է տեխնիկապես բարդ ապրանքների համար տրամադրել ժամանակավոր փոխարինող։</p>
<h3>Norogi hamar arakabumy:</h3> <h3>Առաքում վերանորոգման համար.</h3>
<p>Apranqy serverayin kentrón yev het haskatsutsuné katarumé vacharoghy kam mesnagitakan serverayin kentrony.</p> <p>Ապրանքը սպասարկման կենտրոն տեղափոխելու և հետ վերադարձնելու ծախսերը կրում է վաճառողը կամ լիազորված սպասարկման կենտրոնը։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>5. Երashkhdelays chi gortsoghy depqeré 🔍</h2> <h2>5. Դեպքեր, որոնք չեն մտնում երաշխիքի տակ 🔍</h2>
<p>Eradiqayin chi gortsumé, yete haytnabervel en hetevyal hangamanqneré:</p> <p>Երաշխիքը չի գործում, եթե հայտնաբերվել են հետևյալ հանգամանքները.</p>
<ul> <ul>
<li>Mekhanikan vnasvatsq (harkvatserov, ynknelov, charterov, kertsvatsqnerov);</li> <li>Մեխանիկական վնասվածք (հարվածներ, ընկնել, ճաքեր, քերծվածքներ),</li>
<li>Ogtagortsman kanoanneri khakhtumy (skhaly miatsutsumy, tsanraberrnumy, ochstandard kirarukumy);</li> <li>Օգտագործման կանոնների խախտում (սխալ միացում, գերաբեռնվածություն, ոչ ստանդարտ կիրառություն),</li>
<li>Artaqin gortsónerov vnasvatsqner (heghuk, keghtotutyun, bardzr jermastijan, khonutyun);</li> <li>Արտաքին գործոններից առաջացած վնասվածքներ (հեղուկ, կեղտ, բարձր ջերմաստիճան, խոնավություն),</li>
<li>Inqnuruyn norogy (qandely, ardiakanatsutsuny, bakhichneri poxarinumy);</li> <li>Ինքնուրույն վերանորոգում (ապամոնտաժում, արդիականացում, բաղադրիչների փոխարինում),</li>
<li>Fors-mazhoryan hangamankner (hrdeh, hogheghats, goghnutyun, bunkakan aghety);</li> <li>Ֆորս-մաժորային հանգամանքներ (հրդեհ, հեղեղում, գողություն, բնական աղետներ),</li>
<li>Niwteri bntakan tserevekatsumy (guyni koruts, payliq, chnchik mashumy);</li> <li>Նյութերի բնական մաշվածություն (գույնի կորուստ, փայլի նվազում, չնչին մաշվածություն),</li>
<li>Gortsaranayiny plombneri anuravortsi khakhtumy kam seriakan hamarneri vochnchatsnum.</li> <li>Գործարանային պլոմբների վնասում կամ սերիական համարների ոչնչացում։</li>
</ul> </ul>
<p><strong>Nanapes eradiqayin depqer chen hamarvumé:</strong></p> <p><strong>Երաշխիքային դեպք չեն համարվում նաև.</strong></p>
<ul> <ul>
<li>Kosmetik terutyunner, voronq chi azdumé ashkhatanqin (makaretayin kertsvatsqner, phokr btsher);</li> <li>Կոսմետիկ թերությունները, որոնք չեն ազդում աշխատանքի վրա (մակերեսային քերծվածքներ, փոքր բծեր),</li>
<li>Artaqin teqi pokhutyanneré soveran ogtagortsman hetevanqov;</li> <li>Արտաքին տեսքի փոփոխությունները սովորական օգտագործման հետևանքով,</li>
<li>Tsragvayin khndirner, voronq arrajatsumé eghel yets ardag tsragvayin apahovelov;</li> <li>Ծրագրային խնդիրները, որոնք առաջացել են երրորդ կողմի ծրագրային ապահովման տեղադրման հետևանքով,</li>
<li>Hamateghakanutyuny khndirner ayl artadroghneri sarqerov kam targatshkhayin apahovelov.</li> <li>Այլ արտադրողների սարքերի կամ ծրագրերի հետ համատեղելիության խնդիրները։</li>
</ul> </ul>
<p>Eradiqayin sahmanapakumy tsakhsayin niwterov (marzanocner, lampichkaner, filtrner) arrandzhnahatuk nshanvats é apranqi nkaragrutyunum.</p> <p>Սպառվող նյութերի (մարտկոցներ, լամպեր, ֆիլտրեր) նկատմամբ երաշխիքային սահմանափակումները առանձին նշվում են ապրանքի նկարագրության մեջ։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>6. Երashkhdelays caragayutyanyits ogtvelu kargy 🗒</h2> <h2>6. Երաշխիքային սպասարկման դիմում ներկայացնելու կարգ 🗒</h2>
<p>Eradiqayin caragayutyunitz ogtvelou hamar katareq hetevyal qaylerë:</p> <p>Երաշխիքային սպասարկումից օգտվելու համար կատարեք հետևյալ քայլերը.</p>
<ol> <ol>
<li>Kapvéq vacharoghyin dzert patverumy nvazamya teghekatvakan tvalnerov.</li> <li>Կապ հաստատեք վաճառողի հետ` օգտագործելով ձեր պատվերում նշված կոնտակտային տվյալները։</li>
<li>Bajareq khndiry yev ktsveq lousankarner kam tesanyut (anhradzesht depqum).</li> <li>Նկարագրեք խնդիրը և անհրաժեշտության դեպքում կցեք լուսանկարներ կամ տեսանյութ։</li>
<li>Vacharoghitz stanatsëq tsutsumy serverayin kentrón dimelou kam apranqy ugharkelou hastsen.</li> <li>Ստացեք վաճառողից հրահանգներ` ինչպես դիմել սպասարկման կենտրոն կամ որ հասցեով ուղարկել ապրանքը։</li>
<li>Apranqy haratsreq serverin gnman hasdadoghy phastatgtoghy (eradiqayin talón, statzakan).</li> <li>Ապրանքը հանձնեք սպասարկման կենտրոն` գնման փաստը հաստատող փաստաթղթերով (երաշխիքային կտրոն, կտրոն)։</li>
<li>Stanatsëq apranqi yndunman akty nórogi jamketi nshanakov.</li> <li>Ստացեք ընդունման ակտ` վերանորոգման ժամկետի նշումով։</li>
<li>Veratserëq noroghvats apranqy nórogi avartman mahovy tzanuchatsumitz heto.</li> <li>Վերցրեք վերանորոգված ապրանքը` վերանորոգման ավարտի մասին ծանուցում ստանալուց հետո։</li>
</ol> </ol>
<h3>Apranqy poshtov kam surhandakátsov urargelu kanonerë:</h3> <h3>Ապրանքը փոստով կամ սուրհանդակով ուղարկելու կանոններ.</h3>
<ul> <ul>
<li>Hushteloren pakovéq sarqy, khandarelov hnavary teghapahdman yntatsqum.</li> <li>Սարքը հուսալի փաթեթավորեք` փոխադրման ընթացքում հնարավոր վնասները կանխելու համար։</li>
<li>Gnman phastatgteri patejennery yev khndri manramasn nkaragrutyuny teghadreq pakovkayi mej.</li> <li>Փաթեթի մեջ դրեք գնման փաստաթղթերի պատճենները և խնդրի մանրամասն նկարագրությունը։</li>
<li>Poshtayin arakuln dzevakeq gnahatman arzhéqov.</li> <li>Փոստային առաքումը ձևակերպեք հայտարարագրված արժեքով։</li>
<li>Anverphayanorén pahéq trek hamary berny hetsevelou hamar.</li> <li>Անպայման պահպանեք հետևման համարը` առաքման տեղաշարժը վերահսկելու համար։</li>
</ul> </ul>
<p>Yete dzhvaroutyunner arrajanan, gnoghneré nanapes karogh en dimél mer ajakatsutyuny caragayutyunh tsovazgin email-ov: <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a>:</p> <p>Եթե դժվարություններ առաջանան, գնորդները կարող են նաև դիմել մեր աջակցման ծառայությանը էլեկտրոնային հասցեով` <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a>։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>7. Gnordi havélyal iravunqneré 🎯</h2> <h2>7. Գնորդի լրացուցիչ իրավունքներ 🎯</h2>
<p>Yete apranqy uné lrjakan terutyun, duq iravunq unéq:</p> <p>Եթե ապրանքն ունի էական թերություն, դուք իրավունք ունեք.</p>
<ul> <ul>
<li>Pahanjel amboghakan gumaré veratartznel.</li> <li>Պահանջել ամբողջ գումարի վերադարձ։</li>
<li>Khndrél poxarinumy ayl modeli apranqov hamatéghakan arzhéqi verahashvarqov.</li> <li>Պահանջել փոխարինում այլ մոդելի ապրանքով` արժեքի համապատասխան վերահաշվարկով։</li>
</ul> </ul>
<p><strong>Lrjakan terutyun</strong> — dra depqy, yerb:</p> <p><strong>Էական թերություն</strong> է համարվում այն իրավիճակը, երբ.</p>
<ul> <ul>
<li>Hanchagy hnararvor ché véaratsnel.</li> <li>Անսարքությունը հնարավոր չէ վերացնել։</li>
<li>Hanchagy veratsneli hamar petq en mets tsakhser kam yerkar zhamanak.</li> <li>Անսարքության վերացումը պահանջում է մեծ ծախսեր կամ երկար ժամանակ։</li>
<li>Terutyuny noritz arrajanum é norogi heto.</li> <li>Թերությունը նորից է ի հայտ գալիս վերանորոգումից հետո։</li>
<li>Nwazé khndiry arrajanum é bazmakirats angam.</li> <li>Նույն խնդիրը բազմիցս կրկնվում է։</li>
</ul> </ul>
<p>Batsidranits, duq karogh eq hatahatél anvorak apranqi vacharqov arrajatsats vnasnery.</p> <p>Բացի այդ, դուք կարող եք պահանջել փոխհատուցել ոչ որակյալ ապրանքի վաճառքի հետևանքով առաջացած վնասները։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>8. Kapí teghekatvutyun 📞</h2> <h2>8. Կոնտակտային տվյալներ 📞</h2>
<p>Eradiqayin caragayutyuny veraberyal tsankatsats harts hamar nakh dimëq vacharoghyin (teseq apranqi ejerë kam dzert arakulin masin tzanuchatsumë).</p> <p>Երաշխիքային սպասարկման հետ կապված ցանկացած հարցով նախ դիմեք վաճառողին (տես ապրանքի էջը կամ առաքման մասին ձեր ծանուցումը)։</p>
<p><strong>Yete vech luselu anhradzeshtutyun arrajatsav:</strong></p> <p><strong>Եթե անհրաժեշտ է լուծել վեճը.</strong></p>
<p>Uraréq email Marketplace-in: <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a> temayov «Eradiqayin harts — Patver №[patver hamary:</p> <p>Ուղարկեք նամակ Մարկետփլեյսի էլեկտրոնային հասցեին` <a href="mailto:info@dexarmarket.ru">info&#64;dexarmarket.ru</a>` թեմայում նշելով. «Երաշխիքային հարց — Պատվեր №[պատվերի համար։</p>
<p>Yete vacharoghy hrajarkvumé yndunelou pahanjy, duq iravunq unéq nakhadzernél apranqi vorakin ankakh phortsagrutyun yev datakan haylts nerkayayatsnel.</p> <p>Եթե վաճառողը հրաժարվում է ընդունել պահանջը, դուք իրավունք ունեք նախաձեռնել ապրանքի որակի անկախ փորձաքննություն և հայց ներկայացնել դատարան։</p>
</section> </section>
</div> </div>
</div> </div>

View File

@@ -55,7 +55,23 @@
</div> </div>
<div class="novo-info"> <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"> <div class="novo-rating">
<span class="stars">{{ getRatingStars(item()!.rating) }}</span> <span class="stars">{{ getRatingStars(item()!.rating) }}</span>
@@ -66,23 +82,68 @@
<div class="novo-price-block"> <div class="novo-price-block">
@if (item()!.discount > 0) { @if (item()!.discount > 0) {
<div class="price-row"> <div class="price-row">
<span class="old-price">{{ item()!.price }} {{ item()!.currency }}</span> <span class="old-price">{{ effectivePrice() }} {{ effectiveCurrency() }}</span>
<span class="discount-badge">-{{ item()!.discount }}%</span> <span class="discount-badge">-{{ item()!.discount }}%</span>
</div> </div>
<div class="current-price">{{ getDiscountedPrice() | number:'1.2-2' }} {{ item()!.currency }}</div> <div class="current-price">{{ getDiscountedPrice() | number:'1.2-2' }} {{ effectiveCurrency() }}</div>
} @else { } @else {
<div class="current-price">{{ item()!.price }} {{ item()!.currency }}</div> <div class="current-price">{{ effectivePrice() }} {{ effectiveCurrency() }}</div>
} }
</div> </div>
<div class="novo-stock"> <div class="novo-stock">
<span class="stock-label">{{ 'itemDetail.stock' | translate }}</span> <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> <span class="dot"></span>
{{ item()!.remainings === 'high' ? ('itemDetail.inStock' | translate) : item()!.remainings === 'medium' ? ('itemDetail.mediumStock' | translate) : ('itemDetail.lowStock' | translate) }} {{ getStockLabel() }}
</div> </div>
@if (effectiveRemaining() != null) {
<span class="stock-qty">({{ effectiveRemaining() }} шт.)</span>
}
</div> </div>
@if (availableColours().length || availableSizes().length || item()!.colour || (item()!.size && item()!.size!.toLowerCase() !== 'default')) {
<div class="novo-variants">
@if (availableColours().length) {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.colour' | translate }}:</span>
@for (c of availableColours(); track c) {
<span class="colour-swatch" [class.active]="selectedColour() === c" [style.background-color]="c" [title]="c" (click)="selectColour(c)"></span>
}
</div>
} @else if (item()!.colour) {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.colour' | translate }}:</span>
<span class="colour-swatch active" [style.background-color]="item()!.colour" [title]="item()!.colour"></span>
</div>
}
@if (availableSizes().length) {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.size' | translate }}:</span>
@for (s of availableSizes(); track s) {
<span class="variant-chip size-chip" [class.active]="selectedSize() === s" (click)="selectSize(s)">{{ s }}</span>
}
</div>
} @else if (item()!.size && item()!.size!.toLowerCase() !== 'default') {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.size' | translate }}:</span>
<span class="variant-chip size-chip active">{{ 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()"> <button class="novo-add-cart" (click)="addToCart()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <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> <circle cx="9" cy="21" r="1"></circle>
@@ -93,8 +154,26 @@
</button> </button>
<div class="novo-description"> <div class="novo-description">
@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> <h3>{{ 'itemDetail.description' | translate }}</h3>
<div [innerHTML]="getSafeHtml(item()!.description)"></div> <div [innerHTML]="getSafeHtml(item()!.description)"></div>
}
</div> </div>
</div> </div>
</div> </div>
@@ -249,7 +328,23 @@
<!-- Item Info --> <!-- Item Info -->
<div class="dx-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-rating">
<div class="dx-stars"> <div class="dx-stars">
@@ -266,26 +361,68 @@
<div class="dx-price-block"> <div class="dx-price-block">
@if (item()!.discount > 0) { @if (item()!.discount > 0) {
<div class="dx-price-row"> <div class="dx-price-row">
<span class="dx-old-price">{{ item()!.price }} {{ item()!.currency }}</span> <span class="dx-old-price">{{ effectivePrice() }} {{ effectiveCurrency() }}</span>
<span class="dx-discount-tag">-{{ item()!.discount }}%</span> <span class="dx-discount-tag">-{{ item()!.discount }}%</span>
</div> </div>
} }
<div class="dx-current-price"> <div class="dx-current-price">
{{ item()!.discount > 0 ? (getDiscountedPrice() | number:'1.2-2') : item()!.price }} {{ item()!.currency }} {{ item()!.discount > 0 ? (getDiscountedPrice() | number:'1.2-2') : effectivePrice() }} {{ effectiveCurrency() }}
</div> </div>
</div> </div>
<div class="dx-stock"> <div class="dx-stock">
<span class="dx-stock-label">{{ 'itemDetail.stock' | translate }}</span> <span class="dx-stock-label">{{ 'itemDetail.stock' | translate }}</span>
<span class="dx-stock-status" <span class="dx-stock-status" [class]="getStockClass()">
[class.high]="item()!.remainings === 'high'"
[class.medium]="item()!.remainings === 'medium'"
[class.low]="item()!.remainings === 'low'">
<span class="dx-stock-dot"></span> <span class="dx-stock-dot"></span>
{{ item()!.remainings === 'high' ? ('itemDetail.inStock' | translate) : item()!.remainings === 'medium' ? ('itemDetail.mediumStock' | translate) : ('itemDetail.lastItems' | translate) }} {{ getStockLabel() }}
</span> </span>
@if (effectiveRemaining() != null) {
<span class="dx-stock-qty">({{ effectiveRemaining() }} шт.)</span>
}
</div> </div>
@if (availableColours().length || availableSizes().length || item()!.colour || (item()!.size && item()!.size!.toLowerCase() !== 'default')) {
<div class="dx-variants">
@if (availableColours().length) {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.colour' | translate }}:</span>
@for (c of availableColours(); track c) {
<span class="colour-swatch" [class.active]="selectedColour() === c" [style.background-color]="c" [title]="c" (click)="selectColour(c)"></span>
}
</div>
} @else if (item()!.colour) {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.colour' | translate }}:</span>
<span class="colour-swatch active" [style.background-color]="item()!.colour" [title]="item()!.colour"></span>
</div>
}
@if (availableSizes().length) {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.size' | translate }}:</span>
@for (s of availableSizes(); track s) {
<span class="variant-chip size-chip" [class.active]="selectedSize() === s" (click)="selectSize(s)">{{ s }}</span>
}
</div>
} @else if (item()!.size && item()!.size!.toLowerCase() !== 'default') {
<div class="variant-group">
<span class="variant-label">{{ 'itemDetail.size' | translate }}:</span>
<span class="variant-chip size-chip active">{{ 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()"> <button class="dx-add-cart" (click)="addToCart()">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <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> <circle cx="9" cy="21" r="1"></circle>
@@ -296,8 +433,27 @@
</button> </button>
<div class="dx-description"> <div class="dx-description">
<!-- @if (getSimpleDescription()) { -->
@if (false) {
<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> <h2>{{ 'itemDetail.description' | translate }}</h2>
<div class="dx-description-text" [innerHTML]="getSafeHtml(item()!.description)"></div> <div class="dx-description-text" [innerHTML]="getSafeHtml(item()!.description)"></div>
}
</div> </div>
</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-font: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
$dx-dark: #1e3c38; $dx-dark: #1e3c38;
$dx-primary: #497671; $dx-primary: #497671;
@@ -50,7 +52,7 @@ $dx-card-bg: #f5f3f9;
transition: all 0.2s; transition: all 0.2s;
&:hover { &:hover {
background: darken($dx-primary, 8%); background: color.adjust($dx-primary, $lightness: -8%);
transform: translateY(-1px); transform: translateY(-1px);
} }
} }
@@ -281,7 +283,7 @@ $dx-card-bg: #f5f3f9;
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15); box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
&:hover { &:hover {
background: darken($dx-primary, 8%); background: color.adjust($dx-primary, $lightness: -8%);
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(73, 118, 113, 0.3); box-shadow: 0 6px 16px rgba(73, 118, 113, 0.3);
} }
@@ -291,6 +293,96 @@ $dx-card-bg: #f5f3f9;
} }
} }
// Variant chips (colour/size) — shared between dexar and novo
.dx-variants, .novo-variants {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 12px;
.variant-group {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.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;
cursor: pointer;
transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
&:hover {
background: rgba(73, 118, 113, 0.12);
}
&.active {
border-color: $dx-primary;
background: rgba(73, 118, 113, 0.18);
box-shadow: 0 0 0 2px rgba(73, 118, 113, 0.25);
}
}
.colour-swatch {
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid $dx-border;
cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s;
flex-shrink: 0;
&:hover {
border-color: $dx-primary;
}
&.active {
border-color: $dx-primary;
box-shadow: 0 0 0 2px rgba(73, 118, 113, 0.25);
}
}
}
.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 { .dx-description {
padding-top: 8px; padding-top: 8px;
border-top: 1px solid $dx-border; border-top: 1px solid $dx-border;
@@ -434,7 +526,7 @@ $dx-card-bg: #f5f3f9;
justify-content: center; justify-content: center;
&:hover:not(:disabled) { &:hover:not(:disabled) {
background: darken($dx-primary, 8%); background: color.adjust($dx-primary, $lightness: -8%);
transform: translateY(-1px); transform: translateY(-1px);
} }
@@ -642,22 +734,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) { @media (max-width: 992px) {
.dx-item-content { .dx-item-content {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 32px; 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) { @media (max-width: 768px) {
.dx-item-container { .dx-item-container {
padding: 16px; padding: 16px;
} }
// On mobile: thumbnails go below main photo .dx-item-content {
gap: 24px;
margin-bottom: 32px;
}
// Thumbnails go below main photo
.dx-gallery { .dx-gallery {
flex-direction: column; flex-direction: column;
max-width: 100%;
} }
.dx-thumbnails { .dx-thumbnails {
@@ -666,14 +806,16 @@ $dx-card-bg: #f5f3f9;
max-height: none; max-height: none;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
order: 1; // put below main photo order: 1;
scrollbar-width: none; scrollbar-width: none;
&::-webkit-scrollbar { display: none; } &::-webkit-scrollbar { display: none; }
} }
.dx-main-photo { .dx-main-photo {
order: 0; // main photo first order: 0;
max-height: 400px;
aspect-ratio: auto;
} }
.dx-thumb { .dx-thumb {
@@ -690,8 +832,32 @@ $dx-card-bg: #f5f3f9;
font-size: 1.8rem; font-size: 1.8rem;
} }
.dx-old-price {
font-size: 1rem;
}
.dx-add-cart { .dx-add-cart {
max-width: 100%; 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 { .dx-review-form {
@@ -707,21 +873,153 @@ $dx-card-bg: #f5f3f9;
width: 100%; 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) { @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 { .dx-thumb {
width: 56px; width: 52px;
height: 56px; height: 52px;
min-width: 56px; min-width: 52px;
} }
.dx-title { .dx-title {
font-size: 1.25rem; font-size: 1.2rem;
}
.dx-info {
gap: 16px;
} }
.dx-current-price { .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 +1597,20 @@ $dx-card-bg: #f5f3f9;
} }
} }
// ========== NOVO RESPONSIVE ==========
// Tablet portrait
@media (max-width: 968px) { @media (max-width: 968px) {
.novo-item-content { .novo-item-content {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 2rem; gap: 2rem;
} }
.novo-gallery {
max-width: 600px;
margin: 0 auto;
}
.novo-info .novo-title { .novo-info .novo-title {
font-size: 1.5rem; font-size: 1.5rem;
} }
@@ -1313,6 +1619,10 @@ $dx-card-bg: #f5f3f9;
font-size: 2rem; font-size: 2rem;
} }
.novo-info .novo-add-cart {
max-width: 100%;
}
.novo-review-form { .novo-review-form {
padding: 1.5rem; padding: 1.5rem;
@@ -1327,3 +1637,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

@@ -1,14 +1,15 @@
import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy, inject } from '@angular/core'; import { Component, OnInit, OnDestroy, signal, computed, ChangeDetectionStrategy, inject } from '@angular/core';
import { DecimalPipe } from '@angular/common'; import { DecimalPipe } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ActivatedRoute, RouterLink } from '@angular/router'; import { ActivatedRoute, RouterLink } from '@angular/router';
import { ApiService, CartService, TelegramService, SeoService } from '../../services'; import { ApiService, CartService, TelegramService, LanguageService, SeoService } from '../../services';
import { Item } from '../../models'; import { AuthService } from '../../services/auth.service';
import { Item, ItemDetail, DescriptionField } from '../../models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { SecurityContext } from '@angular/core'; 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 { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe'; import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service'; import { TranslateService } from '../../i18n/translate.service';
@@ -27,6 +28,55 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
error = signal<string | null>(null); error = signal<string | null>(null);
isnovo = environment.theme === 'novo'; isnovo = environment.theme === 'novo';
// Variant selection
selectedColour = signal<string | null>(null);
selectedSize = signal<string | null>(null);
availableColours = computed(() => {
const details = this.item()?.itemDetails;
if (!details?.length) return [] as string[];
const unique = [...new Set(details.map(d => d.colour || d.color).filter((c): c is string => !!c))];
return unique;
});
availableSizes = computed(() => {
const details = this.item()?.itemDetails;
if (!details?.length) return [] as string[];
// If a colour is selected, only show sizes available for that colour
const colour = this.selectedColour();
const filtered = colour
? details.filter(d => (d.colour || d.color) === colour)
: details;
const unique = [...new Set(filtered.map(d => d.size).filter((s): s is string => !!s && s.toLowerCase() !== 'default'))];
return unique;
});
selectedDetail = computed<ItemDetail | null>(() => {
const details = this.item()?.itemDetails;
if (!details?.length) return null;
const colour = this.selectedColour();
const size = this.selectedSize();
return details.find(d =>
(!colour || (d.colour || d.color) === colour) &&
(!size || d.size === size)
) ?? null;
});
effectivePrice = computed(() => {
const detail = this.selectedDetail();
return detail?.price ?? this.item()?.price ?? 0;
});
effectiveCurrency = computed(() => {
const detail = this.selectedDetail();
return detail?.currency ?? this.item()?.currency ?? '';
});
effectiveRemaining = computed(() => {
const detail = this.selectedDetail();
return detail?.remaining ?? this.item()?.quantity ?? null;
});
newReview = { newReview = {
rating: 0, rating: 0,
comment: '', comment: '',
@@ -42,13 +92,15 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
private seoService = inject(SeoService); private seoService = inject(SeoService);
private i18n = inject(TranslateService); private i18n = inject(TranslateService);
private authService = inject(AuthService);
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private apiService: ApiService, private apiService: ApiService,
private cartService: CartService, private cartService: CartService,
private telegramService: TelegramService, private telegramService: TelegramService,
private sanitizer: DomSanitizer private sanitizer: DomSanitizer,
private languageService: LanguageService
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@@ -72,6 +124,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
this.apiService.getItem(itemID).subscribe({ this.apiService.getItem(itemID).subscribe({
next: (item) => { next: (item) => {
this.item.set(item); this.item.set(item);
this.initVariantSelection(item);
this.seoService.setItemMeta(item); this.seoService.setItemMeta(item);
this.loading.set(false); this.loading.set(false);
}, },
@@ -83,6 +136,33 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
}); });
} }
private initVariantSelection(item: Item): void {
// Auto-select the first available colour and size from itemDetails
const details = item.itemDetails;
if (details?.length) {
const firstColour = details[0].colour || details[0].color || null;
this.selectedColour.set(firstColour);
const firstSize = details[0].size || null;
this.selectedSize.set(firstSize);
} else {
this.selectedColour.set(item.colour ?? null);
this.selectedSize.set(item.size ?? null);
}
}
selectColour(colour: string): void {
this.selectedColour.set(colour);
// If current size is not available for the new colour, reset to first available
const sizes = this.availableSizes();
if (sizes.length && this.selectedSize() && !sizes.includes(this.selectedSize()!)) {
this.selectedSize.set(sizes[0]);
}
}
selectSize(size: string): void {
this.selectedSize.set(size);
}
selectPhoto(index: number): void { selectPhoto(index: number): void {
this.selectedPhotoIndex.set(index); this.selectedPhotoIndex.set(index);
} }
@@ -90,16 +170,73 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
addToCart(): void { addToCart(): void {
const currentItem = this.item(); const currentItem = this.item();
if (currentItem) { if (currentItem) {
this.cartService.addItem(currentItem.itemID); this.cartService.addItem(currentItem.itemID, 1, {
colour: this.selectedColour() ?? undefined,
size: this.selectedSize() ?? undefined,
price: this.effectivePrice(),
currency: this.effectiveCurrency()
});
} }
} }
getDiscountedPrice(): number { getDiscountedPrice(): number {
const currentItem = this.item(); const currentItem = this.item();
if (!currentItem) return 0; if (!currentItem) return 0;
return getDiscountedPrice(currentItem); const price = this.effectivePrice();
const discount = currentItem.discount || 0;
return discount > 0 ? price * (1 - discount / 100) : price;
} }
// 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 '';
return currentItem.simpleDescription || currentItem.description || '';
}
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 { getSafeHtml(html: string): SafeHtml {
return this.sanitizer.sanitize(SecurityContext.HTML, html) || ''; return this.sanitizer.sanitize(SecurityContext.HTML, html) || '';
} }
@@ -155,8 +292,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
itemID: currentItem.itemID, itemID: currentItem.itemID,
rating: this.newReview.rating, rating: this.newReview.rating,
comment: this.newReview.comment.trim(), comment: this.newReview.comment.trim(),
username: this.newReview.anonymous ? null : this.getUserDisplayName(), sessionID: this.authService.session()?.sessionId || '',
userId: this.telegramService.getUserId(),
timestamp: new Date().toISOString() timestamp: new Date().toISOString()
}; };

View File

@@ -1,9 +1,15 @@
<div class="legal-page">
<div class="legal-container">
<h1>Company Details</h1> <h1>Company Details</h1>
<section class="legal-section"> <section class="legal-section">
<h2>Full Company Name</h2> <h2>Full Company Name</h2>
<p>LIMITED LIABILITY COMPANY «INT FIN LOGISTIC»</p> <p>LIMITED LIABILITY COMPANY «INT FIN LOGISTIC»</p>
<p><strong>Abbreviated name:</strong> LLC «INT FIN LOGISTIC»</p> <p><strong>Abbreviated name:</strong> LLC «INT FIN LOGISTIC»</p>
<br>
<p>LIMITED LIABILITY COMPANY «INT FACTORING»</p>
<p><strong>Abbreviated name:</strong> LLC «INT FACTORING»</p>
<p><strong>TIN:</strong> 9909697635</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
@@ -96,3 +102,5 @@
<p><strong>General Director:</strong> Hovhannisyan Ashot Rafikovich</p> <p><strong>General Director:</strong> Hovhannisyan Ashot Rafikovich</p>
<p><strong>Basis of authority:</strong> Charter</p> <p><strong>Basis of authority:</strong> Charter</p>
</section> </section>
</div>
</div>

View File

@@ -1,10 +1,14 @@
<div class="legal-page">
<div class="legal-container">
<h1>Կազմակերպության տվյալներ</h1> <h1>Կազմակերպության տվյալներ</h1>
<section class="legal-section"> <section class="legal-section">
<h2>Կազմակերպության լիարժեկ անվանումը</h2> <h2>Կազմակերպության լիարժեկ անվանումը</h2>
<p>ՍԱՀՄԱՆԱՓԱԿ ՊԱՏԱՍԽԱՆԱՏվությամբ Ընկերություն «ԻՆՏ ՖԻՆ ԼՈԳԻՍՏԻԿ»</p> <p>ՍԱՀՄԱՆԱՓԱԿ ՊԱՏԱՍԽԱՆԱՏվությամբ Ընկերություն «ԻՆՏ ՖԻՆ ԼՈԳԻՍՏԻԿ»</p>
<p><strong>Հապավոր անվանումը՝</strong> ՍՊԸ «ԻՆՏ ՖԻՆ ԼՈԳԻՍՏԻԿ»</p> <p><strong>Հապավոր անվանումը՝</strong> ՍՊԸ «ԻՆՏ ՖԻՆ ԼՈԳԻՍՏԻԿ»</p> <br>
</section> <p>ՍԱՀՄԱՆԱՓԱԿ ՊԱՏԱՍԽԱՆԱՏվությամբ Ընկերություն «ԻՆՏ ՖԱԿՏՈՌԻՆԳ»</p>
<p><strong>Հապավоր անվանումը՝</strong> ՍՊԸ «ԻՆՏ ՖԱԿՏՈՌԻՆԳ»</p>
<p><strong>ՀՍՀ՝</strong> 9909697635</p> </section>
<section class="legal-section"> <section class="legal-section">
<h2>Իրավաբանական հասցե</h2> <h2>Իրավաբանական հասցե</h2>
@@ -96,3 +100,5 @@
<p><strong>Գլխավոր տնօրեն՝</strong> Օհաննիսյան Աշոտ Ռաֆիկի</p> <p><strong>Գլխավոր տնօրեն՝</strong> Օհաննիսյան Աշոտ Ռաֆիկի</p>
<p><strong>Գործողության հիմք՝</strong> Կանոնադրություն</p> <p><strong>Գործողության հիմք՝</strong> Կանոնադրություն</p>
</section> </section>
</div>
</div>

View File

@@ -1,9 +1,15 @@
<div class="legal-page">
<div class="legal-container">
<h1>Реквизиты организации</h1> <h1>Реквизиты организации</h1>
<section class="legal-section"> <section class="legal-section">
<h2>Полное наименование организации</h2> <h2>Полное наименование организации</h2>
<p>ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ИНТ ФИН ЛОГИСТИК»</p> <p>ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ИНТ ФИН ЛОГИСТИК»</p>
<p><strong>Сокращенное наименование:</strong> ООО «ИНТ ФИН ЛОГИСТИК»</p> <p><strong>Сокращенное наименование:</strong> ООО «ИНТ ФИН ЛОГИСТИК»</p>
<br>
<p>ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ИНТ ФАКТОРИНГ»</p>
<p><strong>Сокращенное наименование:</strong> ООО «ИНТ ФАКТОРИНГ»</p>
<p><strong>ИНН:</strong> 9909697635</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
@@ -96,3 +102,5 @@
<p><strong>Генеральный директор:</strong> Оганнисян Ашот Рафикович</p> <p><strong>Генеральный директор:</strong> Оганнисян Ашот Рафикович</p>
<p><strong>Основание действий:</strong> Устав</p> <p><strong>Основание действий:</strong> Устав</p>
</section> </section>
</div>
</div>

View File

@@ -1,3 +1,5 @@
<div class="legal-page">
<div class="legal-container">
<h1>Payment Terms</h1> <h1>Payment Terms</h1>
<section class="legal-section"> <section class="legal-section">
@@ -111,3 +113,5 @@
</ul> </ul>
<p>When contacting us, please provide your order number and a brief description of the issue for a faster resolution.</p> <p>When contacting us, please provide your order number and a brief description of the issue for a faster resolution.</p>
</section> </section>
</div>
</div>

View File

@@ -1,3 +1,5 @@
<div class="legal-page">
<div class="legal-container">
<h1>Վճարման կանոններ</h1> <h1>Վճարման կանոններ</h1>
<section class="legal-section"> <section class="legal-section">
@@ -111,3 +113,5 @@
</ul> </ul>
<p>Դիմելիս նշեք պատվերի համարը և խնդրի հակիրճ նկարագրությունը՝ հարցի ավելի արագ լուծման համար։</p> <p>Դիմելիս նշեք պատվերի համարը և խնդրի հակիրճ նկարագրությունը՝ հարցի ավելի արագ լուծման համար։</p>
</section> </section>
</div>
</div>

View File

@@ -1,3 +1,5 @@
<div class="legal-page">
<div class="legal-container">
<h1>Правила оплаты</h1> <h1>Правила оплаты</h1>
<section class="legal-section"> <section class="legal-section">
@@ -111,3 +113,5 @@
</ul> </ul>
<p>При обращении указывайте номер заказа и краткое описание проблемы для более быстрого решения вопроса.</p> <p>При обращении указывайте номер заказа и краткое описание проблемы для более быстрого решения вопроса.</p>
</section> </section>
</div>
</div>

View File

@@ -1,9 +1,11 @@
<div class="legal-page">
<div class="legal-container">
<h1>PERSONAL DATA PROCESSING POLICY</h1> <h1>PERSONAL DATA PROCESSING POLICY</h1>
<section class="legal-section"> <section class="legal-section">
<h2>1. GENERAL PROVISIONS</h2> <h2>1. GENERAL PROVISIONS</h2>
<p>1.1. This policy of LLC "INT FIN LOGISTIC" (TIN 9909697628), hereinafter referred to as the "Operator", describes the procedure for processing personal data and is aimed at protecting the rights and legitimate interests of data subjects. The document has been developed in accordance with Federal Law No. 152-FZ of July 27, 2006 "On Personal Data".</p> <p>1.1. This policy of LLC "INT FIN LOGISTIC" (TIN 9909697628) and LLC "INT FACTORING" (TIN 9909697635), hereinafter referred to as the "Operator", describes the procedure for processing personal data and is aimed at protecting the rights and legitimate interests of data subjects. The document has been developed in accordance with Federal Law No. 152-FZ of July 27, 2006 "On Personal Data".</p>
<p>1.2. The Policy defines the procedure and measures to ensure the security of personal data processing on the website <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>, with the goal of protecting human and civil rights and freedoms, including the right to privacy, personal and family secrets.</p> <p>1.2. The Policy defines the procedure and measures to ensure the security of personal data processing on the website <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>, with the goal of protecting human and civil rights and freedoms, including the right to privacy, personal and family secrets.</p>
@@ -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> <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> </section>
</div>
</div>

View File

@@ -1,363 +1,367 @@
<h1>PERSONAL DATA PROCESSING POLICY</h1> <div class="legal-page">
<div class="legal-container">
<h1>ԱՆՁՆԱԿԱՆ ՏՎՅԱԼՆԵՐԻ ՄՇԱԿՄԱՆ ՔԱՂԱՔԱԿԱՆՈՒԹՅՈՒՆ</h1>
<section class="legal-section"> <section class="legal-section">
<h2>1. GENERAL PROVISIONS</h2> <h2>1. ԸՆԴՀԱՆՈՒՐ ԴՐՈՒՅԹՆԵՐ</h2>
<p>1.1. This policy of LLC "INT FIN LOGISTIC" (TIN 9909697628), hereinafter referred to as the "Operator", describes the procedure for processing personal data and is aimed at protecting the rights and legitimate interests of data subjects. The document has been developed in accordance with Federal Law No. 152-FZ of July 27, 2006 "On Personal Data".</p> <p>1.1. «ԻՆՏ ՖԻՆ ԼՈՋԻՍՏԻԿ» ՍՊԸ-ի (ՀՎՀՀ 9909697628) և «ԻՆՏ ՖԱԿՏՈՐԻՆԳ» ՍՊԸ-ի (ՀՎՀՀ 9909697635), այսուհետ` «Օպերատոր», սույն քաղաքականությունը նկարագրում է անձնական տվյալների մշակման կարգը և ուղղված է տվյալների սուբյեկտների իրավունքների և օրինական շահերի պաշտպանությանը։ Փաստաթուղթը մշակվել է 2006 թվականի հուլիսի 27-ի «Անձնական տվյալների մասին» թիվ 152-ՖԶ դաշնային օրենքի համաձայն։</p>
<p>1.2. The Policy defines the procedure and measures to ensure the security of personal data processing on the website <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>, with the goal of protecting human and civil rights and freedoms, including the right to privacy, personal and family secrets.</p> <p>1.2. Քաղաքականությունը սահմանում է <a href="https://dexarmarket.ru">https://dexarmarket.ru</a> կայքում անձնական տվյալների մշակման անվտանգության ապահովման կարգն ու միջոցները` մարդու և քաղաքացու իրավունքների և ազատությունների, այդ թվում` անձնական կյանքի, անձնական և ընտանեկան գաղտնիքի պաշտպանության նպատակով։</p>
<p>1.3. The document covers all processes carried out by the Operator relating to the processing of personal data.</p> <p>1.3. Փաստաթուղթը տարածվում է անձնական տվյալների մշակման հետ կապված բոլոր գործընթացների վրա, որոնք իրականացվում են Օպերատորի կողմից։</p>
<p>1.4. The Policy is mandatory for study and compliance by all persons authorized to process personal data.</p> <p>1.4. Քաղաքականությունը պարտադիր է ուսումնասիրելու և պահպանելու անձնական տվյալներ մշակելու իրավասություն ունեցող բոլոր անձանց համար։</p>
<p>1.5. It applies to all actions related to the processing of personal data on the website <a href="https://dexarmarket.ru">https://dexarmarket.ru</a> and in the Operator's information systems.</p> <p>1.5. Այն կիրառվում է <a href="https://dexarmarket.ru">https://dexarmarket.ru</a> կայքում և Օպերատորի տեղեկատվական համակարգերում անձնական տվյալների մշակման հետ կապված բոլոր գործողությունների նկատմամբ։</p>
<p>1.6. A User who places an order, opens a personal account, or otherwise interacts with the Operator expresses consent to the processing of their personal data in accordance with the Policy and the legislation of the Russian Federation. Continued use of the website indicates agreement with the provisions of the Policy. A User who is not willing to agree to the terms should refrain from using the resource.</p> <p>1.6. Օգտատերը, որը ձևակերպում է պատվեր, բացում է անձնական հաշիվ կամ այլ կերպ փոխգործակցում է Օպերատորի հետ, արտահայտում է համաձայնություն իր անձնական տվյալների մշակմանը` համաձայն սույն Քաղաքականության և Ռուսաստանի Դաշնության օրենսդրության։ Կայքի հետագա օգտագործումը նշանակում է համաձայնություն Քաղաքականության դրույթների հետ։ Այն Օգտատերը, որը պատրաստ չէ համաձայնել պայմաններին, պետք է ձեռնպահ մնա ռեսուրսից օգտվելուց։</p>
<p><strong>Additionally:</strong> This Policy applies to personal data collected both before and after the document comes into force.</p> <p><strong>Լրացուցիչ.</strong> Սույն Քաղաքականությունը տարածվում է անձնական տվյալների վրա, որոնք հավաքվել են ինչպես մինչև, այնպես էլ փաստաթղթի ուժի մեջ մտնելուց հետո։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>2. TERMS AND DEFINITIONS</h2> <h2>2. ՏԵՐՄԻՆՆԵՐ ԵՎ ՍԱՀՄԱՆՈՒՄՆԵՐ</h2>
<p>The following main terms and definitions are used in this Policy:</p> <p>Սույն Քաղաքականության մեջ օգտագործվում են հետևյալ հիմնական տերմիններն ու սահմանումները.</p>
<p><strong>Personal Data (PD)</strong> — any information directly or indirectly related to a specific individual (personal data subject).</p> <p><strong>Անձնական տվյալներ (ԱՏ)</strong> — ցանկացած տեղեկություն, որը ուղղակիորեն կամ անուղղակիորեն վերաբերում է որոշակի ֆիզիկական անձի (անձնական տվյալների սուբյեկտին)։</p>
<p><strong>Personal Data Information System (PDIS)</strong> — a set of personal data stored in databases, as well as technologies and means for their processing.</p> <p><strong>Անձնական տվյալների տեղեկատվական համակարգ (ԱՏՏՀ)</strong> — տվյալների բազաներում պահպանվող անձնական տվյալների ամբողջություն, ինչպես նաև դրանց մշակման տեխնոլոգիաներն ու միջոցները։</p>
<p><strong>Automated Processing of PD</strong> — data processing using computer means.</p> <p><strong>ԱՏ ավտոմատացված մշակում</strong> — տվյալների մշակում համակարգչային միջոցների կիրառմամբ։</p>
<p><strong>Blocking of PD</strong>temporary suspension of data processing (except in cases of data clarification).</p> <p><strong>ԱՏ արգելափակում</strong>տվյալների մշակման ժամանակավոր դադարեցում (բացառությամբ տվյալների ճշգրտման դեպքերի)։</p>
<p><strong>Depersonalization of PD</strong> — actions leading to the impossibility of determining the ownership of data to a specific person without additional information.</p> <p><strong>ԱՏ ապանձնավորում</strong> — գործողություններ, որոնց հետևանքով առանց լրացուցիչ տեղեկատվության անհնար է որոշել տվյալների պատկանելությունը կոնկրետ անձին։</p>
<p><strong>Internet Website (Website)</strong> — an automated information system available on the Internet at: <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>.</p> <p><strong>Ինտերնետ կայք (Կայք)</strong> — ավտոմատացված տեղեկատվական համակարգ, որը հասանելի է ինտերնետում հետևյալ հասցեով` <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>։</p>
<p><strong>Processing of PD</strong>any actions with personal data, including collection, recording, storage, updating, use, transfer, destruction, and other operations.</p> <p><strong>ԱՏ մշակում</strong>անձնական տվյալների հետ ցանկացած գործողություն, այդ թվում` հավաքագրում, գրանցում, պահպանում, թարմացում, օգտագործում, փոխանցում, ոչնչացում և այլ գործողություններ։</p>
<p><strong>Operator</strong>a state or private body that independently or jointly organizes the processing of personal data.</p> <p><strong>Օպերատոր</strong>պետական կամ մասնավոր մարմին, որը ինքնուրույն կամ համատեղ կազմակերպում է անձնական տվյալների մշակումը։</p>
<p><strong>Provision of PD</strong>transfer of data to a specific person or group of persons.</p> <p><strong>ԱՏ տրամադրում</strong>տվյալների փոխանցում կոնկրետ անձի կամ անձանց խմբի։</p>
<p><strong>Distribution of PD</strong>disclosure of data to an indefinite number of persons, including publication in the media or on the Internet.</p> <p><strong>ԱՏ տարածում</strong>տվյալների բացահայտում անորոշ թվով անձանց, այդ թվում` զանգվածային լրատվության միջոցներում կամ ինտերնետում հրապարակման միջոցով։</p>
<p><strong>Cross-border Transfer of PD</strong> — transfer of data to foreign authorities, companies, or individuals.</p> <p><strong>ԱՏ անդրսահմանային փոխանցում</strong> — տվյալների փոխանցում օտարերկրյա պետական մարմինների, ընկերությունների կամ ֆիզիկական անձանց։</p>
<p><strong>Destruction of PD</strong>actions leading to the loss of the ability to recover data or destruction of material carriers.</p> <p><strong>ԱՏ ոչնչացում</strong>գործողություններ, որոնք հանգեցնում են տվյալների վերականգնման հնարավորության կորստին կամ նյութական կրիչների ոչնչացմանը։</p>
<p><strong>PD Subject</strong>an individual whose information is being processed.</p> <p><strong>ԱՏ սուբյեկտ</strong>ֆիզիկական անձ, որի մասին տեղեկատվությունը մշակվում է։</p>
<p><strong>Confidentiality of PD</strong> — the Operator's obligation to protect data from distribution without the consent of the subject or a legal basis.</p> <p><strong>ԱՏ գաղտնիություն</strong> — Օպերատորի պարտավորությունը` պաշտպանել տվյալները տարածումից առանց սուբյեկտի համաձայնության կամ օրինական հիմքի։</p>
<p><strong>Seller (Contractor)</strong>a person offering goods or services on the website <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>.</p> <p><strong>Վաճառող (Կատարող)</strong>անձ, որը <a href="https://dexarmarket.ru">https://dexarmarket.ru</a> կայքում առաջարկում է ապրանքներ կամ ծառայություններ։</p>
<p><strong>User</strong>a person visiting or using resources managed by the Operator, including the website <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>.</p> <p><strong>Օգտատեր</strong>անձ, որը այցելում կամ օգտագործում է Օպերատորի կողմից կառավարվող ռեսուրսները, ներառյալ` <a href="https://dexarmarket.ru">https://dexarmarket.ru</a> կայքը։</p>
<p><strong>Order</strong>an order for goods or services placed by the User on the website.</p> <p><strong>Պատվեր</strong>Օգտատիրոջ կողմից կայքում ձևակերպված ապրանքների կամ ծառայությունների պատվեր։</p>
<p><strong>Cookies</strong>small files saved on the user's device to remember preferences and actions during subsequent visits to the website.</p> <p><strong>Cookie-ներ</strong>օգտատիրոջ սարքում պահպանվող փոքր ֆայլեր, որոնք օգտագործվում են նախընտրությունները և գործողությունները հիշելու համար կայք հետագա այցելությունների ընթացքում։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>3. LEGAL GROUNDS FOR PERSONAL DATA PROCESSING</h2> <h2>3. ԱՆՁՆԱԿԱՆ ՏՎՅԱԼՆԵՐԻ ՄՇԱԿՄԱՆ ԻՐԱՎԱԿԱՆ ՀԻՄՔԵՐԸ</h2>
<p>3.1. The legal basis for PD Processing, depending on the purposes of the process involving PD Processing, may be:</p> <p>3.1. ԱՏ մշակման իրավական հիմքը, կախված ԱՏ մշակման գործընթացի նպատակներից, կարող է լինել.</p>
<h3>3.1.1. The Constitution of the Russian Federation, as well as a set of legal acts:</h3> <h3>3.1.1. Ռուսաստանի Դաշնության Սահմանադրությունը, ինչպես նաև իրավական ակտերի ամբողջությունը.</h3>
<ul> <ul>
<li>Tax Code of the Russian Federation;</li> <li>Ռուսաստանի Դաշնության հարկային օրենսգիրք,</li>
<li>Civil Code of the Russian Federation;</li> <li>Ռուսաստանի Դաշնության քաղաքացիական օրենսգիրք,</li>
<li>Articles 8690 of the Labor Code of the Russian Federation;</li> <li>Ռուսաստանի Դաշնության աշխատանքային օրենսգրքի 86-90-րդ հոդվածներ,</li>
<li>Federal Law No. 115-FZ of August 7, 2001 "On Countering the Legalization (Laundering) of Proceeds of Crime and the Financing of Terrorism";</li> <li>2001 թվականի օգոստոսի 7-ի «Հանցավոր ճանապարհով ստացված եկամուտների օրինականացման (լվացման) և ահաբեկչության ֆինանսավորման դեմ պայքարի մասին» թիվ 115-ՖԶ դաշնային օրենք,</li>
<li>Federal Law No. 152-FZ of July 27, 2006 "On Personal Data";</li> <li>2006 թվականի հուլիսի 27-ի «Անձնական տվյալների մասին» թիվ 152-ՖԶ դաշնային օրենք,</li>
<li>Federal Law No. 39-FZ of April 22, 1996 "On the Securities Market";</li> <li>1996 թվականի ապրիլի 22-ի «Արժեթղթերի շուկայի մասին» թիվ 39-ՖԶ դաշնային օրենք,</li>
<li>Federal Law No. 208-FZ of December 26, 1995 "On Joint-Stock Companies";</li> <li>1995 թվականի դեկտեմբերի 26-ի «Բաժնետիրական ընկերությունների մասին» թիվ 208-ՖԶ դաշնային օրենք,</li>
<li>Federal Law No. 149-FZ of July 27, 2006 "On Information, Information Technologies and the Protection of Information";</li> <li>2006 թվականի հուլիսի 27-ի «Տեղեկատվության, տեղեկատվական տեխնոլոգիաների և տեղեկատվության պաշտպանության մասին» թիվ 149-ՖԶ դաշնային օրենք,</li>
<li>Federal Law No. 27-FZ of April 1, 1996 "On Individual (Personified) Accounting in the Mandatory Pension Insurance System";</li> <li>1996 թվականի ապրիլի 1-ի «Պարտադիր կենսաթոշակային ապահովագրության համակարգում անհատական (անձնավորված) հաշվառման մասին» թիվ 27-ՖԶ դաշնային օրենք,</li>
<li>Federal Law No. 63-FZ of April 6, 2011 "On Electronic Signatures";</li> <li>2011 թվականի ապրիլի 6-ի «Էլեկտրոնային ստորագրության մասին» թիվ 63-ՖԶ դաշնային օրենք,</li>
<li>Federal Law No. 402-FZ of December 6, 2011 "On Accounting";</li> <li>2011 թվականի դեկտեմբերի 6-ի «Հաշվապահական հաշվառման մասին» թիվ 402-ՖԶ դաշնային օրենք,</li>
<li>Federal Law No. 161-FZ of June 27, 2011 "On the National Payment System";</li> <li>2011 թվականի հունիսի 27-ի «Ազգային վճարային համակարգի մասին» թիվ 161-ՖԶ դաշնային օրենք,</li>
<li>Decree of the Government of the Russian Federation No. 687 of September 15, 2008 "On Approval of the Regulation on the Specifics of Personal Data Processing Carried Out Without the Use of Automation";</li> <li>2008 թվականի սեպտեմբերի 15-ի Ռուսաստանի Դաշնության կառավարության թիվ 687 որոշում «Անձնական տվյալների մշակման առանձնահատկությունների մասին կանոնակարգը հաստատելու մասին, երբ մշակումն իրականացվում է առանց ավտոմատացման միջոցների օգտագործման»,</li>
<li>Decree of the Government of the Russian Federation No. 1119 of November 1, 2012 "On Approval of the Requirements for the Protection of Personal Data During Their Processing in Personal Data Information Systems";</li> <li>2012 թվականի նոյեմբերի 1-ի Ռուսաստանի Դաշնության կառավարության թիվ 1119 որոշում «Անձնական տվյալների տեղեկատվական համակարգերում դրանց մշակման ընթացքում անձնական տվյալների պաշտպանության պահանջները հաստատելու մասին»,</li>
<li>other regulatory legal acts of the Russian Federation and regulatory documents of executive authorities.</li> <li>Ռուսաստանի Դաշնության այլ նորմատիվ իրավական ակտեր և գործադիր մարմինների նորմատիվ փաստաթղթեր։</li>
</ul> </ul>
<p>3.1.2. The Operator's Charter.</p> <p>3.1.2. Օպերատորի կանոնադրությունը։</p>
<p>3.1.3. Contracts concluded between the Operator and the Personal Data Subject, including in cases where the Operator exercises its right to assign rights (claims) under such contracts, between the Operator and another person who has entrusted the Operator with PD Processing, as well as for the conclusion of contracts of which Personal Data Subjects are parties.</p> <p>3.1.3. Օպերատորի և Անձնական տվյալների սուբյեկտի միջև կնքված պայմանագրերը, ներառյալ դեպքերը, երբ Օպերատորն իրականացնում է նման պայմանագրերից բխող իրավունքների (պահանջների) զիջման իր իրավունքը, Օպերատորի և այլ անձի միջև, որը Օպերատորին հանձնարարել է ԱՏ մշակումը, ինչպես նաև պայմանագրերի կնքման համար, որոնց կողմ են հանդիսանում Անձնական տվյալների սուբյեկտները։</p>
<p>3.1.4. Consent to PD Processing (in cases not directly provided for by the legislation of the Russian Federation, but corresponding to the Operator's powers), including the consent of applicants for vacant positions to PD Processing, the consent of interns to PD Processing, the consent of employees to PD Processing; the consent of clients to PD Processing, the consent of Users of the relevant Website, the consent of other Personal Data Subjects.</p> <p>3.1.4. ԱՏ մշակման համաձայնությունը (այն դեպքերում, որոնք ուղղակիորեն նախատեսված չեն Ռուսաստանի Դաշնության օրենսդրությամբ, բայց համապատասխանում են Օպերատորի լիազորություններին), ներառյալ թափուր հաստիքների թեկնածուների համաձայնությունը ԱՏ մշակմանը, պրակտիկանտների համաձայնությունը ԱՏ մշակմանը, աշխատակիցների համաձայնությունը ԱՏ մշակմանը, հաճախորդների համաձայնությունը ԱՏ մշակմանը, համապատասխան Կայքի Օգտատերերի համաձայնությունը, ինչպես նաև այլ Անձնական տվյալների սուբյեկտների համաձայնությունը։</p>
<p>3.1.5. A contract between the operator and a third party, in which the latter entrusts the Operator with the processing of personal data of the Personal Data Subject or transfers personal data of the Personal Data Subject on the basis of a concluded contract.</p> <p>3.1.5. Օպերատորի և երրորդ անձի միջև պայմանագիր, որով վերջինս Օպերատորին հանձնարարում է Անձնական տվյալների սուբյեկտի անձնական տվյալների մշակումը կամ փոխանցում է Անձնական տվյալների սուբյեկտի անձնական տվյալները կնքված պայմանագրի հիման վրա։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>4. CATEGORIES OF PERSONAL DATA SUBJECTS WHOSE PERSONAL DATA IS PROCESSED BY THE OPERATOR</h2> <h2>4. ԱՆՁՆԱԿԱՆ ՏՎՅԱԼՆԵՐԻ ՍՈՒԲՅԵԿՏՆԵՐԻ ԿԱՏԵԳՈՐԻԱՆԵՐԸ, ՈՐՈՆՑ ՏՎՅԱԼՆԵՐԸ ՄՇԱԿՎՈՒՄ ԵՆ ՕՊԵՐԱՏՈՐԻ ԿՈՂՄԻՑ</h2>
<p>4.1. The Operator processes PD obtained in accordance with the law, belonging to:</p> <p>4.1. Օպերատորը մշակում է օրենքով ստացված ԱՏ, որոնք վերաբերում են.</p>
<ul> <ul>
<li>Job candidates and employees of the Operator;</li> <li>Օպերատորի աշխատանքի թեկնածուներին և աշխատակիցներին,</li>
<li>Former employees of the Operator;</li> <li>Օպերատորի նախկին աշխատակիցներին,</li>
<li>Close relatives/family members of the Operator's employees, interns;</li> <li>Օպերատորի աշխատակիցների և պրակտիկանտների մերձավոր ազգականներին/ընտանիքի անդամներին,</li>
<li>Potential clients, individual clients, individual entrepreneur clients (individuals registered in the established manner and carrying out entrepreneurial activities without forming a legal entity), individuals engaged in private practice in accordance with the legislation of the Russian Federation (self-employed), individual beneficiaries, beneficial owners, representatives;</li> <li>պոտենցիալ հաճախորդներին, ֆիզիկական անձ հաճախորդներին, անհատ ձեռնարկատեր հաճախորդներին (օրենքով սահմանված կարգով գրանցված և առանց իրավաբանական անձ ստեղծելու ձեռնարկատիրական գործունեություն իրականացնող ֆիզիկական անձանց), Ռուսաստանի Դաշնության օրենսդրության համաձայն մասնավոր պրակտիկայով զբաղվող ֆիզիկական անձանց (ինքնազբաղվածներ), անհատական շահառուներին, վերջնական շահառուներին, ներկայացուցիչներին,</li>
<li>Individuals who have entered into civil law contracts with the Operator, including purchase and sale, for the provision of services and/or performance of work for the Operator;</li> <li>ֆիզիկական անձանց, որոնք Օպերատորի հետ կնքել են քաղաքացիաիրավական պայմանագրեր, այդ թվում` առուվաճառքի, ծառայությունների մատուցման և/կամ աշխատանքների կատարման պայմանագրեր Օպերատորի համար,</li>
<li>Users of the Operator's Websites, Order recipients (if the User has specified another person as the Order recipient);</li> <li>Օպերատորի Կայքերի Օգտատերերին, Պատվերի ստացողներին (եթե Օգտատերը որպես Պատվերի ստացող նշել է այլ անձի),</li>
<li>Clients of other legal entities, the Processing of Personal Data for which is carried out on behalf of the said legal entities in accordance with the legislation of the Russian Federation;</li> <li>այլ իրավաբանական անձանց հաճախորդներին, որոնց անձնական տվյալների մշակումը կատարվում է նշված իրավաբանական անձանց հանձնարարությամբ` Ռուսաստանի Դաշնության օրենսդրությանը համապատասխան,</li>
<li>Owners of the Operator;</li> <li>Օպերատորի սեփականատերերին,</li>
<li>Other subjects who have entered or intend to enter into contractual relations with the Operator and/or who apply to the Operator with applications/appeals.</li> <li>այլ սուբյեկտներին, որոնք մտել են կամ մտադիր են մտնել պայմանագրային հարաբերությունների մեջ Օպերատորի հետ և/կամ դիմում են Օպերատորին դիմումներով/բողոքներով։</li>
</ul> </ul>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>5. CATEGORIES OF PERSONAL DATA PROCESSED BY THE OPERATOR</h2> <h2>5. ՕՊԵՐԱՏՈՐԻ ԿՈՂՄԻՑ ՄՇԱԿՎՈՂ ԱՆՁՆԱԿԱՆ ՏՎՅԱԼՆԵՐԻ ԿԱՏԵԳՈՐԻԱՆԵՐԸ</h2>
<p>5.1. The Operator processes the following categories of PD of Users and Order recipients:</p> <p>5.1. Օպերատորը մշակում է Օգտատերերի և Պատվերի ստացողների հետևյալ կատեգորիայի ԱՏ.</p>
<ul> <ul>
<li>Information obtained during registration and/or placing an Order (surname, first name, actual address, phone number, email address, cookies);</li> <li>գրանցման և/կամ Պատվերի ձևակերպման ընթացքում ստացված տեղեկատվություն (ազգանուն, անուն, փաստացի հասցե, հեռախոսահամար, էլեկտրոնային փոստի հասցե, cookie-ներ),</li>
<li>Information obtained during interaction with Users (surname, first name, patronymic, gender, place of birth, date of birth, passport data (series, number, issuing authority, department code, issue date, registration address), actual address, phone number, email address);</li> <li>Օգտատերերի հետ փոխգործակցության ընթացքում ստացված տեղեկատվություն (ազգանուն, անուն, հայրանուն, սեռ, ծննդավայր, ծննդյան ամսաթիվ, անձնագրային տվյալներ (սերիա, համար, տված մարմին, ստորաբաժանման կոդ, տրման ամսաթիվ, գրանցման հասցե), փաստացի հասցե, հեռախոսահամար, էլեկտրոնային փոստի հասցե),</li>
<li>Information about the Order delivery method, payment method and payment status, and if the final Order recipient differs from the User, also the surname, first name, patronymic, gender, place of birth, date of birth, passport data, delivery address, and phone number of the Order recipient;</li> <li>Պատվերի առաքման եղանակի, վճարման եղանակի և վճարման կարգավիճակի մասին տեղեկատվություն, և եթե Պատվերի վերջնական ստացողը տարբերվում է Օգտատիրոջից, ապա նաև Պատվերի ստացողի ազգանունը, անունը, հայրանունը, սեռը, ծննդավայրը, ծննդյան ամսաթիվը, անձնագրային տվյալները, առաքման հասցեն և հեռախոսահամարը,</li>
<li>Information about User complaints (submitted by the User via the Websites or otherwise);</li> <li>Օգտատերերի բողոքների մասին տեղեկատվություն (ներկայացված Կայքերի միջոցով կամ այլ եղանակով),</li>
<li>Geolocation (location) information.</li> <li>աշխարհագրական տեղորոշման (լոկացիայի) տվյալներ։</li>
</ul> </ul>
<p>5.2. The Operator processes the following categories of PD of Personal Data Subjects who contact the Operator with claims of alleged violation of their rights: surname, first name, actual address, contact information (phone number and/or email address) of the rights holder or other person whose right was allegedly violated, and/or the applicant, if acting as an authorized representative of the rights holder or other person whose right was allegedly violated, information about received claims, the progress and results of their consideration.</p> <p>5.2. Օպերատորը մշակում է այն Անձնական տվյալների սուբյեկտների հետևյալ կատեգորիայի ԱՏ, որոնք դիմում են Օպերատորին իրենց իրավունքների ենթադրյալ խախտման վերաբերյալ պահանջներով` իրավունքակալի կամ այլ անձի, որի իրավունքը ենթադրաբար խախտվել է, ինչպես նաև դիմողի ազգանուն, անուն, փաստացի հասցե, կոնտակտային տվյալներ (հեռախոսահամար և/կամ էլեկտրոնային փոստի հասցե), եթե դիմողը գործում է որպես իրավունքակալի կամ այլ անձի լիազորված ներկայացուցիչ, ում իրավունքը ենթադրաբար խախտվել է, ինչպես նաև ստացված պահանջների, դրանց քննության ընթացքի և արդյունքների վերաբերյալ տեղեկատվություն։</p>
<p>5.3. Personal data specified in the paragraphs of this section above may be obtained by the Operator in one of the following ways:</p> <p>5.3. Վերը նշված բաժնի կետերում նշված անձնական տվյալները Օպերատորի կողմից կարող են ստացվել հետևյալ եղանակներից մեկով.</p>
<ul> <ul>
<li>Provided by Personal Data Subjects by filling in the appropriate forms on one of the Websites, by sending correspondence or emails to the Operator's email addresses;</li> <li>Անձնական տվյալների սուբյեկտների կողմից համապատասխան ձևաթղթերը լրացնելով Օպերատորի Կայքերից մեկում, նամակագրություն կամ էլեկտրոնային նամակներ ուղարկելով Օպերատորի էլեկտրոնային հասցեներին,</li>
<li>Obtained from third parties in cases provided for in this section. In particular, Users' PD may be obtained by the Operator directly from Sellers or other counterparties of the Operator in connection with the execution of the Seller's instructions and/or the performance of other actions provided for by the current legislation of the Russian Federation.</li> <li>երրորդ անձանցից ստացված` սույն բաժնով նախատեսված դեպքերում։ Մասնավորապես, Օգտատերերի ԱՏ կարող են Օպերատորի կողմից ստացվել անմիջապես Վաճառողներից կամ Օպերատորի այլ կոնտրագենտներից` Վաճառողի հանձնարարությունների կատարման և/կամ Ռուսաստանի Դաշնության գործող օրենսդրությամբ նախատեսված այլ գործողությունների իրականացման հետ կապված։</li>
</ul> </ul>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>6. PRINCIPLES, PROCEDURE AND CONDITIONS OF PERSONAL DATA PROCESSING</h2> <h2>6. ԱՆՁՆԱԿԱՆ ՏՎՅԱԼՆԵՐԻ ՄՇԱԿՄԱՆ ՍԿԶԲՈՒՆՔՆԵՐԸ, ԿԱՐԳԸ ԵՎ ՊԱՅՄԱՆՆԵՐԸ</h2>
<h3>6.1. Principles of Personal Data Processing</h3> <h3>6.1. Անձնական տվյալների մշակման սկզբունքները</h3>
<p>PD Processing by the Operator is carried out on the basis of the following principles:</p> <p>Օպերատորի կողմից ԱՏ մշակումը իրականացվում է հետևյալ սկզբունքների հիման վրա.</p>
<ul> <ul>
<li>legality and fairness;</li> <li>օրինականություն և բարեխղճություն,</li>
<li>limitation of PD Processing to the achievement of specific, predetermined and legitimate purposes;</li> <li>ԱՏ մշակման սահմանափակում միայն կոնկրետ, նախապես սահմանված և օրինական նպատակների հասնելու շրջանակներում,</li>
<li>prevention of PD Processing incompatible with the purposes of PD collection;</li> <li>ԱՏ մշակման բացառումը, որը անհամատեղելի է ԱՏ հավաքագրման նպատակների հետ,</li>
<li>prevention of combining databases containing PD, the Processing of which is carried out for purposes incompatible with each other;</li> <li>տվյալների բազաների միավորումը չթույլատրելը, եթե դրանցում պարունակվող ԱՏ մշակվում են միմյանց հետ անհամատեղելի նպատակներով,</li>
<li>Processing only such PD that correspond to the purposes of their Processing;</li> <li>մշակել միայն այնպիսի ԱՏ, որոնք համապատասխանում են դրանց մշակման նպատակներին,</li>
<li>compliance of the content and volume of Processed PD with the stated purposes of Processing;</li> <li>մշակվող ԱՏ բովանդակության և ծավալի համապատասխանությունը հայտարարված նպատակներին,</li>
<li>prevention of PD Processing that is excessive in relation to the stated purposes of their Processing;</li> <li>չթույլատրել ԱՏ ավելորդ մշակում` դրանց մշակման հայտարարված նպատակների համեմատ,</li>
<li>ensuring the accuracy, sufficiency and relevance of PD in relation to the purposes of PD Processing;</li> <li>ԱՏ ճշգրտության, բավարարության և արդիականության ապահովում` ԱՏ մշակման նպատակների նկատմամբ,</li>
<li>storage of PD in a form that allows identifying the Personal Data Subject for no longer than required by the purposes of PD Processing;</li> <li>ԱՏ պահպանում այնպիսի ձևով, որը թույլ է տալիս նույնականացնել Անձնական տվյալների սուբյեկտին ոչ ավելի երկար, քան պահանջվում է ԱՏ մշակման նպատակներով,</li>
<li>destruction or depersonalization of PD upon achieving the purposes of their Processing, upon withdrawal of consent to their Processing by the Personal Data Subject, or in the event of loss of the need to achieve these purposes.</li> <li>ԱՏ ոչնչացում կամ ապանձնավորում` դրանց մշակման նպատակներին հասնելուց հետո, ԱՏ սուբյեկտի կողմից դրանց մշակման համաձայնությունը հետ կանչելու դեպքում կամ այդ նպատակներին հասնելու անհրաժեշտության վերացման դեպքում։</li>
</ul> </ul>
<h3>6.2. Obligations of the Operator's Employees</h3> <h3>6.2. Օպերատորի աշխատակիցների պարտականությունները</h3>
<p>Employees of the Operator authorized to Process Personal Data are obliged to:</p> <p>Օպերատորի այն աշխատակիցները, որոնք լիազորված են մշակել Անձնական տվյալներ, պարտավոր են.</p>
<ul> <ul>
<li>Know and strictly comply with the provisions of the legislation of the Russian Federation in the field of PD;</li> <li>իմանալ և խստորեն պահպանել Ռուսաստանի Դաշնության օրենսդրության դրույթները ԱՏ ոլորտում,</li>
<li>Know and comply with the provisions of this Policy;</li> <li>իմանալ և պահպանել սույն Քաղաքականության դրույթները,</li>
<li>Know and comply with the Operator's local acts on PD Processing and security;</li> <li>իմանալ և պահպանել Օպերատորի տեղական ակտերը ԱՏ մշակման և անվտանգության վերաբերյալ,</li>
<li>Process PD only within the scope of their job duties;</li> <li>մշակել ԱՏ միայն իրենց ծառայողական պարտականությունների շրջանակում,</li>
<li>Not disclose PD processed by the Operator;</li> <li>չբացահայտել Օպերատորի կողմից մշակվող ԱՏ,</li>
<li>Report actions of other persons that may lead to violation of the provisions of this Policy.</li> <li>տեղեկացնել այլ անձանց այնպիսի գործողությունների մասին, որոնք կարող են հանգեցնել սույն Քաղաքականության դրույթների խախտմանը։</li>
</ul> </ul>
<p>6.3. The Operator is obliged to ensure that the Operator's employees who directly carry out the Processing of Personal Data are familiarized with the provisions of Russian legislation on Personal Data, including the requirements for PD protection, local acts on the Processing and protection of Personal Data, and training of the Operator's employees.</p> <p>6.3. Օպերատորը պարտավոր է ապահովել, որ իր այն աշխատակիցները, որոնք անմիջականորեն իրականացնում են Անձնական տվյալների մշակումը, ծանոթ լինեն Ռուսաստանի օրենսդրության դրույթներին անձնական տվյալների վերաբերյալ, ներառյալ` ԱՏ պաշտպանության պահանջներին, անձնական տվյալների մշակման և պաշտպանության վերաբերյալ տեղական ակտերին, ինչպես նաև ապահովել Օպերատորի աշխատակիցների վերապատրաստումը։</p>
<h3>6.4. Conditions for Personal Data Processing</h3> <h3>6.4. Անձնական տվյալների մշակման պայմանները</h3>
<p>The Operator processes PD if at least one of the following conditions is met:</p> <p>Օպերատորը մշակում է ԱՏ, եթե բավարարվում է հետևյալ պայմաններից առնվազն մեկը.</p>
<ul> <ul>
<li>PD Processing is carried out with the consent of the PD Subject to the Processing of their PD;</li> <li>ԱՏ մշակումը իրականացվում է ԱՏ սուբյեկտի համաձայնությամբ իր ԱՏ մշակման վերաբերյալ,</li>
<li>PD Processing is necessary to achieve the goals provided for by an international treaty of the Russian Federation or by law;</li> <li>ԱՏ մշակումը անհրաժեշտ է Ռուսաստանի Դաշնության միջազգային պայմանագրով կամ օրենքով նախատեսված նպատակներին հասնելու համար,</li>
<li>PD Processing is necessary for the administration of justice, execution of a judicial act;</li> <li>ԱՏ մշակումը անհրաժեշտ է արդարադատության իրականացման, դատական ակտի կատարման համար,</li>
<li>PD Processing is necessary for the performance of a contract to which the PD Subject is a party, beneficiary, or guarantor;</li> <li>ԱՏ մշակումը անհրաժեշտ է պայմանագրի կատարման համար, որի կողմ, շահառու կամ երաշխավոր է ԱՏ սուբյեկտը,</li>
<li>PD Processing is necessary for the exercise of the rights and legitimate interests of the Operator or third parties;</li> <li>ԱՏ մշակումը անհրաժեշտ է Օպերատորի կամ երրորդ անձանց իրավունքների և օրինական շահերի իրականացման համար,</li>
<li>Processing of PD to which the PD Subject has provided access to an unlimited number of persons is carried out;</li> <li>իրականացվում է այն ԱՏ մշակումը, որոնց նկատմամբ ԱՏ սուբյեկտը տրամադրել է հասանելիություն անսահմանափակ թվով անձանց,</li>
<li>Processing of PD subject to publication or mandatory disclosure in accordance with FZ-152 is carried out.</li> <li>իրականացվում է այն ԱՏ մշակումը, որոնք ենթակա են հրապարակման կամ պարտադիր բացահայտման` թիվ 152-ՖԶ օրենքին համապատասխան։</li>
</ul> </ul>
<p>6.5. The Operator carries out PD Processing using automated and non-automated means, including collection, recording, systematization, accumulation, storage, clarification (updating, modification), extraction, use, transfer (Distribution, Provision, access), Depersonalization, Blocking, deletion, Destruction of Personal Data within the timeframes necessary to achieve the purposes of Personal Data Processing.</p> <p>6.5. Օպերատորը իրականացնում է ԱՏ մշակում ավտոմատացված և ոչ ավտոմատացված միջոցներով, ներառյալ` անձնական տվյալների հավաքագրում, գրանցում, համակարգում, կուտակում, պահպանում, ճշգրտում (թարմացում, փոփոխություն), քաղում, օգտագործում, փոխանցում (տարածում, տրամադրում, հասանելիություն), ապանձնավորում, արգելափակում, ջնջում, ոչնչացում` այն ժամկետներում, որոնք անհրաժեշտ են անձնական տվյալների մշակման նպատակներին հասնելու համար։</p>
<p>6.6. The Operator is prohibited from making decisions based solely on the Automated Processing of Personal Data that produce legal consequences with respect to the Personal Data Subject, or otherwise affect their rights and legitimate interests, except in cases and conditions provided for by the legislation of the Russian Federation in the field of Personal Data.</p> <p>6.6. Օպերատորին արգելվում է կայացնել որոշումներ միայն Անձնական տվյալների ավտոմատացված մշակման հիման վրա, որոնք իրավական հետևանքներ են առաջացնում ԱՏ սուբյեկտի համար կամ այլ կերպ ազդում են նրա իրավունքների և օրինական շահերի վրա, բացառությամբ Ռուսաստանի Դաշնության օրենսդրությամբ նախատեսված դեպքերի և պայմանների։</p>
<p>6.7. Representatives of state authorities (including controlling, supervisory, law enforcement, inquiry, investigation, and other authorized bodies on grounds provided for by the current legislation of the Russian Federation) obtain access to PD Processed by the Operator in the scope and manner established by the legislation of the Russian Federation.</p> <p>6.7. Պետական մարմինների ներկայացուցիչները (ներառյալ վերահսկող, հսկողական, իրավապահ, հետաքննության, քննության և այլ լիազորված մարմինները` Ռուսաստանի Դաշնության գործող օրենսդրությամբ նախատեսված հիմքերով) ստանում են Օպերատորի կողմից մշակվող ԱՏ հասանելիություն` Ռուսաստանի Դաշնության օրենսդրությամբ սահմանված ծավալով և կարգով։</p>
<p>6.8. The Processing of Personal Data by the Operator is carried out with the consent of the Personal Data Subject, except in cases established by the legislation of the Russian Federation, in compliance with the requirements for PD Confidentiality established by Art. 7 of FZ-152, as well as the adoption of measures aimed at ensuring the fulfillment of obligations for the Processing and protection of Personal Data established by the legislation of the Russian Federation.</p> <p>6.8. Անձնական տվյալների մշակումը Օպերատորի կողմից իրականացվում է ԱՏ սուբյեկտի համաձայնությամբ, բացառությամբ Ռուսաստանի Դաշնության օրենսդրությամբ սահմանված դեպքերի, պահպանելով թիվ 152-ՖԶ օրենքի 7-րդ հոդվածով սահմանված ԱՏ գաղտնիության պահանջները, ինչպես նաև ընդունելով միջոցներ, որոնք ուղղված են Ռուսաստանի Դաշնության օրենսդրությամբ սահմանված պարտավորությունների կատարմանը` անձնական տվյալների մշակման և պաշտպանության մասով։</p>
<p>6.9. The condition for the termination of Personal Data Processing may be the achievement of Personal Data Processing purposes, the expiration of the consent of the Personal Data Subject to the Processing of their PD, or the withdrawal of consent by the Personal Data Subject to the Processing of their PD, as well as the identification of unlawful PD Processing.</p> <p>6.9. Անձնական տվյալների մշակման դադարեցման հիմք կարող է լինել անձնական տվյալների մշակման նպատակներին հասնելը, ԱՏ սուբյեկտի համաձայնության ժամկետի ավարտը իր ԱՏ մշակման վերաբերյալ կամ ԱՏ սուբյեկտի կողմից համաձայնության հետկանչը, ինչպես նաև ԱՏ ապօրինի մշակման փաստի հայտնաբերումը։</p>
<p>6.10. Personal Data is stored in a form that allows identifying the Personal Data Subject for no longer than required by the purposes of Personal Data Processing, except in cases where the storage period of Personal Data is established by FZ-152, a contract to which the Personal Data Subject is a party, beneficiary, or guarantor.</p> <p>6.10. Անձնական տվյալները պահպանվում են այնպիսի ձևով, որը թույլ է տալիս նույնականացնել Անձնական տվյալների սուբյեկտին ոչ ավելի երկար, քան պահանջվում է անձնական տվյալների մշակման նպատակներով, բացառությամբ այն դեպքերի, երբ ԱՏ պահպանման ժամկետը սահմանված է թիվ 152-ՖԶ օրենքով կամ պայմանագրով, որի կողմ, շահառու կամ երաշխավոր է ԱՏ սուբյեկտը։</p>
<p>6.11. In the event of confirmation of the fact of inaccuracy of Personal Data or unlawfulness of their Processing, the Personal Data is subject to updating by the Operator, and the Processing must be terminated, respectively.</p> <p>6.11. Եթե հաստատվում է անձնական տվյալների անճշտության կամ դրանց մշակման անօրինականության փաստը, ապա անձնական տվյալները ենթակա են թարմացման Օպերատորի կողմից, իսկ մշակումը` համապատասխանաբար դադարեցման։</p>
<p>6.12. The Operator does not verify and, as a rule, does not have the ability to verify the relevance and reliability of the information provided by Personal Data Subjects obtained through each of the Operator's Websites. The Operator assumes that Personal Data Subjects, acting reasonably and in good faith, provide reliable and sufficient PD and maintain them in an up-to-date state.</p> <p>6.12. Օպերատորը չի ստուգում և, որպես կանոն, հնարավորություն չունի ստուգելու իր Կայքերից յուրաքանչյուրի միջոցով Անձնական տվյալների սուբյեկտների կողմից տրամադրված տեղեկատվության արդիականությունն ու հավաստիությունը։ Օպերատորը ենթադրում է, որ Անձնական տվյալների սուբյեկտները, գործելով ողջամտորեն և բարեխղճորեն, տրամադրում են հավաստի և բավարար ԱՏ և պահպանում են դրանք արդիական վիճակում։</p>
<h3>6.13. Procedure for Obtaining Clarifications</h3> <h3>6.13. Պարզաբանումներ ստանալու կարգը</h3>
<p>The Operator provides the Personal Data Subject or their representative with information regarding the Processing of their Personal Data upon the appropriate request or inquiry of the Personal Data Subject or their representative in an accessible form.</p> <p>Օպերատորը Անձնական տվյալների սուբյեկտին կամ նրա ներկայացուցչին տրամադրում է նրա անձնական տվյալների մշակման վերաբերյալ տեղեկատվություն` Անձնական տվյալների սուբյեկտի կամ նրա ներկայացուցչի համապատասխան դիմումի կամ հարցման դեպքում հասանելի ձևով։</p>
<p>6.14. The request of the Personal Data Subject for obtaining information regarding the Processing of their PD by the Operator must contain:</p> <p>6.14. Անձնական տվյալների սուբյեկտի հարցումը Օպերատորի կողմից իր ԱՏ մշակման վերաբերյալ տեղեկատվություն ստանալու համար պետք է պարունակի.</p>
<ul> <ul>
<li>surname, first name, phone number, email address, and actual residential address of the Personal Data Subject or their representative, and, in the case of a representative's inquiry, the details of the power of attorney or other document confirming the representative's authority;</li> <li>Անձնական տվյալների սուբյեկտի կամ նրա ներկայացուցչի ազգանունը, անունը, հեռախոսահամարը, էլեկտրոնային փոստի հասցեն և փաստացի բնակության հասցեն, իսկ ներկայացուցչի դիմելու դեպքում` լիազորագիրը կամ ներկայացուցչի լիազորությունները հաստատող այլ փաստաթղթի տվյալները,</li>
<li>Information confirming the participation of the Personal Data Subject in relations with the Operator (contract number, contract date, etc.);</li> <li>տեղեկատվություն, որը հաստատում է Անձնական տվյալների սուբյեկտի մասնակցությունը Օպերատորի հետ հարաբերություններին (պայմանագրի համար, պայմանագրի ամսաթիվ և այլն),</li>
<li>Signature of the Personal Data Subject or their representative.</li> <li>Անձնական տվյալների սուբյեկտի կամ նրա ներկայացուցչի ստորագրությունը։</li>
</ul> </ul>
<p>6.15. The request may be sent in the form of an electronic document and signed with an electronic signature in accordance with the legislation of the Russian Federation.</p> <p>6.15. Հարցումը կարող է ուղարկվել էլեկտրոնային փաստաթղթի տեսքով և ստորագրվել էլեկտրոնային ստորագրությամբ` Ռուսաստանի Դաշնության օրենսդրությանը համապատասխան։</p>
<p>6.16. The appeal of the Personal Data Subject or their legal representative to the Operator for the purpose of exercising their rights established by FZ-152 is carried out in writing in the form established by the Operator or in free form at the Operator's registered address.</p> <p>6.16. Անձնական տվյալների սուբյեկտի կամ նրա օրինական ներկայացուցչի դիմումը Օպերատորին` իր իրավունքների իրականացման նպատակով, որոնք սահմանված են թիվ 152-ՖԶ օրենքով, կատարվում է գրավոր` Օպերատորի կողմից սահմանված ձևով կամ ազատ ձևով` Օպերատորի գրանցված հասցեով։</p>
<p>6.17. The Operator considers the appeal of the Personal Data Subject/withdrawal of consent to PD Processing and provides a response to it in accordance with the legislation of the Russian Federation. The form for the appeal of the Personal Data Subject/withdrawal of consent to PD Processing is posted on the Internet at https://dexarmarket.ru.</p> <p>6.17. Օպերատորը քննում է Անձնական տվյալների սուբյեկտի դիմումը/ԱՏ մշակման համաձայնության հետկանչը և պատասխանում է դրան` Ռուսաստանի Դաշնության օրենսդրությանը համապատասխան։ Անձնական տվյալների սուբյեկտի դիմումի/ԱՏ մշակման համաձայնության հետկանչի ձևը տեղադրված է ինտերնետում` https://dexarmarket.ru հասցեում։</p>
<h3>6.18. Confidentiality of Personal Data</h3> <h3>6.18. Անձնական տվյալների գաղտնիությունը</h3>
<p>Personal data is not disclosed to third parties and is not distributed otherwise without the consent of the Personal Data Subject, unless otherwise provided by the legislation of the Russian Federation. When disclosing (providing) Personal Data to third parties, the requirements for the protection of Processed Personal Data are observed.</p> <p>Անձնական տվյալները չեն բացահայտվում երրորդ անձանց և այլ կերպ չեն տարածվում առանց Անձնական տվյալների սուբյեկտի համաձայնության, եթե այլ բան նախատեսված չէ Ռուսաստանի Դաշնության օրենսդրությամբ։ Երրորդ անձանց անձնական տվյալներ բացահայտելիս (տրամադրելիս) պահպանվում են մշակվող անձնական տվյալների պաշտպանության պահանջները։</p>
<h3>6.19. Publicly Available Sources of Personal Data</h3> <h3>6.19. Անձնական տվյալների հանրամատչելի աղբյուրներ</h3>
<p>For information purposes, the Operator may create publicly available sources of PD of PD Subjects, including directories and address books. With the written consent of the PD Subject, their surname, first name, place of birth, contact phone number, email address, and other Personal Data communicated by the PD Subject may be included in publicly available PD sources.</p> <p>Տեղեկատվական նպատակներով Օպերատորը կարող է ստեղծել ԱՏ սուբյեկտների ԱՏ հանրամատչելի աղբյուրներ, ներառյալ տեղեկագրքեր և հասցեական գրքեր։ ԱՏ սուբյեկտի գրավոր համաձայնությամբ հանրամատչելի աղբյուրներում կարող են ներառվել նրա ազգանունը, անունը, ծննդավայրը, կոնտակտային հեռախոսահամարը, էլեկտրոնային փոստի հասցեն և ԱՏ սուբյեկտի կողմից հաղորդված այլ անձնական տվյալներ։</p>
<p>Information about the PD Subject must be excluded from publicly available PD sources at any time at the request of the PD Subject, the authorized body for the protection of PD Subjects' rights, or by court order.</p> <p>ԱՏ սուբյեկտի վերաբերյալ տեղեկությունները պետք է ցանկացած ժամանակ բացառվեն հանրամատչելի աղբյուրներից` ԱՏ սուբյեկտի, ԱՏ սուբյեկտների իրավունքների պաշտպանության լիազորված մարմնի պահանջով կամ դատարանի որոշմամբ։</p>
<h3>6.20. Special Categories of Personal Data</h3> <h3>6.20. Անձնական տվյալների հատուկ կատեգորիաներ</h3>
<p>Processing of special categories of PD by the Operator is permitted in the cases specified in Article 10 of FZ-152. Processing of PD about criminal records may be carried out by the Operator exclusively in cases and in the manner determined in accordance with the legislation of the Russian Federation.</p> <p>Օպերատորի կողմից ԱՏ հատուկ կատեգորիաների մշակումը թույլատրվում է թիվ 152-ՖԶ օրենքի 10-րդ հոդվածում նշված դեպքերում։ Դատվածության վերաբերյալ ԱՏ մշակումը կարող է Օպերատորի կողմից իրականացվել բացառապես այն դեպքերում և կարգով, որոնք սահմանվում են Ռուսաստանի Դաշնության օրենսդրությանը համապատասխան։</p>
<h3>6.21. Biometric Personal Data</h3> <h3>6.21. Կենսաչափական անձնական տվյալներ</h3>
<p>Information that characterizes the physiological and biological features of a person, on the basis of which their identity can be established — biometric Personal Data — may be processed by the Operator only with the written consent of the PD Subject.</p> <p>Տեղեկատվությունը, որը բնութագրում է անձի ֆիզիոլոգիական և կենսաբանական առանձնահատկությունները և որի հիման վրա հնարավոր է հաստատել նրա ինքնությունը` կենսաչափական անձնական տվյալները, կարող է Օպերատորի կողմից մշակվել միայն ԱՏ սուբյեկտի գրավոր համաձայնությամբ։</p>
<h3>6.22. Entrusting Personal Data Processing to Another Person</h3> <h3>6.22. Անձնական տվյալների մշակումը այլ անձի հանձնարարված լինելը</h3>
<p>The Operator has the right to entrust PD Processing to another person with the consent of the PD Subject, unless otherwise provided by federal law, on the basis of a contract concluded with that person. The person carrying out PD Processing on behalf of the Operator is obliged to comply with the principles and rules of PD Processing provided for by FZ-152 and this Policy.</p> <p>Օպերատորն իրավունք ունի ԱՏ սուբյեկտի համաձայնությամբ ԱՏ մշակումը հանձնել այլ անձի, եթե այլ բան նախատեսված չէ դաշնային օրենքով, այդ անձի հետ կնքված պայմանագրի հիման վրա։ Օպերատորի հանձնարարությամբ ԱՏ մշակող անձը պարտավոր է պահպանել թիվ 152-ՖԶ օրենքով և սույն Քաղաքականությամբ նախատեսված ԱՏ մշակման սկզբունքներն ու կանոնները։</p>
<h3>6.23. Processing of Personal Data of Citizens of the Russian Federation</h3> <h3>6.23. Ռուսաստանի Դաշնության քաղաքացիների անձնական տվյալների մշակում</h3>
<p>In accordance with Article 2 of Federal Law No. 242-FZ of July 21, 2014, when collecting PD, including through the information and telecommunications network "Internet", the Operator is obliged to ensure the recording, systematization, accumulation, storage, clarification (updating, modification), extraction of PD of citizens of the Russian Federation using databases located on the territory of the Russian Federation, except in cases established by legislation.</p> <p>2014 թվականի հուլիսի 21-ի թիվ 242-ՖԶ դաշնային օրենքի 2-րդ հոդվածի համաձայն` ԱՏ հավաքագրելիս, այդ թվում` «Ինտերնետ» տեղեկատվա-հեռահաղորդակցական ցանցի միջոցով, Օպերատորը պարտավոր է ապահովել Ռուսաստանի Դաշնության քաղաքացիների ԱՏ գրանցումը, համակարգումը, կուտակումը, պահպանումը, ճշգրտումը (թարմացումը, փոփոխումը), քաղումը` օգտագործելով տվյալների բազաներ, որոնք գտնվում են Ռուսաստանի Դաշնության տարածքում, բացառությամբ օրենսդրությամբ սահմանված դեպքերի։</p>
<h3>6.24. Cross-border Transfer of Personal Data</h3> <h3>6.24. Անձնական տվյալների անդրսահմանային փոխանցում</h3>
<p>The Operator is obliged to ensure that the foreign state to whose territory PD is intended to be transferred provides adequate protection of the rights of PD Subjects before carrying out such transfer.</p> <p>Օպերատորը պարտավոր է մինչև նման փոխանցումն իրականացնելը համոզվել, որ այն օտարերկրյա պետությունը, որի տարածք նախատեսվում է փոխանցել ԱՏ, ապահովում է ԱՏ սուբյեկտների իրավունքների բավարար պաշտպանություն։</p>
<p>Cross-border transfer of PD to the territories of foreign states that do not provide adequate protection of the rights of PD Subjects may be carried out in the following cases:</p> <p>ԱՏ անդրսահմանային փոխանցումը այն օտարերկրյա պետությունների տարածք, որոնք չեն ապահովում ԱՏ սուբյեկտների իրավունքների բավարար պաշտպանություն, կարող է իրականացվել հետևյալ դեպքերում.</p>
<ul> <ul>
<li>the written consent of the PD Subject to the cross-border transfer of their PD;</li> <li>ԱՏ սուբյեկտի գրավոր համաձայնությունը իր ԱՏ անդրսահմանային փոխանցման վերաբերյալ,</li>
<li>performance of a contract to which the PD Subject is a party.</li> <li>պայմանագրի կատարում, որի կողմ է ԱՏ սուբյեկտը։</li>
</ul> </ul>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>7. RIGHTS OF THE PERSONAL DATA SUBJECT</h2> <h2>7. ԱՆՁՆԱԿԱՆ ՏՎՅԱԼՆԵՐԻ ՍՈՒԲՅԵԿՏԻ ԻՐԱՎՈՒՆՔՆԵՐԸ</h2>
<h3>7.1. Consent of the Personal Data Subject</h3> <h3>7.1. Անձնական տվյալների սուբյեկտի համաձայնությունը</h3>
<p>The PD Subject makes the decision to provide their PD and gives consent to their Processing freely, of their own will and in their own interest. Consent to PD Processing may be given by the PD Subject or their representative in any form that allows confirmation of the fact of receiving it, unless otherwise established by FZ-152.</p> <p>ԱՏ սուբյեկտը որոշում է տրամադրել իր ԱՏ-ն և տալիս է համաձայնություն դրանց մշակման համար ազատորեն, սեփական կամքով և իր շահերից ելնելով։ ԱՏ մշակման համաձայնությունը կարող է տրվել ԱՏ սուբյեկտի կամ նրա ներկայացուցչի կողմից ցանկացած ձևով, որը թույլ է տալիս հաստատել այն ստանալու փաստը, եթե այլ բան սահմանված չէ թիվ 152-ՖԶ օրենքով։</p>
<p>Processing of PD for the purpose of promoting goods, works, services on the market by making direct contacts with the PD Subject (potential consumer) using communication means, as well as for political campaigning, is permitted only upon prior consent of the PD Subject.</p> <p>ԱՏ մշակումը ապրանքների, աշխատանքների, ծառայությունների շուկայում առաջխաղացման նպատակով` ԱՏ սուբյեկտի (պոտենցիալ սպառողի) հետ կապի միջոցների միջոցով անմիջական կապ հաստատելով, ինչպես նաև քաղաքական քարոզչության համար թույլատրվում է միայն ԱՏ սուբյեկտի նախնական համաձայնությամբ։</p>
<h3>7.2. Rights of the Personal Data Subject</h3> <h3>7.2. Անձնական տվյալների սուբյեկտի իրավունքները</h3>
<p>The PD Subject has the right to obtain from the Operator information regarding the Processing of their PD, if such a right is not restricted in accordance with the legislation of the Russian Federation, including information containing:</p> <p>ԱՏ սուբյեկտն իրավունք ունի Օպերատորից ստանալու տեղեկատվություն իր ԱՏ մշակման վերաբերյալ, եթե նման իրավունքը սահմանափակված չէ Ռուսաստանի Դաշնության օրենսդրությանը համապատասխան, ներառյալ տեղեկատվություն, որը պարունակում է.</p>
<ul> <ul>
<li>confirmation of the fact of PD Processing by the Operator;</li> <li>Օպերատորի կողմից ԱՏ մշակման փաստի հաստատում,</li>
<li>legal grounds and purposes of PD Processing;</li> <li>ԱՏ մշակման իրավական հիմքերն ու նպատակները,</li>
<li>methods of PD Processing used by the Operator;</li> <li>Օպերատորի կողմից օգտագործվող ԱՏ մշակման եղանակները,</li>
<li>information about the name and location of the Operator;</li> <li>Օպերատորի անվանման և գտնվելու վայրի մասին տեղեկատվություն,</li>
<li>list and categories of Processed PD;</li> <li>մշակվող ԱՏ ցանկը և կատեգորիաները,</li>
<li>periods of Personal Data Processing, including storage periods;</li> <li>անձնային տվյալների մշակման ժամկետները, ներառյալ պահման ժամկետները,</li>
<li>the name or surname, first name, patronymic, and address of the person carrying out PD Processing on behalf of the Operator;</li> <li>Օպերատորի հանձնարարությամբ ԱՏ մշակող անձի անունը կամ ազգանունը, անունը, հայրանունը և հասցեն,</li>
<li>clarification of their PD, their Blocking or Destruction if the PD is incomplete, outdated, or inaccurate;</li> <li>իր ԱՏ ճշգրտումը, դրանց արգելափակումը կամ ոչնչացումը, եթե ԱՏ-ն թերի են, հնացած կամ ոչ ճշգրիտ,</li>
<li>other information provided for by FZ-152 or other federal laws of the Russian Federation.</li> <li>այլ տեղեկատվություն, որը նախատեսված է թիվ 152-ՖԶ օրենքով կամ Ռուսաստանի Դաշնության այլ դաշնային օրենքներով։</li>
</ul> </ul>
<p>The Operator is obliged to immediately cease Processing of PD at the request of the PD Subject.</p> <p>Օպերատորը պարտավոր է ԱՏ սուբյեկտի պահանջով անհապաղ դադարեցնել ԱՏ մշակումը։</p>
<p>If the PD Subject believes that the Operator is Processing their PD in violation of the requirements of FZ-152 or otherwise violates their rights and freedoms, the PD Subject has the right to appeal the actions or inaction of the Operator to the authorized body for the protection of PD Subjects' rights or in court.</p> <p>Եթե ԱՏ սուբյեկտը կարծում է, որ Օպերատորը մշակում է իր ԱՏ-ն` խախտելով թիվ 152-ՖԶ օրենքի պահանջները կամ այլ կերպ խախտում է իր իրավունքներն ու ազատությունները, ԱՏ սուբյեկտն իրավունք ունի բողոքարկել Օպերատորի գործողությունները կամ անգործությունը ԱՏ սուբյեկտների իրավունքների պաշտպանության լիազորված մարմնում կամ դատարանում։</p>
<p>The PD Subject has the right to protect their rights and legitimate interests, including compensation for losses and/or moral damages.</p> <p>ԱՏ սուբյեկտն իրավունք ունի պաշտպանելու իր իրավունքներն ու օրինական շահերը, ներառյալ վնասների և/կամ բարոյական վնասի փոխհատուցման պահանջով։</p>
<p>The PD Subject has the right to demand correction of their Personal Data if inaccuracies are found in the PD processed by the Operator, as well as to supplement the PD, including by providing an additional statement.</p> <p>ԱՏ սուբյեկտն իրավունք ունի պահանջել իր Անձնական տվյալների ուղղում, եթե Օպերատորի կողմից մշակվող ԱՏ-ում հայտնաբերվեն անճշտություններ, ինչպես նաև լրացնել ԱՏ-ն, այդ թվում` լրացուցիչ հայտարարություն ներկայացնելու միջոցով։</p>
<p><strong>The PD Subject has the right to withdraw their Consent to Personal Data Processing and to demand the deletion of their PD</strong> from the Operator's systems if the PD is no longer required for the purposes for which it was obtained. You can withdraw your consent to Personal Data Processing at any time by sending an electronic message with an electronic signature to the email address: <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a>, or by sending a written notice to the Operator's registered address.</p> <p><strong>ԱՏ սուբյեկտն իրավունք ունի հետ կանչել իր համաձայնությունը Անձնական տվյալների մշակման վերաբերյալ և պահանջել իր ԱՏ ջնջումը</strong> Օպերատորի համակարգերից, եթե այդ ԱՏ-ն այլևս անհրաժեշտ չեն այն նպատակների համար, որոնց համար ստացվել են։ Դուք կարող եք ցանկացած ժամանակ հետ կանչել ձեր համաձայնությունը Անձնական տվյալների մշակման վերաբերյալ` ուղարկելով էլեկտրոնային հաղորդագրություն էլեկտրոնային ստորագրությամբ հետևյալ հասցեին` <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a>, կամ ուղարկելով գրավոր ծանուցում Օպերատորի գրանցված հասցեին։</p>
<p>The Personal Data Subject has the right to demand the restriction of Processing of their Personal Data for the purposes of the Operator's advertising offers.</p> <p>Անձնական տվյալների սուբյեկտն իրավունք ունի պահանջել սահմանափակել իր Անձնական տվյալների մշակումը Օպերատորի գովազդային առաջարկների նպատակներով։</p>
<p>The Personal Data Subject also has other rights established by FZ-152.</p> <p>Անձնական տվյալների սուբյեկտն ունի նաև այլ իրավունքներ, որոնք սահմանված են թիվ 152-ՖԶ օրենքով։</p>
<p>The Personal Data Subject whose PD is Processed by the Operator has the right at any time to change (update, supplement) the PD they have provided by logging into their personal account in cases where the functionality of the relevant Website allows this.</p> <p>Անձնական տվյալների սուբյեկտը, որի ԱՏ-ն մշակվում են Օպերատորի կողմից, իրավունք ունի ցանկացած պահի փոխել (թարմացնել, լրացնել) իր կողմից տրամադրված ԱՏ-ն` մուտք գործելով իր անձնական հաշիվ այն դեպքերում, երբ համապատասխան Կայքի ֆունկցիոնալությունը դա թույլ է տալիս։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>8. OBLIGATIONS OF THE OPERATOR</h2> <h2>8. ՕՊԵՐԱՏՈՐԻ ՊԱՐՏԱԿԱՆՈՒԹՅՈՒՆՆԵՐԸ</h2>
<p>8.1. In cases established by the legislation of the Russian Federation in the field of Personal Data, the Operator is obliged to provide the Personal Data Subject or their representative, upon request or upon receipt of a request from the Personal Data Subject or their representative, with the information provided for in clause 7.2 of this Policy.</p> <p>8.1. Ռուսաստանի Դաշնության անձնական տվյալների ոլորտի օրենսդրությամբ սահմանված դեպքերում Օպերատորը պարտավոր է Անձնական տվյալների սուբյեկտին կամ նրա ներկայացուցչին, պահանջի դեպքում կամ նրանցից հարցում ստանալու դեպքում, տրամադրել սույն Քաղաքականության 7.2 կետով նախատեսված տեղեկատվությունը։</p>
<p>8.2. When collecting Personal Data, including through the information and telecommunications network "Internet", the Operator ensures the recording, systematization, accumulation, storage, clarification (updating, modification), extraction of Personal Data of citizens of the Russian Federation using databases located in the territory of the Russian Federation, except in cases provided for by Federal Law No. 152-FZ.</p> <p>8.2. Անձնական տվյալներ հավաքագրելիս, այդ թվում` «Ինտերնետ» տեղեկատվա-հեռահաղորդակցական ցանցի միջոցով, Օպերատորը ապահովում է Ռուսաստանի Դաշնության քաղաքացիների անձնական տվյալների գրանցումը, համակարգումը, կուտակումը, պահպանումը, ճշգրտումը (թարմացումը, փոփոխումը), քաղումը` օգտագործելով Ռուսաստանի Դաշնության տարածքում գտնվող տվյալների բազաներ, բացառությամբ թիվ 152-ՖԶ դաշնային օրենքով նախատեսված դեպքերի։</p>
<p>8.3. The Operator bears other obligations established by Federal Law No. 152-FZ.</p> <p>8.3. Օպերատորը կրում է նաև այլ պարտականություններ, որոնք սահմանված են թիվ 152-ՖԶ դաշնային օրենքով։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>9. ENSURING PERSONAL DATA SECURITY</h2> <h2>9. ԱՆՁՆԱԿԱՆ ՏՎՅԱԼՆԵՐԻ ԱՆՎՏԱՆԳՈՒԹՅԱՆ ԱՊԱՀՈՎՈՒՄ</h2>
<p>9.1. The Operator independently determines the composition and list of measures necessary and sufficient to ensure the fulfillment of obligations provided for by Federal Law No. 152-FZ and regulatory legal acts adopted in accordance with it.</p> <p>9.1. Օպերատորն ինքնուրույն որոշում է այն միջոցների կազմն ու ցանկը, որոնք անհրաժեշտ և բավարար են թիվ 152-ՖԶ դաշնային օրենքով և դրա հիման վրա ընդունված նորմատիվ իրավական ակտերով նախատեսված պարտավորությունների կատարման ապահովման համար։</p>
<p>9.2. The security of PD Processed by the Operator is ensured by the implementation of legal, organizational, and technical measures necessary to meet the requirements of federal legislation in the field of PD protection.</p> <p>9.2. Օպերատորի կողմից մշակվող ԱՏ անվտանգությունն ապահովվում է իրավական, կազմակերպական և տեխնիկական միջոցառումների իրականացման միջոցով, որոնք անհրաժեշտ են դաշնային օրենսդրության պահանջներին համապատասխանելու համար ԱՏ պաշտպանության ոլորտում։</p>
<p>9.3. To prevent unauthorized access to PD, the Operator applies the following organizational and technical measures:</p> <p>9.3. ԱՏ նկատմամբ չարտոնված հասանելիությունը կանխելու նպատակով Օպերատորը կիրառում է հետևյալ կազմակերպական և տեխնիկական միջոցները.</p>
<ul> <ul>
<li>appointment of officials responsible for organizing PD Processing and protection;</li> <li>ԱՏ մշակման և պաշտպանության կազմակերպման համար պատասխանատու պաշտոնատար անձանց նշանակում,</li>
<li>limitation of the number of persons authorized to Process PD;</li> <li>ԱՏ մշակելու իրավասություն ունեցող անձանց թվի սահմանափակում,</li>
<li>familiarization of PD Subjects with the requirements of federal legislation and the Operator's regulatory documents on PD Processing and protection;</li> <li>ԱՏ սուբյեկտներին ծանոթացում դաշնային օրենսդրության և Օպերատորի նորմատիվ փաստաթղթերի պահանջներին` ԱՏ մշակման և պաշտպանության մասով,</li>
<li>organization of accounting, storage, and handling of carriers containing information with Personal Data;</li> <li>Անձնական տվյալներ պարունակող կրիչների հաշվառման, պահպանման և շրջանառության կազմակերպում,</li>
<li>development and approval of local acts on PD Processing and protection;</li> <li>ԱՏ մշակման և պաշտպանության վերաբերյալ տեղական ակտերի մշակում և հաստատում,</li>
<li>identification of PD security threats during their Processing, forming threat models based on them;</li> <li>ԱՏ մշակման ընթացքում դրանց անվտանգության սպառնալիքների բացահայտում և դրանց հիման վրա սպառնալիքների մոդելների ձևավորում,</li>
<li>development of a PD protection system based on the threat model;</li> <li>սպառնալիքների մոդելի հիման վրա ԱՏ պաշտպանության համակարգի մշակում,</li>
<li>verification of the readiness and effectiveness of information protection tools;</li> <li>տեղեկատվության պաշտպանության միջոցների պատրաստվածության և արդյունավետության ստուգում,</li>
<li>differentiation of User access to information resources and hardware and software tools for information Processing;</li> <li>Օգտատերերի հասանելիության տարբերակում տեղեկատվական ռեսուրսների և տեղեկատվության մշակման ապարատա-ծրագրային միջոցների նկատմամբ,</li>
<li>registration and recording of User actions in information systems;</li> <li>Օգտատերերի գործողությունների գրանցում և հաշվառում տեղեկատվական համակարգերում,</li>
<li>use of antivirus tools;</li> <li>հակավիրուսային միջոցների օգտագործում,</li>
<li>conducting activities to detect facts of unauthorized access to PD and taking appropriate measures;</li> <li>ԱՏ նկատմամբ չարտոնված հասանելիության փաստերի հայտնաբերման և համապատասխան միջոցների կիրառման միջոցառումների անցկացում,</li>
<li>compliance with conditions that exclude unauthorized access to material carriers of Personal Data;</li> <li>այնպիսի պայմանների պահպանում, որոնք բացառում են անձնական տվյալների նյութական կրիչների նկատմամբ չարտոնված հասանելիությունը,</li>
<li>application, when necessary, of firewalling, intrusion detection, security analysis, and cryptographic information protection tools;</li> <li>անհրաժեշտության դեպքում firewall-ների, ներխուժումների հայտնաբերման, անվտանգության վերլուծության և տեղեկատվության ծածկագրային պաշտպանության միջոցների կիրառում,</li>
<li>organization of access control to the Operator's territory, protection of premises with technical means of Personal Data Processing;</li> <li>Օպերատորի տարածք մուտքի վերահսկման կազմակերպում, ինչպես նաև անձնական տվյալների մշակման տեխնիկական միջոցներով հագեցած տարածքների պաշտպանություն,</li>
<li>monitoring of measures taken to ensure PD security.</li> <li>ԱՏ անվտանգության ապահովմանն ուղղված ձեռնարկված միջոցների վերահսկում։</li>
</ul> </ul>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>10. LIABILITY</h2> <h2>10. ՊԱՏԱՍԽԱՆԱՏՎՈՒԹՅՈՒՆ</h2>
<p>10.1. Persons guilty of violating the norms regulating the Processing of Personal Data and the protection of Personal Data Processed by the Operator bear liability as provided by the legislation of the Russian Federation.</p> <p>10.1. Այն անձինք, որոնք մեղավոր են անձնական տվյալների մշակումը և Օպերատորի կողմից մշակվող անձնական տվյալների պաշտպանությունը կարգավորող նորմերի խախտման մեջ, կրում են պատասխանատվություն` Ռուսաստանի Դաշնության օրենսդրությամբ սահմանված կարգով։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>11. PURPOSES OF PERSONAL INFORMATION PROCESSING</h2> <h2>11. ԱՆՁՆԱԿԱՆ ՏԵՂԵԿԱՏՎՈՒԹՅԱՆ ՄՇԱԿՄԱՆ ՆՊԱՏԱԿՆԵՐԸ</h2>
<p>11.1. The Operator always Processes PD for specific purposes and only the PD that is relevant to the achievement of such purposes. In particular, the Operator processes PD for the following purposes:</p> <p>11.1. Օպերատորը միշտ մշակում է ԱՏ կոնկրետ նպատակներով և միայն այն ԱՏ-ն, որոնք առնչվում են այդ նպատակներին հասնելուն։ Մասնավորապես, Օպերատորը մշակում է ԱՏ հետևյալ նպատակներով.</p>
<ul> <ul>
<li>Providing the User with access to the relevant Website;</li> <li>Օգտատիրոջը համապատասխան Կայքին հասանելիություն տրամադրելու համար,</li>
<li>Providing access to the User's account (personal cabinet) on the relevant Website;</li> <li>համապատասխան Կայքում Օգտատիրոջ հաշվին (անձնական կաբինետին) հասանելիություն տրամադրելու համար,</li>
<li>For the execution of a purchase and sale agreement, a service agreement, or other agreement between the Seller and the User;</li> <li>Վաճառողի և Օգտատիրոջ միջև առուվաճառքի պայմանագրի, ծառայությունների մատուցման պայմանագրի կամ այլ պայմանագրի կատարման համար,</li>
<li>Delivery of goods of the Seller and/or the Operator to Users;</li> <li>Վաճառողի և/կամ Օպերատորի ապրանքների առաքման համար Օգտատերերին,</li>
<li>Assistance in settling claims between the User and the Seller;</li> <li>Օգտատիրոջ և Վաճառողի միջև պահանջների կարգավորման հարցում աջակցելու համար,</li>
<li>Settlement of claims between the User and the Operator;</li> <li>Օգտատիրոջ և Օպերատորի միջև պահանջների կարգավորման համար,</li>
<li>Execution of instructions of the Seller and/or the Operator regarding the collection and transfer of funds received for goods and/or services;</li> <li>Վաճառողի և/կամ Օպերատորի հանձնարարությունների կատարման համար` ապրանքների և/կամ ծառայությունների դիմաց ստացված միջոցների հավաքագրման և փոխանցման մասով,</li>
<li>Improving the quality of User service, market study and analysis, studying User needs;</li> <li>Օգտատերերի սպասարկման որակը բարելավելու, շուկայի ուսումնասիրման և վերլուծության, Օգտատերերի պահանջների ուսումնասիրման համար,</li>
<li>Receiving feedback regarding goods and/or services posted on the Websites;</li> <li>Կայքերում տեղադրված ապրանքների և/կամ ծառայությունների վերաբերյալ հետադարձ կապ ստանալու համար,</li>
<li>Execution of instructions of Sellers and Users in connection with the refusal to accept services and/or return of goods;</li> <li>Վաճառողների և Օգտատերերի հանձնարարությունների կատարման համար` կապված ծառայություններից հրաժարվելու և/կամ ապրանքների վերադարձի հետ,</li>
<li>Posting User reviews of goods and/or services;</li> <li>Օգտատերերի կարծիքները ապրանքների և/կամ ծառայությունների վերաբերյալ հրապարակելու համար,</li>
<li>Analysis of the quality of the service provided by the Operator and improvement of customer service quality;</li> <li>Օպերատորի կողմից մատուցվող ծառայությունների որակը վերլուծելու և հաճախորդների սպասարկման որակը բարելավելու համար,</li>
<li>For the implementation of labor agreements in accordance with the legislation of the Russian Federation;</li> <li>Ռուսաստանի Դաշնության օրենսդրությանը համապատասխան աշխատանքային պայմանագրերի իրականացման համար,</li>
<li>Making a decision on hiring a candidate;</li> <li>թեկնածուի աշխատանքի ընդունման վերաբերյալ որոշում կայացնելու համար,</li>
<li>Fulfillment of obligations arising from the conclusion of contractual relations between the Operator and third parties;</li> <li>Օպերատորի և երրորդ անձանց միջև պայմանագրային հարաբերությունների կնքումից բխող պարտավորությունների կատարման համար,</li>
<li>Providing responses to requests from individuals;</li> <li>ֆիզիկական անձանց հարցումներին պատասխաններ տրամադրելու համար,</li>
<li>Conducting marketing and other research;</li> <li>մարքեթինգային և այլ հետազոտություններ իրականացնելու համար,</li>
<li>Sending advertising and informational messages about goods and/or services of Sellers, as well as goods and/or services of the Operator and advertising personalization;</li> <li>Վաճառողների ապրանքների և/կամ ծառայությունների, ինչպես նաև Օպերատորի ապրանքների և/կամ ծառայությունների վերաբերյալ գովազդային և տեղեկատվական հաղորդագրություններ ուղարկելու և գովազդի անհատականացման համար,</li>
<li>In any other cases directly provided for by the current legislation of the Russian Federation.</li> <li>Ռուսաստանի Դաշնության գործող օրենսդրությամբ ուղղակիորեն նախատեսված այլ դեպքերում։</li>
</ul> </ul>
</section> </section>
<section class="legal-section"> <section class="legal-section">
<h2>12. AUTOMATICALLY COLLECTED INFORMATION</h2> <h2>12. ԻՆՔՆԱԲԵՐԱԲԱՐ ՀԱՎԱՔՎՈՂ ՏԵՂԵԿԱՏՎՈՒԹՅՈՒՆ</h2>
<p>12.1. The Operator has the right to collect and Process, including information that is not PD:</p> <p>12.1. Օպերատորն իրավունք ունի հավաքագրել և մշակել, այդ թվում` տեղեկատվություն, որը չի հանդիսանում ԱՏ.</p>
<ul> <ul>
<li>information about User interests on the Websites based on search queries entered by Website Users in order to provide relevant information to Users when using the Websites;</li> <li>տեղեկատվություն Օգտատերերի հետաքրքրությունների մասին Կայքերում` հիմնված Կայքերի Օգտատերերի կողմից մուտքագրված որոնողական հարցումների վրա, որպեսզի Կայքերից օգտվելիս Օգտատերերին տրամադրվի համապատասխան տեղեկատվություն,</li>
<li>information that forms the system rating of the Seller/Operator: User reviews of the Seller/Operator, information on Order fulfillment, other information;</li> <li>տեղեկատվություն, որը ձևավորում է Վաճառողի/Օպերատորի համակարգային վարկանիշը` Օգտատերերի կարծիքները Վաճառողի/Օպերատորի մասին, Պատվերների կատարման վերաբերյալ տեղեկատվությունը, այլ տեղեկատվություն,</li>
<li>The Operator processes and stores search queries of Website Users for the purpose of generalizing and creating customer statistics on the use of Website sections.</li> <li>Օպերատորը մշակում և պահպանում է Կայքերի Օգտատերերի որոնողական հարցումները` Կայքի բաժինների օգտագործման վերաբերյալ հաճախորդների վիճակագրությունը ընդհանրացնելու և ստեղծելու նպատակով։</li>
</ul> </ul>
<p>12.2. The Operator automatically receives certain types of information obtained in the process of User interaction with the Websites. This refers to technologies and services such as web protocols, cookies, web beacons, as well as third-party applications and tools. At the same time, web beacons, cookies, and other monitoring technologies do not allow the automatic collection of PD.</p> <p>12.2. Օպերատորն ավտոմատ կերպով ստանում է որոշակի տեսակի տեղեկատվություն, որը ձևավորվում է Օգտատերերի կողմից Կայքերի հետ փոխգործակցության ընթացքում։ Խոսքը վերաբերում է այնպիսի տեխնոլոգիաների և ծառայությունների, ինչպիսիք են web protocol-ները, cookie-ները, web beacon-ները, ինչպես նաև երրորդ կողմի հավելվածներն ու գործիքները։ Միևնույն ժամանակ, web beacon-ները, cookie-ները և վերահսկման այլ տեխնոլոգիաները չեն թույլատրում ավտոմատ կերպով հավաքագրել ԱՏ։</p>
<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> <p>12.3. Եթե Օպերատորը կարող է ողջամտորեն կապել սույն բաժնում նշված տեղեկատվությունը կոնկրետ Օգտատիրոջ անձնական հաշվի հետ, ապա այդպիսի տեղեկատվությունը կարող է մշակվել տվյալ Օգտատիրոջ ԱՏ և այլ անձնական տեղեկատվության հետ միասին։</p>
</section> </section>
</div>
</div>

View File

@@ -1,9 +1,11 @@
<div class="legal-page">
<div class="legal-container">
<h1>ПОЛИТИКА В ОТНОШЕНИИ ОБРАБОТКИ ПЕРСОНАЛЬНЫХ ДАННЫХ</h1> <h1>ПОЛИТИКА В ОТНОШЕНИИ ОБРАБОТКИ ПЕРСОНАЛЬНЫХ ДАННЫХ</h1>
<section class="legal-section"> <section class="legal-section">
<h2>1. ОБЩИЕ ПОЛОЖЕНИЯ</h2> <h2>1. ОБЩИЕ ПОЛОЖЕНИЯ</h2>
<p>1.1. Настоящая политика ООО "ИНТ ФИН ЛОГИСТИК" (ИНН 9909697628), именуемого далее как «Оператор», описывает порядок обработки персональных данных и направлена на защиту прав и законных интересов субъектов данных. Документ разработан в соответствии с Федеральным законом №152-ФЗ от 27 июля 2006 года «О персональных данных».</p> <p>1.1. Настоящая политика ООО "ИНТ ФИН ЛОГИСТИК" (ИНН 9909697628) и ООО "ИНТ ФАКТОРИНГ" (ИНН 9909697635), именуемых далее как «Оператор», описывает порядок обработки персональных данных и направлена на защиту прав и законных интересов субъектов данных. Документ разработан в соответствии с Федеральным законом №152-ФЗ от 27 июля 2006 года «О персональных данных».</p>
<p>1.2. Политика определяет порядок и меры обеспечения безопасности обработки персональных данных на сайте <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>, ставя своей задачей защитить права и свободы человека и гражданина, включая право на неприкосновенность частной жизни, личную и семейную тайны.</p> <p>1.2. Политика определяет порядок и меры обеспечения безопасности обработки персональных данных на сайте <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>, ставя своей задачей защитить права и свободы человека и гражданина, включая право на неприкосновенность частной жизни, личную и семейную тайны.</p>
@@ -361,3 +363,5 @@
<p>12.3. Если Оператор может разумно соотнести указанные в настоящем разделе сведения с личным кабинетом конкретного Пользователя, то такие сведения могут обрабатываться совместно с ПДн и иной личной информацией такого Пользователя.</p> <p>12.3. Если Оператор может разумно соотнести указанные в настоящем разделе сведения с личным кабинетом конкретного Пользователя, то такие сведения могут обрабатываться совместно с ПДн и иной личной информацией такого Пользователя.</p>
</section> </section>
</div>
</div>

View File

@@ -1,3 +1,5 @@
<div class="legal-page">
<div class="legal-container">
<h1>PUBLIC OFFER AGREEMENT</h1> <h1>PUBLIC OFFER AGREEMENT</h1>
<section class="legal-section"> <section class="legal-section">
@@ -16,7 +18,7 @@
<li><strong>Buyer</strong> — a registered User who has placed an order through the site.</li> <li><strong>Buyer</strong> — a registered User who has placed an order through the site.</li>
<li><strong>Administrator or Site Owner</strong> — the legal entity INT FIN LOGISTIK LLC, tax identification number (TIN) 9909697628.</li> <li><strong>Administrator or Site Owner</strong> — the legal entities INT FIN LOGISTIK LLC (TIN 9909697628) and INT FACTORING LLC (TIN 9909697635).</li>
<li><strong>Seller</strong> — individuals or legal entities, entrepreneurs offering goods and services on the resource. Sellers bear personal responsibility for the quality, safety and compliance of the product descriptions.</li> <li><strong>Seller</strong> — individuals or legal entities, entrepreneurs offering goods and services on the resource. Sellers bear personal responsibility for the quality, safety and compliance of the product descriptions.</li>
@@ -175,6 +177,62 @@
<p><strong>6.3. User Rights</strong></p> <p><strong>6.3. User Rights</strong></p>
<p>The User has the right to refuse to receive advertising messages by using the appropriate tool on the site or by sending a request by email to <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a> or by letter to the official address of the Site Owner.</p> <p>The User has the right to refuse to receive advertising messages by using the appropriate tool on the site or by sending a request by email to <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a> or by letter to the official address of the Site Owner.</p>
<p><strong>6.4. Prohibited Goods for Sale on the Site</strong></p>
<p>The following categories of goods and services are prohibited from being listed and sold on the site:</p>
<ul>
<li>6.4.1. Weapons, ammunition, military equipment, spare parts, components and instruments thereof, explosives, detonation devices, all types of rocket fuel, as well as special materials and special equipment for their production, special equipment of paramilitary organizations, and technical documentation for their production and operation.</li>
<li>6.4.2. Rocket and space complexes, military communications and control systems, and technical documentation for their production and operation.</li>
<li>6.4.3. Chemical warfare agents, protective equipment against them, and technical documentation for their production and use.</li>
<li>6.4.4. Results of research and design work, as well as fundamental exploratory research on the creation of weapons and military equipment.</li>
<li>6.4.5. Services, works and materials related to military service and paramilitary activities.</li>
<li>6.4.6. Any weapons, including hunting, civilian and other types, as well as components thereof, knives (except kitchen, penknives and stationery knives).</li>
<li>6.4.7. Radioactive substances and isotopes, uranium and other fissile materials and products made from them.</li>
<li>6.4.8. Radioactive material waste.</li>
<li>6.4.9. Precious and rare-earth metals, precious stones, as well as waste containing precious and rare-earth metals and precious stones.</li>
<li>6.4.10. X-ray equipment, instruments and equipment using radioactive substances and isotopes.</li>
<li>6.4.11. Poisons, narcotic drugs and psychotropic substances, and their precursors.</li>
<li>6.4.12. Ethyl alcohol, alcoholic beverages.</li>
<li>6.4.13. Prescription medicines, as well as narcotic, psychotropic and alcohol-containing (with a volume fraction of ethyl alcohol exceeding 25%) medicines and spirit-based balms.</li>
<li>6.4.14. Medicinal raw materials obtained from reindeer husbandry (antlers and endocrine raw materials).</li>
<li>6.4.15. Tobacco products or vaping products.</li>
<li>6.4.16. Encryption equipment and technical documentation for its production and use.</li>
<li>6.4.17. Counterfeit banknotes.</li>
<li>6.4.18. Foreign currency and other currency valuables, coins and banknotes of the Russian Federation in circulation.</li>
<li>6.4.19. Radio-electronic and special technical means designed for covert acquisition of information, as well as high-frequency devices consisting of one or more radio transmitting devices and (or) their combinations and auxiliary equipment designed for transmitting and receiving radio waves at a frequency above 8 GHz.</li>
<li>6.4.20. Materials and services that violate the privacy of private life, encroach on the honor, dignity and business reputation of citizens and legal entities, as well as containing state, banking, commercial and other secrets.</li>
<li>6.4.21. State awards of the Russian Federation, RSFSR, USSR, as well as copies thereof.</li>
<li>6.4.22. State identity documents, badges, passes, permits, certificates, travel documents and licenses, as well as other documents granting rights or exempting from rights or obligations, blank forms for these documents, as well as services for obtaining them.</li>
<li>6.4.23. Cultural heritage objects of the peoples of the Russian Federation, as well as archaeological heritage objects.</li>
<li>6.4.24. Human organs and tissues, as well as donor services.</li>
<li>6.4.25. Animals and plants listed in the Red Book of the Russian Federation and the Red Books of the constituent entities of the Russian Federation, parts and organs of animals listed in the Red Book of the Russian Federation and the Red Books of the constituent entities of the Russian Federation, as well as animals and plants protected by international treaties of the Russian Federation.</li>
<li>6.4.26. Skins and products made from skins of rare and endangered animal species in accordance with the current legislation of the Russian Federation.</li>
<li>6.4.27. Fishing nets, materials for their manufacture, as well as services for their manufacture, electric fishing rods and traps prohibited for sale on the territory of the Russian Federation.</li>
<li>6.4.28. Extremist materials, materials calling for mass riots, terrorist and extremist activities, participation in mass public events, incitement of interethnic and interfaith discord.</li>
<li>6.4.29. Items with Nazi symbols or symbols of organizations banned in the Russian Federation.</li>
<li>6.4.30. Counterfeit or stolen products or property.</li>
<li>6.4.31. Databases, including those containing personal data, that may facilitate unauthorized mailings.</li>
<li>6.4.32. Materials transmitted exclusively virtually and not recorded on any tangible medium (ideas, methods, principles, etc.).</li>
<li>6.4.33. Gaming equipment used for gambling, lottery equipment, provision of services for accepting bets for participation in online gambling, acceptance of payments for lottery tickets, receipts and other documents certifying the right to participate in a lottery, as well as the sale of virtual currency.</li>
<li>6.4.34. Vehicle documents, state license plates for vehicles.</li>
<li>6.4.35. Goods whose circulation violates the intellectual property rights of third parties (including patents, trademarks, copyrights, etc.).</li>
<li>6.4.36. Investment services, transactions with monetary funds and cryptocurrencies, as well as goods and services whose acquisition or use is guaranteed to generate earnings or profit.</li>
<li>6.4.37. Goods and services sold by multilevel network marketing organizations whose activities are based on the creation of a network of independent distributors or sales agents.</li>
<li>6.4.38. Services and (or) work of an intimate, erotic or sexual nature, as well as pornographic or erotic materials.</li>
<li>6.4.39. Goods or services whose use may be aimed at violating the current legislation of the Russian Federation.</li>
<li>6.4.40. Non-existent goods or services, as well as goods or services that have no consumer value.</li>
<li>6.4.41. Transcendental services and alternative medicine services.</li>
<li>6.4.42. Services for replacing licensed software or disrupting the operation of technical protection means installed by the rights holder on phones, smartphones, laptops, navigators, personal computers, etc.</li>
<li>6.4.43. Other goods or services whose circulation is prohibited or restricted under the legislation of the Russian Federation, as well as those capable of having a negative impact on the business reputation of international payment systems.</li>
<li>6.4.44. Injectable preparations and solutions, as well as substances used for their manufacture.</li>
<li>6.4.45. Services, works and materials related to the activities of occult organizations and sects.</li>
<li>6.4.46. Goods and services sold by companies organized in the form of financial pyramids.</li>
<li>6.4.47. Antiques.</li>
<li>6.4.48. Biologically active additives (dietary supplements). Sale of dietary supplements is permitted only through pharmacy institutions (pharmacies, pharmacy stores, pharmacy kiosks), specialized stores with dietary products, and grocery stores with special departments and sections.</li>
<li>6.4.49. Custom term papers and diplomas.</li>
<li>6.4.50. Anonymous work (drug couriers, etc.).</li>
<li>6.4.51. Copies and replicas of original goods.</li>
</ul>
</section> </section>
<section class="legal-section"> <section class="legal-section">
@@ -459,3 +517,5 @@
<p><strong>16.8. Response to Violations</strong></p> <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> <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> </section>
</div>
</div>

View File

@@ -1 +1,521 @@
<h1>Հdelays DELAYS ՀԱՄDELAYS</h1> <div class="legal-page">
<div class="legal-container">
<h1>ՀԱՆՐԱՅԻՆ ՕՖԵՐՏԱՅԻ ՀԱՄԱՁԱՅՆԱԳԻՐ</h1>
<section class="legal-section">
<h2>Հիմնական հասկացություններ</h2>
<p>Սույն փաստաթղթում ներկայացվում են հանրային օֆերտայի համաձայնագրում կիրառվող հիմնական տերմիններն ու բացատրությունները.</p>
<ul>
<li><strong>Մարկետփլեյս, ինտերնետ-կայք կամ Կայք</strong> - տեխնիկական և ծրագրային միջոցների համալիր, որը նախատեսված է https://dexarmarket.ru հասցեում գործող վեբ-ռեսուրսի աշխատանքի համար: Կայքը պատկանում է սեփականատիրոջը և ծառայում է որպես հարթակ անկախ վաճառողների և գնորդների փոխգործակցության կազմակերպման համար: Կայքի սեփականատերն ինքնուրույն չի զբաղվում ապրանքների և ծառայությունների վաճառքով, այլ տրամադրում է տարածք հայտարարությունների տեղադրման և գործարքների կնքման համար:</li>
<li><strong>Օգտատեր</strong> - ֆիզիկական անձ, որը օգտվում է կայքից, գրանցվում է և ընդունում է սույն համաձայնագրի պայմանները:</li>
<li><strong>Անձնական հաշիվ</strong> - Օգտատիրոջ անհատական էջը, որը պաշտպանված է անհատական գրանցման տվյալներով (մուտքանուն և գաղտնաբառ): Մուտքանուն կարող է լինել էլեկտրոնային փոստի հասցեն կամ բջջային հեռախոսահամարը, որն օգտագործվում է նույնականացման համար:</li>
<li><strong>Հաշվառման տվյալներ</strong> - օգտատիրոջ անվան (մուտքանուն) և գաղտնաբառի համակցություն, որը ստեղծվում է գրանցման ընթացքում:</li>
<li><strong>Գնորդ</strong> - գրանցված Օգտատեր, որը պատվեր է կատարել կայքի միջոցով:</li>
<li><strong>Ադմինիստրատոր կամ Կայքի սեփականատեր</strong> - «ԻՆՏ ՖԻՆ ԼՈԳԻՍՏԻԿ» ՍՊԸ (ՀՎՀՀ 9909697628) և «ԻՆՏ ՖԱԿՏՈՐԻՆԳ» ՍՊԸ (ՀՎՀՀ 9909697635):</li>
<li><strong>Վաճառող կամ Սելլեր</strong> - ֆիզիկական կամ իրավաբանական անձինք, ձեռնարկատերեր, որոնք ռեսուրսում առաջարկում են ապրանքներ և ծառայություններ: Վաճառողները անձամբ պատասխանատվություն են կրում ապրանքների որակի, անվտանգության և նկարագրությանը համապատասխանության համար:</li>
<li><strong>Կայքի բովանդակություն</strong> - ռեսուրսում տեղակայված նյութերի ամբողջություն, ներառյալ դիզայնը, տեքստերը, գրաֆիկան, տեսանյութերը, աուդիոնյութերը և այլ օբյեկտներ:</li>
<li><strong>Ապրանք</strong> - նյութական և թվային արտադրանք, ինչպես նաև ծառայություններ, որոնք առաջարկվում են վաճառքի կայքի միջոցով:</li>
<li><strong>Թվային ապրանք</strong> - ապրանքներ, որոնք տարածվում են էլեկտրոնային ձևով, օրինակ` ծրագրային ապահովում, առցանց դասընթացների բաժանորդագրություն, վիրտուալ ակտիվներ, երաժշտական թրեքեր, էլեկտրոնային գրքեր և նմանատիպ արտադրանք:</li>
<li><strong>Պատվեր</strong> - Գնորդի կողմից ապրանքներ կամ ծառայություններ ձեռք բերելու ձևակերպված հայտ:</li>
<li><strong>Սերվիս</strong> - ռեսուրսում ինտեգրված հատուկ ծրագրային ապահովում, որը հնարավորություն է տալիս օգտվել կայքի ֆունկցիոնալությունից:</li>
<li><strong>Կայքի սեփականատիրոջ պրոդուկտներ</strong> - տեղեկատվական և ուղեկցող ծառայություններ, որոնք տրամադրվում են կայքի սեփականատիրոջ կողմից:</li>
</ul>
</section>
<section class="legal-section">
<h2>1. Ընդհանուր դրույթներ</h2>
<p>1.1. Սույն փաստաթուղթը սահմանում է ադմինիստրատորին պատկանող ռեսուրսների օգտագործման կարգը, ներառյալ կայքը, բջջային տարբերակները և հավելվածները, որոնք կառավարվում են Ադմինիստրացիայի կողմից:</p>
<p>1.2. Փաստաթուղթը հանդիսանում է Օգտատիրոջ և ռեսուրսի Սեփականատիրոջ միջև պարտադիր համաձայնագիր:</p>
<p>1.3. Սույն համաձայնագիրը և կայքում տեղադրված պրոդուկտների մասին տեղեկատվությունը համապատասխանում են հանրային օֆերտայի սահմանմանը` համաձայն ՌԴ քաղաքացիական օրենսգրքի 435-րդ հոդվածի և 437-րդ հոդվածի 2-րդ կետի:</p>
<p>1.4. Օգտատերը համաձայնում է պայմանագրի պայմաններին ինքնաբերաբար` սկսած կայք առաջին մուտքից, գրանցումից կամ առանց նույնականացման պատվերի ձևակերպումից, «Պատվիրել», «Ուղարկել հաղորդագրություն» կամ «Գնել» կոճակները սեղմելուց:</p>
<p>1.5. Օֆերտայի ակցեպտով համաձայնագրի կնքումը կողմերի ստորագրություն չի պահանջում և ճանաչվում է որպես վավեր էլեկտրոնային ձևով:</p>
<p>1.6. Կայքի օգտագործումը ենթադրում է համաձայնություն համաձայնագրի պայմաններին, որը ուժի մեջ է մտնում օգտատիրոջ կողմից համաձայնությունն արտահայտելուց անմիջապես հետո:</p>
<p>1.7. Եթե Օգտատերը համաձայն չէ պայմաններին, նա պարտավորվում է անհապաղ դադարեցնել ռեսուրսից օգտվելը:</p>
<p>1.8. Կայքի օգտագործման լրացուցիչ կարգավորումը իրականացվում է <a [routerLink]="'/privacy-policy' | langRoute">Անձնական տվյալների մշակման քաղաքականությամբ</a>:</p>
<p>1.9. Համաձայնագրում փոփոխություններ կարող են կատարվել Սեփականատիրոջ կողմից առանց նախնական ծանուցման և դառնում են պարտադիր փոփոխությունների հրապարակման պահից:</p>
<p>1.10. Գովազդային արշավների ընթացքում կարող են սահմանվել պատվերի ձևակերպման, վերադարձի կամ ապրանքների փոխանակման հատուկ պայմաններ:</p>
<p>1.14. Օֆերտան նախատեսված է ռեսուրսի բոլոր օգտատերերի համար, ներառյալ իրավաբանական անձինք և անհատ ձեռնարկատերերը:</p>
</section>
<section class="legal-section">
<h2>2. Համաձայնագրի առարկան</h2>
<p>2.1. Համաձայնագրի նպատակը Օգտատերերին հնարավորություն տալն է ձեռք բերել ռեսուրսում ներկայացված ապրանքներ և ծառայություններ, ինչպես նաև օգտվել Կայքի սեփականատիրոջ կողմից տրամադրվող պրոդուկտներից:</p>
<p>2.2. Համաձայնագիրը կարգավորում է կայքի և Սեփականատիրոջ կողմից տրամադրվող գործառույթների օգտագործման կարգը:</p>
<p>2.3. Փաստաթղթի գործողությունը տարածվում է կայքում առկա ապրանքների, ծառայությունների և պրոդուկտների բոլոր տեսակների վրա:</p>
</section>
<section class="legal-section">
<h2>3. Ապրանքների վաճառքի և ծառայությունների մատուցման պայմաններ</h2>
<p><strong>3.1. Վաճառքի պայմանների ընդունում</strong></p>
<p>Օգտատերը, մարկետփլեյսի միջոցով պատվերներ կատարելով, արտահայտում է լիակատար համաձայնություն ապրանքների վաճառքի և ծառայությունների մատուցման պայմաններին, որոնք սահմանված են սույն համաձայնագրով:</p>
<p><strong>3.2. Պայմանագրերի կնքում</strong></p>
<p>Մանրածախ առուվաճառքի պայմանագիրը կամ ծառայությունների մատուցման պայմանագիրը կնքվում է անմիջապես Վաճառողի և Գնորդի միջև` Վաճառողի կողմից վճարումը հաստատող դրամարկղային կամ ապրանքային կտրոններ տրամադրելու պահից: Մարկետփլեյսը կատարում է տեղեկատվական միջնորդի դեր, տրամադրում է ենթակառուցվածք գործարքների կատարման համար, բայց չի հանդիսանում այդ պայմանագրի կողմ: Պայմանագրի կատարման, ապրանքների և ծառայությունների որակի համար պատասխանատվությունը կրում է Վաճառողը:</p>
<p><strong>3.3. Համաձայնություն կոնտակտների մշակմանը</strong></p>
<p>Օգտատերը համաձայնություն է տալիս իր կոնտակտային տվյալների (էլեկտրոնային փոստի հասցե, հեռախոսահամար) օգտագործմանը կայքի ադմինիստրացիայի, Վաճառողի և ներգրավված երրորդ կողմերի կողմից Գնորդի նկատմամբ պարտավորությունների կատարման համար, ներառյալ գովազդային և այլ տեղեկատվության ուղարկումը:</p>
<p><strong>3.4. Երրորդ անձանց ներգրավում</strong></p>
<p>Օգտատերը համաձայնում է, որ Վաճառողը կարող է երրորդ անձանց ներգրավել առուվաճառքի կամ ծառայությունների մատուցման պայմանագրի կատարման համար, միաժամանակ Վաճառողի պատասխանատվությունը պարտավորությունների կատարման համար պահպանվում է:</p>
<p><strong>3.5. Պայմանագրերից բխող իրավունքներ և պարտավորություններ</strong></p>
<p>Առուվաճառքի պայմանագրերից բխող իրավունքներն ու պարտավորությունները ծագում են անմիջապես Վաճառողի մոտ: Օգտատերը գիտակցում է, որ Կայքի սեփականատերը կատարում է մարկետփլեյսի օպերատորի դեր և պատասխանատվություն չի կրում Վաճառողների գործողությունների, ապրանքների որակի, Վաճառողի կողմից գործարքներ իրականացնելու իրավական հիմքերի կամ պարտավորությունների փաստացի կատարման համար:</p>
<p><strong>3.6. Կայքի սեփականատիրոջ գործառույթները</strong></p>
<p>Կայքի սեփականատերը տրամադրում է տեղեկատվական և տեխնիկական ուղեկցում. համակարգում է Գնորդի և Վաճառողի միջև փոխգործակցությունը, փոխանցում է պատվերի մասին տեղեկատվությունը, կարող է աջակցել վեճերի կարգավորմանը: Այնուամենայնիվ, պայմանագրի կատարման պատասխանատվությունը մնում է Վաճառողի վրա:</p>
<p><strong>3.7. Վաճառողի կողմից իրավունքների փոխանցում</strong></p>
<p>Գնորդը տեղեկացված է և համաձայնում է, որ Վաճառողը կարող է զիջել կամ փոխանցել իր իրավունքներն ու պարտավորությունները երրորդ անձանց:</p>
<p><strong>3.8. Օրենքների կիրառություն</strong></p>
<p>Օգտատիրոջ և Վաճառողի միջև հարաբերությունները կարգավորվում են «Սպառողների իրավունքների պաշտպանության մասին» դաշնային օրենքի (թիվ 2300-1, 07 փետրվարի 1992 թ.) և ՌԴ քաղաքացիական օրենսգրքի դրույթներով:</p>
<p><strong>3.10. Գովազդային հաղորդագրություններ</strong></p>
<p>Օգտատերը համաձայնություն է տալիս գովազդային հաղորդագրություններ ստանալուն` «Գովազդի մասին» դաշնային օրենքին համապատասխան:</p>
</section>
<section class="legal-section">
<h2>4. Կայքում գրանցում և Օգտատիրոջ անձնական հաշիվ</h2>
<p><strong>4.1. Գրանցման ընթացակարգեր</strong></p>
<p>Օգտատիրոջը առաջարկվում է գրանցվել կայքում` առանձին գործառույթներից և սերվիսներից լիարժեք օգտվելու համար: Թեև գրանցումը պարտադիր չէ պատվեր ձևակերպելու համար, այն ստեղծում է լրացուցիչ հնարավորություններ, ներառյալ մուտք դեպի Անձնական հաշիվ:</p>
<p><strong>4.2. Օգտատիրոջ պարտավորությունները գրանցման ժամանակ</strong></p>
<p>Գրանցվելիս Օգտատերը պարտավորվում է տրամադրել ճշգրիտ և արդիական տեղեկատվություն` լրացնելով գրանցման հայտը: Անհրաժեշտ է պահպանել տվյալների արդիականությունը:</p>
<p><strong>4.3. Օգտատիրոջ նույնականացում</strong></p>
<p>Անհատական մուտքանվան և գաղտնաբառի օգտագործմամբ կատարված ցանկացած գործողություն համարվում է հենց Օգտատիրոջ գործողություն, քանի դեռ հակառակը չի ապացուցվել:</p>
<p><strong>4.4. Հաշվառման տվյալների գաղտնիություն</strong></p>
<p>Օգտատերը պարտավորվում է գաղտնի պահել գրանցման ընթացքում տրամադրված մուտքանունն ու գաղտնաբառը: Անվտանգության խախտման կասկածի դեպքում անհրաժեշտ է անհապաղ տեղեկացնել կայքի ադմինիստրացիային` ուղարկելով նամակ <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a> հասցեին:</p>
<p><strong>4.7. Տվյալների հաստատում</strong></p>
<p>Կայքի սեփականատերը իրավունք ունի ցանկացած պահի պահանջել տրամադրված տեղեկատվության հաստատում:</p>
<p><strong>4.11. Աշխատանքային սեսիայի ավարտ</strong></p>
<p>Օգտատերը պարտավոր է ինքնուրույն ավարտել աշխատանքը Անձնական հաշվում («Ելք»)` լքելով կայքը, իր հաշվի անվտանգությունն ապահովելու համար:</p>
</section>
<section class="legal-section">
<h2>5. Կայքի սեփականատիրոջ իրավունքներն ու պարտավորությունները</h2>
<p><strong>5.1. Կայքի սեփականատիրոջ իրավունքները</strong></p>
<p>Կայքի սեփականատերը օժտված է հետևյալ լիազորություններով.</p>
<ul>
<li>Սահմանել ռեսուրսի օգտագործման սահմանափակումներ բոլոր կամ առանձին խմբերի օգտատերերի համար:</li>
<li>Օգտատերերին տեղեկություններ ուղարկել կայքում նորարարությունների և փոփոխությունների մասին:</li>
<li>Առանց Գնորդի նախնական համաձայնության փոխել ապրանքների մատակարարին և/կամ առաքող ընկերությանը:</li>
<li>Միակողմանի կարգով փոխել անցկացվող ակցիաների պայմանները:</li>
<li>Սահմանափակել այն Օգտատերերի գործողությունները, որոնք ռիսկեր են ստեղծում կայքի աշխատունակության համար:</li>
<li>Տեխնիկական աշխատանքներ իրականացնել առանց օգտատերերին նախապես զգուշացնելու:</li>
<li>Կայքում հավաքված վիճակագրությունն օգտագործել սեփական նպատակների համար:</li>
<li>Ցանկացած պահի փոխել ապրանքների և ծառայությունների ցանկը, գները և պայմանները:</li>
<li>Մերժել սպասարկումը այն Օգտատերերին, որոնց նկատմամբ կան իրավախախտ գործողությունների կասկածներ:</li>
</ul>
<p><strong>5.2. Կայքի սեփականատիրոջ պարտավորությունները</strong></p>
<p>Կայքի սեփականատիրոջ հիմնական պարտականությունն է Օգտատերերին մատուցել սույն համաձայնագրի 2.1 կետով նախատեսված ծառայությունները:</p>
</section>
<section class="legal-section">
<h2>6. Օգտատիրոջ իրավունքներն ու պարտավորությունները</h2>
<p><strong>6.1. Օգտատիրոջ պարտավորությունները</strong></p>
<p>Օգտատերը պարտավորվում է պահպանել մի շարք կարևոր պարտականություններ.</p>
<ul>
<li>Ծանոթանալ համաձայնագրի տեքստին մինչև առուվաճառքի կամ ծառայությունների մատուցման պայմանագիր կնքելը:</li>
<li>Պահպանել էթիկական վարքագիծ կարծիքներ գրելիս և կայքի անձնակազմի հետ շփվելիս:</li>
<li>Շփվել քաղաքավարի և հարգալից աշխատակիցների և գործընկերների հետ:</li>
<li>Չթույլատրել հայհոյանքների, կոպտության և վիրավորանքների տարածում:</li>
<li>Չստեղծել խոչընդոտներ կայքի և դրա սերվիսների բնականոն աշխատանքի համար:</li>
<li>Չզբաղվել վիրուսների, վնասակար ծրագրերի և այլ վտանգավոր ֆայլերի բեռնմամբ:</li>
<li>Առանց Ադմինիստրացիայի թույլտվության չօգտագործել տեղեկատվություն հավաքելու արգելված ավտոմատացված ծրագրեր:</li>
<li>Չփորձել մուտք գործել այլ անձանց հաշվառման տվյալներին:</li>
<li>Օրինական կերպով օգտագործել կայքի բովանդակությունը և խուսափել նյութերի անօրինական պատճենումից:</li>
<li>Գրանցվել միայն սեփական անունից և չներկայանալ որպես այլ անձ:</li>
<li>Ժամանակին վճարել պատվիրված ապրանքների և ծառայությունների համար` համաձայն համաձայնագրի:</li>
<li>Ձեռք բերված ապրանքներն օգտագործել բացառապես անձնական կարիքների համար` առանց առևտրային նպատակների:</li>
<li>Պահպանել ռեսուրսի տեղեկատվական անվտանգությունը` զերծ մնալով կայքը կոտրելու կամ դրա ամբողջականությունը խախտելու փորձերից:</li>
</ul>
<p><strong>6.2. Օգտատիրոջ արգելված գործողությունները</strong></p>
<p>Օգտատիրոջը խստիվ արգելվում է.</p>
<ul>
<li>Բեռնել և հրապարակել օրենքին հակասող, վիրուսային ծրագրեր, կեղծ տեղեկատվություն կամ նվաստացուցիչ քննադատություն պարունակող բովանդակություն:</li>
<li>Խախտել այլ օգտատերերի իրավունքները և վնաս պատճառել անչափահաս քաղաքացիներին:</li>
<li>Ցանկացած վարքագիծ, որը խախտում է ռուսական օրենսդրությունը, ներառյալ միջազգային իրավունքի նորմերը:</li>
</ul>
<p><strong>6.3. Օգտատիրոջ իրավունքները</strong></p>
<p>Օգտատերն իրավունք ունի հրաժարվել գովազդային հաղորդագրություններ ստանալուց` օգտվելով կայքում համապատասխան գործիքից կամ ուղարկելով հարցում <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a> էլեկտրոնային փոստին կամ նամակով Կայքի սեփականատիրոջ պաշտոնական հասցեին:</p>
<p><strong>6.4. Կայքում վաճառքի համար արգելված ապրանքներ</strong></p>
<p>Կայքում արգելվում է տեղադրել և վաճառել հետևյալ կատեգորիայի ապրանքներն ու ծառայությունները.</p>
<ul>
<li>6.4.1. Զենք, զինամթերք, ռազմական տեխնիկա, պահեստամասեր, համալրիչներ և սարքեր դրանց համար, պայթուցիկ նյութեր, պայթեցման միջոցներ, հրթիռային վառելիքի բոլոր տեսակները, ինչպես նաև հատուկ նյութեր և հատուկ սարքավորումներ դրանց արտադրության համար, ռազմականացված կազմակերպությունների հատուկ հանդերձանք և դրանց արտադրության ու շահագործման նորմատիվ-տեխնիկական արտադրանք:</li>
<li>6.4.2. Հրթիռատիեզերական համալիրներ, ռազմական նշանակության կապի և կառավարման համակարգեր, ինչպես նաև դրանց արտադրության և շահագործման նորմատիվ-տեխնիկական փաստաթղթեր:</li>
<li>6.4.3. Մարտական թունավոր նյութեր, դրանցից պաշտպանվելու միջոցներ և դրանց արտադրության ու օգտագործման նորմատիվ-տեխնիկական փաստաթղթեր:</li>
<li>6.4.4. Զենքի և ռազմական տեխնիկայի ստեղծմանն ուղղված գիտահետազոտական և նախագծային աշխատանքների արդյունքներ, ինչպես նաև հիմնարար հետազոտություններ:</li>
<li>6.4.5. Ծառայություններ, աշխատանքներ և նյութեր, որոնք կապված են զինվորական ծառայության և ռազմականացված գործունեության իրականացման հետ:</li>
<li>6.4.6. Ցանկացած զենք, ներառյալ որսորդական, քաղաքացիական և այլ տեսակներ, ինչպես նաև դրա համալրիչներ, դանակներ (բացառությամբ խոհանոցային, գրպանի և գրասենյակային դանակների):</li>
<li>6.4.7. Ռադիոակտիվ նյութեր և իզոտոպներ, ուրան և այլ բաժանվող նյութեր ու դրանցից պատրաստված արտադրանք:</li>
<li>6.4.8. Ռադիոակտիվ նյութերի թափոններ:</li>
<li>6.4.9. Թանկարժեք և հազվագյուտ հողային մետաղներ, թանկարժեք քարեր, ինչպես նաև թանկարժեք և հազվագյուտ հողային մետաղներ ու թանկարժեք քարեր պարունակող թափոններ:</li>
<li>6.4.10. Ռենտգենյան սարքավորում, սարքեր և սարքավորումներ, որոնք օգտագործում են ռադիոակտիվ նյութեր և իզոտոպներ:</li>
<li>6.4.11. Թույներ, թմրամիջոցներ և հոգեմետ նյութեր, ինչպես նաև դրանց պրեկուրսորներ:</li>
<li>6.4.12. Էթիլային սպիրտ, ալկոհոլային խմիչքներ:</li>
<li>6.4.13. Դեղատոմսով տրվող դեղամիջոցներ, ինչպես նաև թմրամիջոց պարունակող, հոգեմետ և սպիրտ պարունակող (էթիլային սպիրտի ծավալային բաժինը 25%-ից ավելի) դեղամիջոցներ և սպիրտային հիմքով բալզամներ:</li>
<li>6.4.14. Հյուսիսային եղջերուաբուծությունից ստացվող դեղագործական հումք (եղջյուրներ և էնդոկրին հումք):</li>
<li>6.4.15. Ծխախոտային արտադրանք կամ վեյփինգի ապրանքներ:</li>
<li>6.4.16. Գաղտնագրման տեխնիկա և դրա արտադրության ու օգտագործման նորմատիվ-տեխնիկական փաստաթղթեր:</li>
<li>6.4.17. Կեղծ դրամանիշներ:</li>
<li>6.4.18. Արտարժույթ և այլ արժութային արժեքներ, ինչպես նաև շրջանառության մեջ գտնվող Ռուսաստանի Դաշնության մետաղադրամներ և թղթադրամներ:</li>
<li>6.4.19. Ռադիոէլեկտրոնային և հատուկ տեխնիկական միջոցներ, որոնք նախատեսված են տեղեկատվությունը գաղտնի ստանալու համար, ինչպես նաև բարձր հաճախականության սարքեր, որոնք կազմված են մեկ կամ մի քանի ռադիոհաղորդիչ սարքերից և (կամ) դրանց համակցություններից ու օժանդակ սարքավորումից, և նախատեսված են 8 ԳՀց-ից բարձր հաճախականությամբ ռադիոալիքների փոխանցման և ընդունման համար:</li>
<li>6.4.20. Նյութեր և ծառայություններ, որոնք խախտում են անձնական կյանքի գաղտնիությունը, ոտնահարում են քաղաքացիների և իրավաբանական անձանց պատիվը, արժանապատվությունն ու գործարար համբավը, ինչպես նաև պարունակում են պետական, բանկային, առևտրային կամ այլ գաղտնիքներ:</li>
<li>6.4.21. ՌԴ, ՌԽՖՍՀ, ԽՍՀՄ պետական պարգևներ, ինչպես նաև դրանց պատճենները:</li>
<li>6.4.22. Պետական ինքնությունը հաստատող փաստաթղթեր, նշաններ, անցագրեր, թույլտվություններ, հավաստագրեր, ճանապարհորդական փաստաթղթեր և լիցենզիաներ, ինչպես նաև այլ փաստաթղթեր, որոնք իրավունքներ են տրամադրում կամ ազատում են իրավունքներից կամ պարտականություններից, այդ փաստաթղթերի բլանկներ, ինչպես նաև դրանք ստանալու ծառայություններ:</li>
<li>6.4.23. Ռուսաստանի Դաշնության ժողովուրդների մշակութային ժառանգության օբյեկտներ, ինչպես նաև հնագիտական ժառանգության օբյեկտներ:</li>
<li>6.4.24. Մարդու օրգաններ և հյուսվածքներ, ինչպես նաև դոնորական ծառայություններ:</li>
<li>6.4.25. Կենդանիներ և բույսեր, որոնք ներառված են Ռուսաստանի Դաշնության և դրա սուբյեկտների Կարմիր գրքերում, այդ կենդանիների մասեր և օրգաններ, ինչպես նաև կենդանիներ և բույսեր, որոնք պաշտպանվում են Ռուսաստանի Դաշնության միջազգային պայմանագրերով:</li>
<li>6.4.26. Հազվագյուտ և վերացման վտանգի տակ գտնվող կենդանիների տեսակների կաշվից պատրաստված կաշիներ և արտադրանք` Ռուսաստանի Դաշնության գործող օրենսդրությանը համապատասխան:</li>
<li>6.4.27. Ձկնորսական ցանցեր, դրանց պատրաստման նյութեր, ինչպես նաև դրանց պատրաստման ծառայություններ, էլեկտրաձկնորսական սարքեր և թակարդներ, որոնց վաճառքը արգելված է Ռուսաստանի Դաշնության տարածքում:</li>
<li>6.4.28. Ծայրահեղական նյութեր, նյութեր, որոնք կոչ են անում զանգվածային անկարգությունների, ահաբեկչական և ծայրահեղական գործունեության, զանգվածային հանրային միջոցառումներին մասնակցելու, ազգամիջյան և միջկրոնական թշնամանքի հրահրման:</li>
<li>6.4.29. Նացիստական խորհրդանիշներ կամ Ռուսաստանի Դաշնությունում արգելված կազմակերպությունների խորհրդանիշներ պարունակող առարկաներ:</li>
<li>6.4.30. Կոնտրաֆակտ կամ գողացված արտադրանք կամ գույք:</li>
<li>6.4.31. Տվյալների բազաներ, ներառյալ անձնական տվյալներ պարունակող բազաները, որոնք կարող են նպաստել չարտոնված զանգվածային ուղարկումներին:</li>
<li>6.4.32. Նյութեր, որոնք փոխանցվում են բացառապես վիրտուալ ձևով և գրանցված չեն որևէ նյութական կրիչի վրա (գաղափարներ, մեթոդներ, սկզբունքներ և այլն):</li>
<li>6.4.33. Խաղային սարքավորում, որն օգտագործվում է ազարտային խաղերի անցկացման համար, վիճակախաղային սարքավորում, ինտերնետում ազարտային խաղերին մասնակցելու համար խաղադրույքներ ընդունելու ծառայություններ, վիճակախաղի տոմսերի համար վճարումների ընդունում, անդորրագրեր և այլ փաստաթղթեր, որոնք հավաստում են վիճակախաղին մասնակցելու իրավունքը, ինչպես նաև վիրտուալ արժույթի վաճառք:</li>
<li>6.4.34. Տրանսպորտային միջոցների փաստաթղթեր, տրանսպորտային միջոցների պետական համարանիշներ:</li>
<li>6.4.35. Ապրանքներ, որոնց շրջանառությունը խախտում է երրորդ անձանց մտավոր սեփականության իրավունքները (ներառյալ արտոնագրեր, ապրանքային նշաններ, հեղինակային իրավունքներ և այլն):</li>
<li>6.4.36. Ներդրումային ծառայություններ, դրամական միջոցների և կրիպտոարժույթների հետ գործառնություններ, ինչպես նաև ապրանքներ և ծառայություններ, որոնց ձեռքբերումը կամ օգտագործումը երաշխավորված եկամուտ կամ շահույթ է խոստանում:</li>
<li>6.4.37. Բազմամակարդակ ցանցային մարքեթինգի կազմակերպությունների կողմից իրացվող ապրանքներ և ծառայություններ, որոնց գործունեությունը հիմնված է անկախ դիստրիբյուտորների կամ վաճառքի գործակալների ցանց ստեղծելու վրա:</li>
<li>6.4.38. Ինտիմ, էրոտիկ կամ սեռական բնույթի ծառայություններ և (կամ) աշխատանքներ, ինչպես նաև պոռնոգրաֆիկ կամ էրոտիկ նյութեր:</li>
<li>6.4.39. Ապրանքներ կամ ծառայություններ, որոնց օգտագործումը կարող է ուղղված լինել Ռուսաստանի Դաշնության գործող օրենսդրության խախտմանը:</li>
<li>6.4.40. Չգոյություն ունեցող ապրանքներ կամ ծառայություններ, ինչպես նաև ապրանքներ կամ ծառայություններ, որոնք չունեն սպառողական արժեք:</li>
<li>6.4.41. Տրանսցենդենտ ծառայություններ և ոչ ավանդական բժշկության ծառայություններ:</li>
<li>6.4.42. Ծառայություններ` լիցենզավորված ծրագրային ապահովման փոխարինման կամ իրավատիրոջ կողմից տեղադրված տեխնիկական պաշտպանության միջոցների աշխատանքի խափանման համար հեռախոսներում, սմարթֆոններում, նոթբուքերում, նավիգատորներում, անհատական համակարգիչներում և այլն:</li>
<li>6.4.43. Այլ ապրանքներ կամ ծառայություններ, որոնց շրջանառությունն արգելված է կամ սահմանափակված է Ռուսաստանի Դաշնության օրենսդրությամբ, ինչպես նաև կարող է բացասաբար ազդել միջազգային վճարային համակարգերի գործարար համբավի վրա:</li>
<li>6.4.44. Ներարկային դեղամիջոցներ և լուծույթներ, ինչպես նաև դրանց արտադրության համար կիրառվող նյութեր:</li>
<li>6.4.45. Ծառայություններ, աշխատանքներ և նյութեր, որոնք կապված են օկուլտ կազմակերպությունների և աղանդների գործունեության իրականացման հետ:</li>
<li>6.4.46. Ֆինանսական բուրգերի ձևաչափով կազմակերպված ընկերությունների կողմից իրացվող ապրանքներ և ծառայություններ:</li>
<li>6.4.47. Հնաոճ իրեր:</li>
<li>6.4.48. Կենսաբանորեն ակտիվ հավելումներ: Սննդային ԲԱՀ-երի վաճառքը հնարավոր է միայն դեղատների, դեղատնային խանութների, դեղատնային կրպակների, դիետիկ ապրանքների մասնագիտացված խանութների, ինչպես նաև հատուկ բաժիններ և սեկցիաներ ունեցող մթերային խանութների միջոցով:</li>
<li>6.4.49. Պատվերով կուրսային աշխատանքներ և դիպլոմներ:</li>
<li>6.4.50. Անանուն աշխատանք (թաքստոցներով զբաղվողներ և այլն):</li>
<li>6.4.51. Բնօրինակ ապրանքների պատճեններ և կրկնօրինակներ:</li>
</ul>
</section>
<section class="legal-section">
<h2>7. Կայքի բովանդակության բացառիկ իրավունքներ</h2>
<p><strong>7.1. Մտավոր սեփականության օբյեկտներ</strong></p>
<p>Կայքում հասանելի ողջ բովանդակությունը, ներառյալ.</p>
<ul>
<li>դիզայնի տարրեր,</li>
<li>տեքստային նյութեր,</li>
<li>գրաֆիկա և պատկերներ,</li>
<li>տեսանյութեր,</li>
<li>համակարգչային ծրագրեր,</li>
<li>տվյալների բազաներ,</li>
<li>այլ նյութեր -</li>
</ul>
<p>հանդիսանում է Կայքի սեփականատիրոջ և այլ իրավատերերի բացառիկ հեղինակային իրավունքների օբյեկտ:</p>
<p><strong>7.2. Բովանդակության օգտագործում</strong></p>
<p>Բովանդակության օգտագործումը թույլատրվում է միայն կայքի ֆունկցիոնալ հնարավորությունների սահմաններում: Կայքի տարրերի կամ տեղադրված բովանդակության ցանկացած այլ օգտագործում առանց սեփականատերերի նախնական թույլտվության անթույլատրելի է:</p>
<p><strong>7.3. Անձնական օգտագործում</strong></p>
<p>Օգտատերն իրավունք ունի օգտագործել կայքի բովանդակությունը անձնական ոչ առևտրային նպատակներով, պայմանով, որ պահպանվում են հեղինակային իրավունքի, հարակից իրավունքների, ապրանքային նշանների և հեղինակության մասին այլ նշումները:</p>
</section>
<section class="legal-section">
<h2>8. Երրորդ անձանց կայքեր և բովանդակություն</h2>
<p><strong>8.1. Արտաքին ռեսուրսների հղումների առկայություն</strong></p>
<p>Կայքը կարող է ներառել հիպերհղումներ դեպի ինտերնետում գտնվող երրորդ անձանց ռեսուրսներ: Այդ ռեսուրսները Կայքի սեփականատիրոջ կողմից չեն ստուգվում օրենսդրական նորմերին կամ այլ չափանիշներին համապատասխանության տեսանկյունից: Համապատասխանաբար, երրորդ անձանց կայքերում առկա բովանդակության և նյութերի համար պատասխանատվությունը չի մտնում Կայքի սեփականատիրոջ իրավասության մեջ:</p>
<p><strong>8.2. Հղումների բնույթը</strong></p>
<p>Երրորդ կողմի կայքերի հղումները, ինչպես նաև կայքում առկա ապրանքների, ծառայությունների կամ այլ առևտրային կամ ոչ առևտրային տեղեկատվության հիշատակումները չեն նշանակում Կայքի սեփականատիրոջ կողմից դրանց հավանություն կամ առաջարկություն:</p>
<p><strong>8.3. Մեջբերման պահանջներ</strong></p>
<p>Կայքի նյութերի ցանկացած վերարտադրության դեպքում անհրաժեշտ է նշել ակտիվ հղում դեպի աղբյուրը:</p>
<p><strong>8.4. Մտավոր սեփականություն</strong></p>
<p>Լոգոն, ֆիրմային անվանումը, տեսողական ձևավորումը և կայքի ընդհանուր տեսքը պատկանում են Կայքի սեփականատիրոջ մտավոր իրավունքներին: Դրանց օգտագործումը առանց Կայքի սեփականատիրոջ ուղղակի և հստակ թույլտվության արգելվում է:</p>
</section>
<section class="legal-section">
<h2>9. Կողմերի պատասխանատվությունը</h2>
<p><strong>9.1. Ընդհանուր պատասխանատվություն</strong></p>
<p>Կայքը տեղեկատվական-տեխնոլոգիական ռեսուրս է, որն ապահովում է ապրանքների և ծառայությունների մասին տեղեկատվության տեղադրումը, ինչպես նաև գործարքների կատարման անվտանգությունը: Կայքի սեփականատերը պատասխանատվություն է կրում ռեսուրսում արգելված ապրանքների տեղակայման կանխարգելման, ինչպես նաև հարթակում ներկայացված ապրանքների և ծառայությունների մասին տեղեկատվության որակի և արժանահավատության համար:</p>
<p><strong>9.2. Պատասխանատվության սահմանափակումներ</strong></p>
<p>Կայքի սեփականատերը չի երաշխավորում.</p>
<ul>
<li>Կայքի համապատասխանությունը օգտատիրոջ պահանջներին:</li>
<li>Սերվիսի մշտական հասանելիությունը, արագությունը և սխալների բացակայությունը:</li>
</ul>
<p><strong>9.3. Տեղեկատվության օգտագործման հետևանքներ</strong></p>
<p>Կայքի միջոցով ստացվող ողջ տեղեկատվությունն ու նյութերը օգտատերն օգտագործում է իր հայեցողությամբ և ռիսկով: Օգտատերը պատասխանատվություն է կրում այդ տեղեկությունների օգտագործման հնարավոր բացասական հետևանքների համար:</p>
<p><strong>9.4. Մասնակիցների նույնականացում և Վաճառողների վերահսկում</strong></p>
<p>Գնորդը նույնականացվում է հեռախոսահամարի և Telegram-ի միջոցով տրամադրված տվյալների հիման վրա: Յուրաքանչյուր Վաճառող անցնում է ամբողջական նույնականացման (ոնբորդինգ) գործընթաց, և նրա տվյալները հասանելի են Կայքի սեփականատիրոջը` վեճերի դեպքում օգտագործելու համար: Կայքի սեփականատերը իրականացնում է Վաճառողների գործունեության ստուգում և վերահսկում հարթակում:</p>
<p><strong>9.5. Հաշվի անվտանգություն</strong></p>
<p>Օգտատերը ինքնուրույն պատասխանատու է իր պրոֆիլ մուտք գործելու միջոցների գաղտնիության պահպանման համար:</p>
<p><strong>9.6. Վաճառողի պատասխանատվություն</strong></p>
<p>Վաճառողը լիովին պատասխանատու է վաճառվող արտադրանքի որակի, անվտանգության և հայտարարված բնութագրերին համապատասխանության, ինչպես նաև Գնորդի նկատմամբ պարտավորությունների խախտման հետևանքով առաջացած վնասների համար:</p>
<p><strong>9.7. Կայքի սեփականատիրոջ պատասխանատվությունը և պահանջների կարգավորման ընթացակարգը</strong></p>
<p>Կայքի սեփականատերը պատասխանատու է հարթակում տեղադրված ապրանքների և ծառայությունների մասին տեղեկատվության որակի, անվտանգության և արժանահավատության համար: Միևնույն ժամանակ Կայքի սեփականատերը պատասխանատվություն չի կրում հետևյալի համար.</p>
<ul>
<li>Վաճառողների կողմից Գնորդների հանդեպ իրենց պարտավորությունների կատարումը կամ ոչ պատշաճ կատարումը:</li>
<li>Երրորդ անձանց իրավունքների, ներառյալ մտավոր սեփականության, խախտումը:</li>
<li>Առաքման, կոմպլեկտացիայի և ապրանքների վիճակի հետ կապված հարցերը:</li>
</ul>
<p>Գնորդը համաձայնում է, որ ապրանքների որակի, քանակի, կոմպլեկտացիայի և ծառայությունների վերաբերյալ պահանջները կարող են ուղղվել ինչպես Վաճառողին, այնպես էլ կայքի Ադմինիստրացիային: Կայքի Ադմինիստրացիան պատասխանատվություն է ստանձնում հարթակի միջոցով տրամադրվող ծառայությունների որակի և արժանահավատության համար և ակտիվորեն մասնակցում է վիճահարույց իրավիճակների կարգավորմանը:</p>
<p><strong>9.8. Առաքման պատասխանատվություն</strong></p>
<p>Ապրանքների առաքման ժամկետների, պայմանների և որակի համար պատասխանատվությունը կրում են տրանսպորտային ընկերություններն ու սուրհանդակային ծառայությունները: Կայքի սեփականատերը հանդես է գալիս միայն որպես տեղեկատվական միջնորդ և պատասխանատվություն չի կրում առաքման ծառայությունների գործողությունների համար:</p>
<p><strong>9.9. Օգտատիրոջ պատասխանատվությունը երրորդ անձանց առջև</strong></p>
<p>Օգտատերը անձամբ պատասխանատու է երրորդ անձանց առջև կայքի օգտագործման հետ կապված իր գործողությունների համար:</p>
<p><strong>9.10. Ֆորս-մաժորային հանգամանքներ</strong></p>
<p>Կայքի աշխատանքը կարող է կասեցվել չնախատեսված հանգամանքների, վթարների, սարքավորումների խափանումների կամ երրորդ անձանց միջամտության դեպքում` առանց նախնական ծանուցումների:</p>
<p><strong>9.11. Գովազդի պատասխանատվություն</strong></p>
<p>Գովազդային հայտարարությունների բովանդակության համար պատասխանատվությունը կրում են Վաճառողները, որոնք տեղադրում են իրենց ապրանքների և ծառայությունների մասին տեղեկատվությունը: Կայքի սեփականատերը տրամադրում է միայն տեղեկատվության տեղադրման տեխնիկական հարթակը:</p>
<p><strong>9.12. Բնական աղետներ և արտակարգ իրադարձություններ</strong></p>
<p>Ոչ մի կողմ պատասխանատվություն չի կրում պարտավորությունների չկատարման համար, եթե դա պայմանավորված է անհաղթահարելի ուժի հանգամանքներով, ինչպիսիք են բնական աղետները, պատերազմները կամ տեխնածին վթարները:</p>
</section>
<section class="legal-section">
<h2>10. Ապրանքը և գնման կատարման կարգը</h2>
<p><strong>10.1. Պատվերի ձևակերպում</strong></p>
<p>Պատվերը ձևակերպվում է Գնորդի կողմից` խստորեն համաձայն կայքում սահմանված ընթացակարգի:</p>
<p><strong>10.2. Սխալ տվյալների համար պատասխանատվություն</strong></p>
<p>Գնորդը ստանձնում է պատվերը չստանալու ռիսկերը, որոնք կապված են ձևակերպման ժամանակ տրամադրված սխալ կամ ոչ ամբողջական տեղեկատվության հետ:</p>
<p><strong>10.3. Առաքման վերաբերյալ նախնական տեղեկատվություն</strong></p>
<p>Պատվերի ձևավորումից հետո Գնորդը ստանում է ծանուցում առաքման մոտավոր ամսաթվի մասին` գրանցման ժամանակ նշված էլեկտրոնային փոստի կամ հեռախոսի միջոցով:</p>
<p><strong>10.4. Առաքման ժամանակի հստակեցում</strong></p>
<p>Առաքումից առաջ խանութի աշխատակիցը կապ կհաստատի Գնորդի հետ հեռախոսով` առաքման ճշգրիտ ժամանակը հստակեցնելու համար:</p>
<p><strong>10.5. Առաքման ակնկալվող ժամանակի ճշգրտում</strong></p>
<p>Պատվերի մենեջերը տեղեկացնում է Գնորդին պատվերը առաքման ծառայությանը փոխանցելու նախատեսվող ամսաթվի մասին` էլեկտրոնային փոստով կամ զանգով:</p>
<p><strong>10.6. Ապրանքի պակասի հնարավորությունը</strong></p>
<p>Վաճառողը չի երաշխավորում պատվիրված ապրանքի մշտական առկայությունը պահեստում պատվերի մշակման պահին:</p>
<p><strong>10.11. Մատակարարման օրենսդրական շրջանակ</strong></p>
<p>Ապրանքների մատակարարումը իրականացվում է Ռուսաստանի Դաշնության և Վաճառողի ծագման երկրի օրենքներին համապատասխան:</p>
<p><strong>10.12. Ռուսաստանի օրենսդրության կիրառելիություն</strong></p>
<p>Ֆիզիկական ապրանքները փոխանցվում են գնորդին Ռուսաստանի Դաշնության օրենսդրությանը համապատասխան:</p>
</section>
<section class="legal-section">
<h2>11. Պատվերի առաքում</h2>
<p><strong>11.1. Առաքման եղանակներ և ժամկետներ</strong></p>
<p>Ապրանքների առաքման եղանակներն ու մոտավոր ժամկետները նշված են կայքում: Փաստացի ժամկետները կարող են ճշգրտվել պատվերը հաստատելիս call-center-ի աշխատակցի կամ Վաճառողի ներկայացուցչի կողմից:</p>
<p><strong>11.2. Թվային ապրանքների մատակարարման հաստատում</strong></p>
<p>Թվային ապրանքների մատակարարման փաստը հաստատվում է գրանցման ժամանակ նշված հասցեին թվային ֆայլի կամ կոդի կցմամբ էլեկտրոնային նամակ ուղարկելով: Նման նամակի ստացումը նշանակում է, որ թվային ապրանքի մատակարարման ծառայությունը կատարվել է:</p>
<p>Ֆիզիկական ապրանքը համարվում է առաքված այն պահին, երբ այն փաստացի հանձնվում է Գնորդին կամ նրա կողմից նշանակված ստացողին` ընտրված առաքման պայմաններին համապատասխան:</p>
<p><strong>11.3. Առաքման տարածք</strong></p>
<p>Վաճառողի կողմից առաջարկվող ապրանքների առաքման տարածքային սահմանները հրապարակվում են կայքում:</p>
<p><strong>11.4. Ապրանքի ընդունում</strong></p>
<p>Առաքման ժամանակ ապրանքը հանձնվում է Գնորդին կամ պատվերում նշված երրորդ անձին:</p>
<p><strong>11.5. Անձի ստուգում</strong></p>
<p>Նախապես վճարված պատվերը հանձնելիս առաքման ծառայությունը կարող է խնդրել ներկայացնել անձը հաստատող փաստաթուղթ` խարդախության ռիսկը բացառելու համար:</p>
<p><strong>11.6. Ապրանքի պատահական կորստի ռիսկի անցում</strong></p>
<p>Ապրանքի պատահական կորստի կամ վնասման ռիսկը անցնում է Գնորդին պատվերի անմիջական հանձնման պահին:</p>
<p><strong>11.7. Առաքման արժեքի հաշվարկ</strong></p>
<p>Առաքման արժեքը հաշվարկվում է անհատապես յուրաքանչյուր պատվերի համար` հաշվի առնելով քաշը, տարածաշրջանը և ընտրված փոխադրման եղանակը:</p>
<p><strong>11.8. Վաճառողի պարտավորությունների ավարտ</strong></p>
<p>Վաճառողը համարվում է կատարել իր պարտավորությունները ապրանքը հանձնելու մասով, եթե ապրանքը հանձնվել է Գնորդին սուրհանդակի միջոցով կամ տրամադրվել է Գնորդին փոստային բաժանմունքում կամ այլ համաձայնեցված վայրում:</p>
<p><strong>11.9. Ապրանքի զննում ստանալիս</strong></p>
<p>Ապրանքը ստանալիս Գնորդն իրավունք ունի տեսողականորեն գնահատել դրա վիճակը և անհրաժեշտության դեպքում զննել այն Վաճառողի աշխատակցի կամ սուրհանդակի ներկայությամբ:</p>
<p><strong>11.10. Ապրանքի ստուգում սուրհանդակից ընդունելիս</strong></p>
<p>Գնորդը կարող է զննել ստացված ապրանքը և համոզվել դրա համապատասխանության մեջ հայտարարված քանակին, տեսականուն և կոմպլեկտացիային:</p>
<p><strong>11.11. Սուրհանդակային առաքման ժամանակային սահմանափակումներ</strong></p>
<p>Սուրհանդակները Գնորդի հասցեում կարող են գտնվել սահմանափակ ժամանակ` առավելագույնը 20 րոպե:</p>
<p><strong>11.12. Ապրանքի սերտիֆիկացում</strong></p>
<p>Կայքում տեղադրված ապրանքները համապատասխանում են պետական ստանդարտներին (ԳՕՍՏ) և տեխնիկական պայմաններին (ՏՊ), ինչը հաստատվում է հավաստագրերով և այլ անհրաժեշտ փաստաթղթերով:</p>
</section>
<section class="legal-section">
<h2>12. Ապրանքի վճարում</h2>
<p><strong>12.1. Արժույթ և գնի կառուցվածք</strong></p>
<p>Կայքում տեղադրված ապրանքների գինը արտահայտվում է Ռուսաստանի Դաշնության ռուբլիներով և ներառում է բոլոր հարկային վճարումները և պարտադիր գանձումները:</p>
<p><strong>12.2. Գների հնարավոր սխալներ</strong></p>
<p>Երբեմն կայքում կարող են առաջանալ տեխնիկական սխալներ, որոնք հանգեցնում են ապրանքների կամ առաքման արժեքի սխալ արտացոլման: Նման սխալների հայտնաբերման դեպքում Վաճառողը տեղեկացնում է Գնորդին և ճշգրտում է գինը` թողնելով Գնորդին պատվերը թարմացված պայմաններով հաստատելու կամ գործարքը չեղարկելու ընտրություն:</p>
<p><strong>12.3. Գների փոփոխություն</strong></p>
<p>Վաճառողն իրավունք ունի միակողմանի կարգով փոխել ապրանքների գները: Սակայն եթե ապրանքի համար արդեն վճարվել է, տվյալ ապրանքի գնի փոփոխություն չի թույլատրվում:</p>
<p><strong>12.4. Վճարման եղանակներ</strong></p>
<p>Գնորդին հասանելի վճարման տարբերակները ներկայացված են կայքում պատվերի ձևակերպման ընթացքում: Վճարման եղանակն ընդունվում է Օգտատիրոջ ընտրության հիման վրա հասանելի տարբերակներից:</p>
<p><strong>12.6. Բանկային քարտերով վճարման առանձնահատկություններ</strong></p>
<p>Բանկային քարտերով վճարելիս գործում են հետևյալ կանոնները.</p>
<ul>
<li>Վճարումները կատարվում են քարտատիրոջ կամ լիազորված անձի կողմից:</li>
<li>Գործարքների հեղինակացումը կատարվում է բանկային կազմակերպության միջոցով:</li>
<li>Վաճառողն իրավունք ունի պահանջել անձը հաստատող փաստաթուղթ` քարտից օգտվելու լիազորությունը հաստատելու համար:</li>
<li>Կայքում բանկային քարտով վճարում կատարելիս Գնորդը համաձայնում է էլեկտրոնային փոստով էլեկտրոնային դրամարկղային կտրոն ստանալուն:</li>
<li>Քարտով վճարված պատվերները ստուգվում են Վաճառողի կողմից` խարդախությունը կանխելու նպատակով:</li>
<li>Գնորդների բանկային քարտերի տվյալների հավաքագրում և պահպանում չի իրականացվում ոչ Կայքի սեփականատիրոջ, ոչ էլ Վաճառողի կողմից:</li>
</ul>
<p><strong>12.7. Զեղչեր և բոնուսներ</strong></p>
<p>Վաճառողն իրավունք ունի սահմանել զեղչեր և ներդնել բոնուսային ծրագրեր: Զեղչերի տեսակները, տրամադրման կարգը և պայմանները սահմանվում են Վաճառողի կողմից և հրապարակվում են կայքում:</p>
<p><strong>12.13. Կանխիկ վճարման գումարի սահմանափակումներ</strong></p>
<p>Ապրանքի վճարումը կանխիկ եղանակով չի իրականացվում:</p>
<p><strong>12.14. Դրամական միջոցների վերադարձի ժամկետներ</strong></p>
<p>Ապրանքի համար դրամական միջոցների վերադարձի մասին Գնորդի պահանջները բավարարվում են համապատասխան դիմումը ներկայացնելու պահից 10 օրվա ընթացքում:</p>
</section>
<section class="legal-section">
<h2>13. Ապրանքի վերադարձ և փոխանակում</h2>
<p><strong>13.1. Վերադարձի ընդհանուր կանոններ</strong></p>
<p>Թվային ապրանքները (էլեկտրոնային ձևով տրամադրվող) վերադարձման ենթակա չեն` ռուսական օրենսդրությանը համապատասխան: Ֆիզիկական ապրանքների վերադարձը կատարվում է <a [routerLink]="'/return-policy' | langRoute">«Վերադարձի քաղաքականություն»</a> բաժնի և սպառողների իրավունքների մասին գործող օրենքների համաձայն:</p>
<p><strong>13.2. Ապրանքի վերադարձ</strong></p>
<p>Կայքում ներկայացված և վերադարձման ենթակա ապրանքների վերադարձը կամ փոխարինումը կատարվում է սույն համաձայնագրին և Ռուսաստանի Դաշնության օրենսդրությանը համապատասխան:</p>
<p><strong>13.4. Ակցիոն հավաքածուներ</strong></p>
<p>Եթե ապրանքների հավաքածուն ձեռք է բերվել ակցիայով և զեղչով, վերադարձը կամ փոխանակումը հնարավոր է միայն ամբողջ հավաքածուի համար: Հավաքածուից առանձին ապրանքներ վերադարձնել հնարավոր չէ:</p>
<p><strong>13.5. Առաքման ծախսերի փոխհատուցում</strong></p>
<p>Որակյալ ապրանքը վերադարձնելու դեպքում Վաճառողը կամ Կայքի սեփականատերը իրավունք ունի Գնորդից գանձել ապրանքի առաքման ծախսերը:</p>
<p><strong>13.6. Պահանջների բավարարման ժամկետներ</strong></p>
<p>Դրամական միջոցների վերադարձի դիմումները բավարարվում են ներկայացման պահից 10 օրվա ընթացքում:</p>
<p><strong>13.6.7. Փաստաթղթեր ապրանքի վերադարձի դեպքում</strong></p>
<p>Ապրանքը վերադարձնելիս Գնորդը պարտավոր է տրամադրել հետևյալ փաստաթղթերը.</p>
<ul>
<li>Վերադարձի դիմում:</li>
<li>Վճարման անդորրագրի կամ կտրոնի պատճեն:</li>
<li>Ընդունման-հանձնման ակտի (կամ ներդրվածքի ցանկի) պատճեն:</li>
</ul>
<p><strong>13.6.9. Դրամական միջոցների վերադարձի մեխանիզմ</strong></p>
<p>Դրամական միջոցների վերադարձն իրականացվում է վճարված ապրանքի արժեքը վերադարձնելու միջոցով: Վերադարձի եղանակը նշվում է վերադարձի դիմումում:</p>
</section>
<section class="legal-section">
<h2>14. Համաձայնագրի գործողության ժամկետը</h2>
<p><strong>14.1. Գործողության սկիզբ և դադարեցում</strong></p>
<p>Համաձայնագիրն ուժի մեջ է մտնում Օգտատիրոջ կամ Գնորդի կողմից այն ընդունելու (ակցեպտելու) պահից և գործում է մինչև հանրային օֆերտայի ակցեպտը հետ կանչելու պահը: Օֆերտայի հետկանչը ենթադրում է համաձայնագրի գործողության անհապաղ դադարեցում:</p>
<p><strong>14.2. Օֆերտան հետ կանչելու իրավունք</strong></p>
<p>Կայքի սեփականատերն իրավունք ունի հետ կանչել սույն առաջարկը` Ռուսաստանի Դաշնության քաղաքացիական օրենսգրքի 436-րդ հոդվածով նախատեսված կարգով:</p>
</section>
<section class="legal-section">
<h2>15. Վեճերի լուծման և պահանջների կարգավորման կարգը</h2>
<p><strong>15.1. Վեճերի կամավոր կարգավորում</strong></p>
<p>Եթե Օգտատիրոջ և Կայքի սեփականատիրոջ միջև առաջանում են տարաձայնություններ, որոնք կապված են համաձայնագրի պայմանների կատարման հետ, երկու կողմերն էլ պարտավոր են փորձել լուծել խնդիրը փոխադարձ խորհրդակցությունների և բանակցությունների միջոցով: Սահմանվում է պարտադիր նախադատական կարգ:</p>
<p><strong>15.2. Պահանջ ներկայացնելու կարգ</strong></p>
<p>Պահանջների քննության ընթացակարգը նախատեսում է հետևյալ փուլերը.</p>
<ul>
<li>Օգտատերը, որը կարծում է, որ իր իրավունքները խախտվել են Կայքի սեփականատիրոջ գործողությամբ կամ անգործությամբ, ուղարկում է համապատասխան պահանջ` նշելով պահանջի էությունը:</li>
<li>Պահանջը ստանալուց հետո 25 աշխատանքային օրվա ընթացքում Կայքի սեփականատերը պարտավոր է պատասխան պատրաստել և ներկայացնել իր դիրքորոշումը:</li>
<li>Եթե փոխզիջման հասնել չի հաջողվում, վեճը քննվում է դատական կարգով` համաձայն համաձայնագրի 15.5 կետի:</li>
</ul>
<p><strong>15.3. Պահանջների քննություն</strong></p>
<p>Կայքի սեփականատերը չի ընդունում անանուն պահանջներ կամ բողոքներ, որոնք բավարար տեղեկություն չեն պարունակում դիմողի նույնականացման համար:</p>
<p><strong>15.5. Վեճերի դատական քննություն</strong></p>
<p>Եթե բանակցությունները և պահանջների նախադատական կարգավորումը չեն հանգեցնում համաձայնության, վեճը լուծվում է դատական կարգով` Կայքի սեփականատիրոջ գտնվելու վայրով:</p>
</section>
<section class="legal-section">
<h2>16. Այլ պայմաններ</h2>
<p><strong>16.1. Համաձայնագրի գործողությունը</strong></p>
<p>Սույն փաստաթուղթը հանդիսանում է Օգտատիրոջ և Կայքի սեփականատիրոջ միջև ռեսուրսի օգտագործման վերաբերյալ պաշտոնական համաձայնագիր և փոխարինում է բոլոր նախորդ պայմանավորվածությունները:</p>
<p><strong>16.2. Համաձայնություն պայմաններին</strong></p>
<p>Օգտատերը, շարունակելով օգտագործել կայքը, արտահայտում է իր համաձայնությունը համաձայնագրի պայմաններին: Անհամաձայնությունը ենթադրում է անձնական հաշվի հեռացում:</p>
<p><strong>16.3. Կիրառվող օրենսդրություն</strong></p>
<p>Համաձայնագրով չկարգավորված հարցերը լուծվում են Հայաստանի օրենսդրությանը համապատասխան:</p>
<p><strong>16.4. «Օրենսդրություն» տերմինի սահմանում</strong></p>
<p>Համաձայնագրի ողջ տեքստում «օրենսդրություն» տերմինը, եթե հատուկ այլ բան նշված չէ, ենթադրում է Հայաստանի օրենքները:</p>
<p><strong>16.5. Անվճար ծառայություններ</strong></p>
<p>Համաձայնագրի շրջանակում տրամադրվող անվճար ծառայությունները չեն ենթադրում սպառողների իրավունքների պաշտպանության մասին օրենսդրության նորմերի կիրառում Օգտատիրոջ և Վաճառողի հարաբերությունների նկատմամբ:</p>
<p><strong>16.6. Բացակայող հարաբերություններ</strong></p>
<p>Համաձայնագրի ոչ մի դրույթ չի կարող մեկնաբանվել որպես գործակալական հարաբերությունների, գործընկերության, համատեղ ձեռնարկության, աշխատանքային հարաբերությունների կամ այլ հարաբերությունների հաստատում, որոնք ուղղակիորեն նախատեսված չեն համաձայնագրով:</p>
<p><strong>16.7. Կետերի անվավեր ճանաչում</strong></p>
<p>Համաձայնագրի մեկ կամ մի քանի դրույթների անվավեր ճանաչումը չի ազդում մնացած կետերի իրավաբանական ուժի և կիրառելիության վրա:</p>
<p><strong>16.8. Արձագանք խախտումներին</strong></p>
<p>Օգտատերերի կողմից համաձայնագրերի խախտման դեպքում Կայքի սեփականատիրոջ չմիջամտելը հետագայում չի խոչընդոտում նրա շահերի պաշտպանության միջոցների կիրառմանը:</p>
</section>
</div>
</div>

View File

@@ -1,9 +1,12 @@
import { Component, ChangeDetectionStrategy } from '@angular/core'; import { Component, ChangeDetectionStrategy } from '@angular/core';
import { RouterLink } from "@angular/router";
import { LangRoutePipe } from '../../../../pipes/lang-route.pipe';
@Component({ @Component({
selector: 'app-public-offer-hy', selector: 'app-public-offer-hy',
templateUrl: './public-offer-hy.component.html', templateUrl: './public-offer-hy.component.html',
styleUrls: ['../public-offer.component.scss'], styleUrls: ['../public-offer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterLink, LangRoutePipe]
}) })
export class PublicOfferHyComponent {} export class PublicOfferHyComponent {}

View File

@@ -1,8 +1,8 @@
import { Component, ChangeDetectionStrategy, inject } from '@angular/core'; import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
import { LanguageService } from '../../../services/language.service'; import { LanguageService } from '../../../services/language.service';
import { PublicOfferRuComponent } from './ru/public-offer-ru.component';
import { PublicOfferEnComponent } from './en/public-offer-en.component'; import { PublicOfferEnComponent } from './en/public-offer-en.component';
import { PublicOfferHyComponent } from './hy/public-offer-hy.component'; import { PublicOfferHyComponent } from './hy/public-offer-hy.component';
import { PublicOfferRuComponent } from './ru/public-offer-ru.component';
@Component({ @Component({
selector: 'app-public-offer', selector: 'app-public-offer',

View File

@@ -1,3 +1,5 @@
<div class="legal-page">
<div class="legal-container">
<h1>СОГЛАШЕНИЕ ПУБЛИЧНОЙ ОФЕРТЫ</h1> <h1>СОГЛАШЕНИЕ ПУБЛИЧНОЙ ОФЕРТЫ</h1>
<section class="legal-section"> <section class="legal-section">
@@ -16,7 +18,7 @@
<li><strong>Покупатель</strong> — зарегистрированный Пользователь, который сделал заказ через сайт.</li> <li><strong>Покупатель</strong> — зарегистрированный Пользователь, который сделал заказ через сайт.</li>
<li><strong>Администратор или Владелец Сайта</strong> — юридическое лицо ООО "ИНТ ФИН ЛОГИСТИК", идентификационный налоговый номер (ИНН) 9909697628.</li> <li><strong>Администратор или Владелец Сайта</strong> — юридические лица ООО "ИНТ ФИН ЛОГИСТИК" (ИНН 9909697628) и ООО "ИНТ ФАКТОРИНГ" (ИНН 9909697635).</li>
<li><strong>Продавец или Селлер</strong> — физические или юридические лица, предприниматели, предлагающие товары и услуги на ресурсе. Продавцы несут личную ответственность за качество, безопасность и соответствие описания товаров.</li> <li><strong>Продавец или Селлер</strong> — физические или юридические лица, предприниматели, предлагающие товары и услуги на ресурсе. Продавцы несут личную ответственность за качество, безопасность и соответствие описания товаров.</li>
@@ -175,6 +177,62 @@
<p><strong>6.3. Права Пользователя</strong></p> <p><strong>6.3. Права Пользователя</strong></p>
<p>Пользователь имеет право отказаться от получения рекламных сообщений, воспользовавшись соответствующим инструментом на сайте или направив заявку по электронной почте <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a> или письмом по официальному адресу Владельца сайта.</p> <p>Пользователь имеет право отказаться от получения рекламных сообщений, воспользовавшись соответствующим инструментом на сайте или направив заявку по электронной почте <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a> или письмом по официальному адресу Владельца сайта.</p>
<p><strong>6.4. Запрещённые товары для продажи на сайте</strong></p>
<p>На сайте запрещается размещение и продажа следующих категорий товаров и услуг:</p>
<ul>
<li>6.4.1. Вооружение, боеприпасы к нему, военная техника, запасные части, комплектующие изделия и приборы к ним, взрывчатые вещества, средства взрывания, все виды ракетного топлива, а также специальные материалы и специальное оборудование для их производства, специальное снаряжение военизированных организаций и нормативно-техническая продукция на их производство и эксплуатацию.</li>
<li>6.4.2. Ракетно-космические комплексы, системы связи и управления военного назначения и нормативно-техническая документация на их производство и эксплуатацию.</li>
<li>6.4.3. Боевые отравляющие вещества, средства защиты от них и нормативно-техническая документация на их производство и использование.</li>
<li>6.4.4. Результаты научно-исследовательских и проектных работ, а также фундаментальных поисковых исследований по созданию вооружения и военной техники.</li>
<li>6.4.5. Услуги, работы и материалы, связанные с осуществлением военной службы и военизированной деятельности.</li>
<li>6.4.6. Любое оружие, в том числе охотничье, гражданское и иное, а также комплектующие изделия к нему, ножи (за исключением кухонных, перочинных и канцелярских).</li>
<li>6.4.7. Радиоактивные вещества и изотопы, уран и другие делящиеся материалы и изделия из них.</li>
<li>6.4.8. Отходы радиоактивных материалов.</li>
<li>6.4.9. Драгоценные и редкоземельные металлы, драгоценные камни, а также отходы, содержащие драгоценные и редкоземельные металлы и драгоценные камни.</li>
<li>6.4.10. Рентгеновское оборудование, приборы и оборудование с использованием радиоактивных веществ и изотопов.</li>
<li>6.4.11. Яды, наркотические средства и психотропные вещества, их прекурсоры.</li>
<li>6.4.12. Спирт этиловый, алкогольные напитки.</li>
<li>6.4.13. Лекарственные препараты, отпускаемые по рецепту, а также наркотические, психотропные и спиртосодержащие (с объёмной долей этилового спирта свыше 25%) лекарственные препараты и бальзамы на основе спирта.</li>
<li>6.4.14. Лекарственное сырье, получаемое от северного оленеводства (панты и эндокринное сырье).</li>
<li>6.4.15. Табачная продукция или продукты для вейпинга.</li>
<li>6.4.16. Шифровальная техника и нормативно-техническая документация на ее производство и использование.</li>
<li>6.4.17. Поддельные денежные знаки.</li>
<li>6.4.18. Иностранная валюта и иные валютные ценности, монеты и банкноты Российской Федерации, находящиеся в обращении.</li>
<li>6.4.19. Радиоэлектронные и специальные технические средства, предназначенные для негласного получения информации, а также высокочастотные устройства, состоящие из одного или нескольких радиопередающих устройств и (или) их комбинаций и вспомогательного оборудования, предназначенные для передачи и приема радиоволн на частоте выше 8 ГГц.</li>
<li>6.4.20. Материалы и услуги, нарушающие тайну частной жизни, посягающие на честь, достоинство и деловую репутацию граждан и юридических лиц, а также содержащие государственную, банковскую, коммерческую и иную тайны.</li>
<li>6.4.21. Государственные награды РФ, РСФСР, СССР, а также их копии.</li>
<li>6.4.22. Государственные удостоверения личности, знаки, пропуска, разрешения, сертификаты, проездные документы и лицензии, а также иные документы, предоставляющие права или освобождающие от прав или обязанностей, бланки для этих документов, а также услуги по их получению.</li>
<li>6.4.23. Объекты культурного наследия народов Российской Федерации, а также объекты археологического наследия.</li>
<li>6.4.24. Человеческие органы и ткани, а также донорские услуги.</li>
<li>6.4.25. Животные и растения, занесенные в Красную книгу Российской Федерации и Красные книги субъектов Российской Федерации, части и органы животных, занесенных в Красную книгу Российской Федерации и Красные книги субъектов Российской Федерации, а также животные и растения, охраняемые международными договорами Российской Федерации.</li>
<li>6.4.26. Шкуры и изделия из шкур редких и находящихся под угрозой исчезновения видов животных в соответствии с действующим законодательством Российской Федерации.</li>
<li>6.4.27. Рыболовные сети, материалы для их изготовления, а также услуги по их изготовлению, электроудочки и капканы, запрещенные к реализации на территории Российской Федерации.</li>
<li>6.4.28. Экстремистские материалы, материалы, призывающие к массовым беспорядкам, осуществлению террористической и экстремистской деятельности, к участию в массовых публичных мероприятиях, разжиганию межнациональной и межконфессиональной розни.</li>
<li>6.4.29. Предметы с нацистской символикой или символикой запрещенных в Российской Федерации организаций.</li>
<li>6.4.30. Контрафактная или краденая продукция, или имущество.</li>
<li>6.4.31. Базы данных, в том числе содержащие персональные данные, которые могут способствовать несанкционированным рассылкам.</li>
<li>6.4.32. Материалы, передаваемые исключительно виртуально и не записанные на какой-либо материальный носитель (идеи, методы, принципы и т. д.).</li>
<li>6.4.33. Игровое оборудование, используемое для проведения азартных игр, лотерейное оборудование, оказание услуг по приему ставок для участия в азартных играх в интернете, прием платежей за лотерейные билеты, квитанции и иные документы, удостоверяющие право на участие в лотерее, а также продажа виртуальной валюты.</li>
<li>6.4.34. Документы на транспортные средства, государственные номера для транспортных средств.</li>
<li>6.4.35. Товары, оборот которых нарушает интеллектуальные права третьих лиц (в том числе патенты, товарные знаки, авторские права и т. д.).</li>
<li>6.4.36. Инвестиционные услуги, операции с денежными средствами и криптовалютами, а также товары и услуги, приобретение или использование которых гарантированно приносит заработок или прибыль.</li>
<li>6.4.37. Товары и услуги, реализуемые организацией многоуровневого сетевого маркетинга, деятельность которых основана на создании сети независимых дистрибьюторов или сбытовых агентов.</li>
<li>6.4.38. Услуги и (или) работа интимного, эротического или сексуального характера, а также порнографические или эротические материалы.</li>
<li>6.4.39. Товары или услуги, использование которых может быть направлено на нарушение действующего законодательства Российской Федерации.</li>
<li>6.4.40. Несуществующие товары или услуги, а также товары или услуги, не имеющие потребительской ценности.</li>
<li>6.4.41. Трансцендентные услуги и услуги нетрадиционной медицины.</li>
<li>6.4.42. Услуги по замене лицензионного программного обеспечения или нарушению работы установленных правообладателем средств технической защиты телефонов, смартфонов, ноутбуков, навигаторов, персональных компьютеров и т. д.</li>
<li>6.4.43. Иные товары или услуги, оборот которых запрещен или ограничен согласно законодательству Российской Федерации, а также способен оказать негативное влияние на деловую репутацию международных платежных систем.</li>
<li>6.4.44. Инъекционные препараты и растворы, а также вещества, применяемые для их изготовления.</li>
<li>6.4.45. Услуги, работы и материалы, связанные с осуществлением деятельности оккультных организаций и сект.</li>
<li>6.4.46. Товары и услуги, реализуемые компаниями по форме организации финансовых пирамид.</li>
<li>6.4.47. Антиквариат.</li>
<li>6.4.48. Биологически активные добавки. Продажа БАД к пище возможна только через аптечные учреждения (аптеки, аптечные магазины, аптечные киоски), специализированные магазины с диетическими продуктами, продовольственные магазины со специальными отделами и секциями.</li>
<li>6.4.49. Курсовые и дипломы на заказ.</li>
<li>6.4.50. Анонимная работа (закладчики и т. п.).</li>
<li>6.4.51. Копии и реплики оригинальных товаров.</li>
</ul>
</section> </section>
<section class="legal-section"> <section class="legal-section">
@@ -459,3 +517,5 @@
<p><strong>16.8. Реакция на нарушения</strong></p> <p><strong>16.8. Реакция на нарушения</strong></p>
<p>Невмешательство Владельца сайта в случае нарушений соглашений Пользователями не препятствует последующим мерам защиты интересов Владельца позже.</p> <p>Невмешательство Владельца сайта в случае нарушений соглашений Пользователями не препятствует последующим мерам защиты интересов Владельца позже.</p>
</section> </section>
</div>
</div>

View File

@@ -1,3 +1,5 @@
<div class="legal-page">
<div class="legal-container">
<h1>Return Policy</h1> <h1>Return Policy</h1>
<section class="legal-section"> <section class="legal-section">
@@ -128,3 +130,5 @@
</ul> </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> <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> </section>
</div>
</div>

View File

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

View File

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

View File

@@ -56,17 +56,28 @@
@if (items().length > 0) { @if (items().length > 0) {
<div class="items-grid"> <div class="items-grid">
@for (item of items(); track trackByItemId($index, item)) { @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"> <a [routerLink]="['/item', item.itemID] | langRoute" class="item-link">
<div class="item-image"> <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) { @if (item.discount > 0) {
<div class="discount-badge">-{{ item.discount }}%</div> <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>
<div class="item-details"> <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"> <div class="item-rating">
<span class="rating-stars">⭐ {{ item.rating }}</span> <span class="rating-stars">⭐ {{ item.rating }}</span>
@@ -94,7 +105,7 @@
</div> </div>
</a> </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 }} {{ 'search.addToCart' | translate }}
</button> </button>
</div> </div>
@@ -102,10 +113,20 @@
</div> </div>
@if (loading() && items().length > 0) { @if (loading() && items().length > 0) {
<div class="loading-more"> @for (i of skeletonSlots; track i) {
<div class="spinner"></div> <div class="item-card skeleton-card">
<p>{{ 'search.loadingMore' | translate }}</p> <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>
<div class="skeleton-btn"></div>
</div>
}
} }
@if (!hasMore() && items().length > 0) { @if (!hasMore() && items().length > 0) {

View File

@@ -344,6 +344,59 @@
text-align: center; 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) { @media (max-width: 768px) {
.search-header h1 { .search-header h1 {
font-size: 1.5rem; font-size: 1.5rem;

View File

@@ -3,13 +3,16 @@ import { DecimalPipe } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { ApiService, CartService } from '../../services'; import { ApiService, CartService } from '../../services';
import { PrefetchService } from '../../services/prefetch.service';
import { Item } from '../../models'; import { Item } from '../../models';
import { Subject, Subscription } from 'rxjs'; import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; 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 { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe'; import { TranslatePipe } from '../../i18n/translate.pipe';
import { TranslateService } from '../../i18n/translate.service'; import { TranslateService } from '../../i18n/translate.service';
import { SEARCH_DEBOUNCE_MS, ITEMS_PER_PAGE, SCROLL_THRESHOLD_PX, SCROLL_DEBOUNCE_MS } from '../../config/constants';
@Component({ @Component({
selector: 'app-search', selector: 'app-search',
@@ -27,7 +30,7 @@ export class SearchComponent implements OnDestroy {
totalResults = signal<number>(0); totalResults = signal<number>(0);
private skip = 0; private skip = 0;
private readonly count = 20; private readonly count = ITEMS_PER_PAGE;
private isLoadingMore = false; private isLoadingMore = false;
private searchSubject = new Subject<string>(); private searchSubject = new Subject<string>();
private searchSubscription: Subscription; private searchSubscription: Subscription;
@@ -35,11 +38,12 @@ export class SearchComponent implements OnDestroy {
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
private cartService: CartService private cartService: CartService,
private prefetchService: PrefetchService
) { ) {
this.searchSubscription = this.searchSubject this.searchSubscription = this.searchSubject
.pipe( .pipe(
debounceTime(300), debounceTime(SEARCH_DEBOUNCE_MS),
distinctUntilChanged() distinctUntilChanged()
) )
.subscribe(query => { .subscribe(query => {
@@ -63,7 +67,7 @@ export class SearchComponent implements OnDestroy {
performSearch(query: string): void { performSearch(query: string): void {
if (!query.trim()) { if (!query.trim()) {
this.items.set([]); this.items.set([]);
this.hasMore.set(true); this.hasMore.set(false);
this.totalResults.set(0); this.totalResults.set(0);
return; return;
} }
@@ -119,12 +123,12 @@ export class SearchComponent implements OnDestroy {
this.scrollTimeout = setTimeout(() => { this.scrollTimeout = setTimeout(() => {
const scrollPosition = window.innerHeight + window.scrollY; 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()) { if (scrollPosition >= bottomPosition && !this.loading() && this.hasMore()) {
this.loadResults(); this.loadResults();
} }
}, 100); }, SCROLL_DEBOUNCE_MS);
} }
addToCart(itemID: number, event: Event): void { addToCart(itemID: number, event: Event): void {
@@ -133,7 +137,17 @@ export class SearchComponent implements OnDestroy {
this.cartService.addItem(itemID); this.cartService.addItem(itemID);
} }
onItemHover(itemID: number): void {
this.prefetchService.prefetchItem(itemID);
}
readonly skeletonSlots = Array.from({ length: 8 });
readonly getDiscountedPrice = getDiscountedPrice; readonly getDiscountedPrice = getDiscountedPrice;
readonly getMainImage = getMainImage; readonly getMainImage = getMainImage;
readonly trackByItemId = trackByItemId; 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 { export class LangRoutePipe implements PipeTransform {
private langService = inject(LanguageService); private langService = inject(LanguageService);
private lastLang = '';
private lastInput: unknown = null;
private lastResult: string | (string | number)[] = '';
transform(value: string | (string | number)[]): string | (string | number)[] { transform(value: string | (string | number)[]): string | (string | number)[] {
const lang = this.langService.currentLanguage(); 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') { if (typeof value === 'string') {
return value === '/' ? `/${lang}` : `/${lang}${value}`; this.lastResult = value === '/' ? `/${lang}` : `/${lang}${value}`;
} } else if (Array.isArray(value) && value.length > 0) {
if (Array.isArray(value) && value.length > 0) {
const [first, ...rest] = value; 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,318 @@
import { Injectable, inject } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs'; import { Observable, timer } from 'rxjs';
import { map } from 'rxjs/operators'; import { map, retry } from 'rxjs/operators';
import { Category, Item } from '../models'; import { Category, Item, Subcategory } from '../models';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
import { LocationService } from './location.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ApiService { export class ApiService {
private readonly baseUrl = environment.apiUrl; 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) {} constructor(private http: HttpClient) {}
private normalizeItem(item: Item): Item { /** Map API language codes (RU/EN/AM) → frontend codes (ru/en/hy) */
return { private normalizeLang(apiLang: string): string {
...item, const map: Record<string, string> = { 'RU': 'ru', 'EN': 'en', 'AM': 'hy' };
remainings: item.remainings || 'high' return map[apiLang] || apiLang.toLowerCase();
};
} }
private normalizeItems(items: Item[] | null | undefined): Item[] { /** Convert Go-style hex colour (0xfffca0) → CSS hex (#fffca0) */
private normalizeColor(c: string): string {
if (!c) return '';
return c.startsWith('0x') ? '#' + c.slice(2) : c;
}
/** Resolve relative image URLs (e.g. ./images/x.webp) against site origin */
private resolveImageUrl(url: string): string {
if (!url) return '';
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/')) return url;
const origin = `https://${environment.domain}`;
if (url.startsWith('./')) return `${origin}/${url.slice(2)}`;
return `${origin}/${url}`;
}
/**
* 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 };
// Extract price/currency/remaining/colour/size from itemDetails[]
// Note: Go struct tag is "itemdetails" but actual API may send "itemDetails"
const details = raw.itemDetails || raw.itemdetails;
if (details && Array.isArray(details) && details.length > 0) {
const detail = details[0];
item.itemDetails = details.map((d: any) => ({
...d,
colour: this.normalizeColor(d.colour || d.color || ''),
color: undefined,
}));
if (item.price == null || item.price === 0) item.price = detail.price;
if (!item.currency) item.currency = detail.currency;
if (!item.colour) item.colour = this.normalizeColor(detail.colour || detail.color || '');
if (!item.size) item.size = detail.size || '';
// Use remaining from detail for stock level
if (raw.remaining == null && detail.remaining != null) {
(raw as any).remaining = detail.remaining;
}
}
// 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
// Also resolve relative URLs (e.g. ./images/x.webp) against API base
if (item.photos) {
item.photos = item.photos.map((p: any) => ({
...p,
url: this.resolveImageUrl(p.url),
video: p.video || (p.type === 'video' ? p.url : undefined),
}));
}
item.imgs = raw.imgs?.map((u: string) => this.resolveImageUrl(u))
|| item.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)
// Note: API has typo "valuue" in some responses, handle both
if (raw.names && Array.isArray(raw.names)) {
item.names = raw.names;
if (!item.translations) item.translations = {};
for (const entry of raw.names) {
const lang = this.normalizeLang(entry.language);
const val = entry.value || entry.valuue || '';
if (val) {
if (!item.translations[lang]) item.translations[lang] = {};
item.translations[lang].name = val;
}
}
// Fallback: if top-level name is missing, use first available translation
if (!item.name && raw.names.length > 0) {
const ruName = raw.names.find((n: any) => n.language === 'RU' || n.language === 'ru');
item.name = ruName?.value || ruName?.valuue || raw.names[0].value || raw.names[0].valuue || '';
}
}
// Preserve attributes from backend
item.attributes = raw.attributes || [];
// Preserve colour & size (only if not already set from itemDetails)
if (!item.colour) item.colour = this.normalizeColor(raw.colour || '');
if (!item.size) 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.name = item.name || '';
item.price = item.price ?? 0;
item.discount = item.discount || 0;
item.remainings = item.remainings || (raw.remaining != null
? (raw.remaining <= 0 ? 'out' : raw.remaining <= 5 ? 'low' : raw.remaining <= 20 ? 'medium' : 'high')
: 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;
item.visits = raw.visits ?? 0;
// Map question like/dislike → upvotes/downvotes
if (item.questions) {
item.questions = item.questions.map((q: any) => ({
...q,
upvotes: q.upvotes ?? q.like ?? 0,
downvotes: q.downvotes ?? q.dislike ?? 0,
}));
}
return item;
}
private normalizeItems(items: any[] | null | undefined): Item[] {
if (!items || !Array.isArray(items)) { if (!items || !Array.isArray(items)) {
return []; return [];
} }
return items.map(item => this.normalizeItem(item)); return items.map(item => this.normalizeItem(item));
} }
/** Append region query param if a region is selected */ /**
private withRegion(params: HttpParams = new HttpParams()): HttpParams { * Normalize a category from the API response — supports both
const regionId = this.locationService.regionId(); * the flat legacy format and nested backOffice format.
return regionId ? params.set('region', regionId) : params; */
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;
// Resolve relative icon/image URLs
if (cat.icon) cat.icon = this.resolveImageUrl(cat.icon);
if (cat.img) cat.img = this.resolveImageUrl(cat.img);
// Map backend wideicon → wideBanner
if (raw.wideicon && !cat.wideBanner) {
cat.wideBanner = raw.wideicon;
}
cat.parentID = raw.parentID ?? 0;
cat.visible = raw.visible ?? true;
cat.priority = raw.priority ?? 0;
cat.itemCount = raw.itemCount ?? raw.ItemsCount ?? 0;
cat.categoriesCount = raw.categoriesCount ?? raw.CategoriesCount ?? 0;
// Map backend names[] → translations (multi-lang name support)
// Note: API has typo "valuue" in some responses, handle both
if (raw.names && Array.isArray(raw.names)) {
cat.names = raw.names;
cat.translations = cat.translations || {};
for (const entry of raw.names) {
const lang = this.normalizeLang(entry.language);
const val = entry.value || entry.valuue || '';
if (val) {
if (!cat.translations[lang]) cat.translations[lang] = {};
cat.translations[lang].name = val;
}
}
// Fallback: if top-level name is missing, use first available translation
if (!cat.name && raw.names.length > 0) {
const ruName = raw.names.find((n: any) => n.language === 'RU' || n.language === 'ru');
cat.name = ruName?.value || ruName?.valuue || raw.names[0].value || raw.names[0].valuue || '';
}
}
cat.name = cat.name || '';
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 }> { ping(): Observable<{ message: string }> {
return this.http.get<{ message: string }>(`${this.baseUrl}/ping`); return this.http.get<{ message: string }>(`${this.baseUrl}/ping`);
} }
getCategories(): Observable<Category[]> { 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[]> { getCategoryItems(categoryID: number, count: number = 50, skip: number = 0): Observable<Item[]> {
const params = this.withRegion( const params = new HttpParams()
new HttpParams()
.set('count', count.toString()) .set('count', count.toString())
.set('skip', skip.toString()) .set('skip', skip.toString());
); return this.http.get<any[]>(`${this.baseUrl}/category/${categoryID}`, { params })
return this.http.get<Item[]>(`${this.baseUrl}/category/${categoryID}`, { params }) .pipe(retry(this.retryConfig), map(items => this.normalizeItems(items)));
.pipe(map(items => this.normalizeItems(items)));
} }
getItem(itemID: number): Observable<Item> { getItem(itemID: number): Observable<Item> {
return this.http.get<Item>(`${this.baseUrl}/item/${itemID}`, { params: this.withRegion() }) return this.http.get<any>(`${this.baseUrl}/items/${itemID}`)
.pipe(map(item => this.normalizeItem(item))); .pipe(retry(this.retryConfig), map(item => this.normalizeItem(item)));
} }
searchItems(search: string, count: number = 50, skip: number = 0): Observable<{ items: Item[], total: number }> { searchItems(
const params = this.withRegion( search: string,
new HttpParams() count: number = 50,
skip: number = 0,
options?: {
categoryIDs?: number[];
minPrice?: number;
maxPrice?: number;
tag?: string;
sort?: 'relevance' | 'price_asc' | 'price_desc' | 'popular' | 'rating';
}
): Observable<{ items: Item[], total: number }> {
let params = new HttpParams()
.set('search', search) .set('search', search)
.set('count', count.toString()) .set('count', count.toString())
.set('skip', skip.toString()) .set('skip', skip.toString());
); if (options?.categoryIDs?.length) {
return this.http.get<{ items: Item[], total: number, count: number, skip: number }>(`${this.baseUrl}/searchitems`, { params }) params = params.set('categoryIDs', options.categoryIDs.join(','));
}
if (options?.minPrice != null) {
params = params.set('minPrice', options.minPrice.toString());
}
if (options?.maxPrice != null) {
params = params.set('maxPrice', options.maxPrice.toString());
}
if (options?.tag) {
params = params.set('tag', options.tag);
}
if (options?.sort) {
params = params.set('sort', options.sort);
}
return this.http.get<any>(`${this.baseUrl}/searchitems`, { params })
.pipe( .pipe(
retry(this.retryConfig),
map(response => ({ map(response => ({
items: this.normalizeItems(response?.items || []), items: this.normalizeItems(response?.items || []),
total: response?.total || 0 total: response?.total || 0
@@ -74,21 +320,9 @@ export class ApiService {
); );
} }
addToCart(itemID: number, quantity: number = 1): Observable<{ message: string }> { // Cart operations — spec uses websession-based paths
return this.http.post<{ message: string }>(`${this.baseUrl}/cart`, { itemID, quantity }); addToCart(sessionId: string, items: Array<{ itemID: number; quantity: number; colour?: string; size?: string; price?: number }>): Observable<any> {
} return this.http.post<any>(`${this.baseUrl}/websession/${sessionId}`, items);
updateCartQuantity(itemID: number, quantity: number): Observable<{ message: string }> {
return this.http.patch<{ message: string }>(`${this.baseUrl}/cart`, { itemID, quantity });
}
removeFromCart(itemIDs: number[]): Observable<{ message: string }> {
return this.http.delete<{ message: string }>(`${this.baseUrl}/cart`, { body: itemIDs });
}
getCart(): Observable<Item[]> {
return this.http.get<Item[]>(`${this.baseUrl}/cart`)
.pipe(map(items => this.normalizeItems(items)));
} }
// Review submission // Review submission
@@ -96,39 +330,42 @@ export class ApiService {
itemID: number; itemID: number;
rating: number; rating: number;
comment: string; comment: string;
username: string | null; sessionID: string;
userId: number | null;
timestamp: string; timestamp: string;
}): Observable<{ message: string }> { }): Observable<{ message: string }> {
return this.http.post<{ message: string }>(`${this.baseUrl}/comment`, reviewData); const { itemID, ...body } = reviewData;
return this.http.post<{ message: string }>(`${this.baseUrl}/items/${itemID}/callback`, body);
} }
// Payment - SBP Integration // Question submission — spec path has typo "questiion"
createPayment(paymentData: { submitQuestion(questionData: {
amount: number; itemID: number;
currency: string; question: string;
siteuserID: string; sessionID: string;
siteorderID: string; timestamp: string;
redirectUrl: string; }): Observable<{ message: string }> {
telegramUsername: string; const { itemID, ...body } = questionData;
items: Array<{ itemID: number; price: number; name: string }>; return this.http.post<{ message: string }>(`${this.baseUrl}/items/${itemID}/questiion`, body);
}): Observable<{ }
// Payment - SBP Integration via websession QR
createPayment(sessionId: string): Observable<{
qrId: string; qrId: string;
qrStatus: string; qrStatus: string;
qrExpirationDate: string; qrExpirationDate: string;
payload: string; Payload: string;
qrUrl: string; qrUrl: string;
}> { }> {
return this.http.post<{ return this.http.post<{
qrId: string; qrId: string;
qrStatus: string; qrStatus: string;
qrExpirationDate: string; qrExpirationDate: string;
payload: string; Payload: string;
qrUrl: string; qrUrl: string;
}>(`${this.baseUrl}/cart`, paymentData); }>(`${this.baseUrl}/websession/${sessionId}/qr`, {});
} }
checkPaymentStatus(qrId: string): Observable<{ checkPaymentStatus(sessionId: string, qrId: string): Observable<{
additionalInfo: string; additionalInfo: string;
paymentPurpose: string; paymentPurpose: string;
amount: number; amount: number;
@@ -141,7 +378,6 @@ export class ApiService {
transactionDate: string; transactionDate: string;
transactionId: number; transactionId: number;
qrExpirationDate: string; qrExpirationDate: string;
phoneNumber: string;
}> { }> {
return this.http.get<{ return this.http.get<{
additionalInfo: string; additionalInfo: string;
@@ -156,8 +392,7 @@ export class ApiService {
transactionDate: string; transactionDate: string;
transactionId: number; transactionId: number;
qrExpirationDate: string; qrExpirationDate: string;
phoneNumber: string; }>(`${this.baseUrl}/websession/${sessionId}/${qrId}`);
}>(`${this.baseUrl}/qr/payment/${qrId}`);
} }
submitPurchaseEmail(emailData: { submitPurchaseEmail(emailData: {
@@ -169,11 +404,11 @@ export class ApiService {
} }
getRandomItems(count: number = 5, categoryID?: number): Observable<Item[]> { 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) { if (categoryID) {
params = params.set('category', categoryID.toString()); params = params.set('category', categoryID.toString());
} }
return this.http.get<Item[]>(`${this.baseUrl}/randomitems`, { params }) return this.http.get<any[]>(`${this.baseUrl}/items/randomitems`, { params })
.pipe(map(items => this.normalizeItems(items))); .pipe(retry(this.retryConfig), map(items => this.normalizeItems(items)));
} }
} }

View File

@@ -1,7 +1,7 @@
import { Injectable, signal, computed } from '@angular/core'; import { Injectable, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable, of, catchError, map, tap } from 'rxjs'; import { Observable, of, catchError, map, tap } from 'rxjs';
import { AuthSession, AuthStatus } from '../models/auth.model'; import { AuthSession, AuthStatus, QrPollResponse } from '../models/auth.model';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
@Injectable({ @Injectable({
@@ -72,7 +72,7 @@ export class AuthService {
/** Generate the Telegram login URL for bot-based auth */ /** Generate the Telegram login URL for bot-based auth */
getTelegramLoginUrl(): string { getTelegramLoginUrl(): string {
const botUsername = (environment as Record<string, unknown>)['telegramBot'] as string || 'dexarmarket_bot'; const botUsername = (environment as Record<string, unknown>)['telegramBot'] as string || 'DexarSupport_bot';
const callbackUrl = encodeURIComponent(`${this.apiUrl}/auth/telegram/callback`); const callbackUrl = encodeURIComponent(`${this.apiUrl}/auth/telegram/callback`);
return `https://t.me/${botUsername}?start=auth_${callbackUrl}`; return `https://t.me/${botUsername}?start=auth_${callbackUrl}`;
} }
@@ -82,6 +82,34 @@ export class AuthService {
return this.getTelegramLoginUrl(); return this.getTelegramLoginUrl();
} }
/** Create a one-time QR login token via backend */
createQrToken(): Observable<{ token: string; url: string }> {
return this.http.post<{ token: string; url: string }>(
`${this.apiUrl}/auth/qr/create`,
{},
{ withCredentials: true }
);
}
/** Poll the QR token status (pending → confirmed / expired) */
pollQrToken(token: string): Observable<QrPollResponse> {
return this.http.get<QrPollResponse>(
`${this.apiUrl}/auth/qr/poll`,
{
params: { token },
withCredentials: true,
}
);
}
/** Sync local cart to the backend session after login */
syncCart(sessionId: string, items: Array<{ itemID: number; quantity: number; colour?: string; size?: string; price?: number }>): Observable<unknown> {
if (!items.length) return of(null);
return this.http.post(`${this.apiUrl}/websession/${sessionId}`, items, {
withCredentials: true,
}).pipe(catchError(() => of(null)));
}
/** Show login dialog (called when user tries to pay without being logged in) */ /** Show login dialog (called when user tries to pay without being logged in) */
requestLogin(): void { requestLogin(): void {
this.showLoginSignal.set(true); this.showLoginSignal.set(true);

View File

@@ -13,6 +13,7 @@ export class CartService {
private cartItems = signal<CartItem[]>([]); private cartItems = signal<CartItem[]>([]);
private isTelegram = typeof window !== 'undefined' && !!window.Telegram?.WebApp; private isTelegram = typeof window !== 'undefined' && !!window.Telegram?.WebApp;
private addingItems = new Set<number>(); private addingItems = new Set<number>();
private initialized = false;
items = this.cartItems.asReadonly(); items = this.cartItems.asReadonly();
itemCount = computed(() => { itemCount = computed(() => {
@@ -31,10 +32,12 @@ export class CartService {
constructor(private apiService: ApiService) { constructor(private apiService: ApiService) {
this.loadCart(); this.loadCart();
// Auto-save whenever cart changes // Auto-save whenever cart changes (skip the initial empty state)
effect(() => { effect(() => {
const items = this.cartItems(); const items = this.cartItems();
if (this.initialized) {
this.saveToStorage(items); this.saveToStorage(items);
}
}); });
} }
@@ -67,9 +70,11 @@ export class CartService {
// No data in CloudStorage, try localStorage // No data in CloudStorage, try localStorage
this.loadFromLocalStorage(); this.loadFromLocalStorage();
} }
this.initialized = true;
}); });
} else { } else {
this.loadFromLocalStorage(); this.loadFromLocalStorage();
this.initialized = true;
} }
} }
@@ -98,7 +103,7 @@ export class CartService {
return false; return false;
} }
addItem(itemID: number, quantity: number = 1): void { addItem(itemID: number, quantity: number = 1, variant?: { colour?: string; size?: string; price?: number; currency?: string }): void {
// Prevent duplicate API calls for same item // Prevent duplicate API calls for same item
if (this.addingItems.has(itemID)) return; if (this.addingItems.has(itemID)) return;
@@ -113,7 +118,14 @@ export class CartService {
this.addingItems.add(itemID); this.addingItems.add(itemID);
this.apiService.getItem(itemID).subscribe({ this.apiService.getItem(itemID).subscribe({
next: (item) => { next: (item) => {
const cartItem: CartItem = { ...item, quantity }; const cartItem: CartItem = {
...item,
quantity,
...(variant?.colour != null && { colour: variant.colour }),
...(variant?.size != null && { size: variant.size }),
...(variant?.price != null && { price: variant.price }),
...(variant?.currency != null && { currency: variant.currency }),
};
this.cartItems.set([...this.cartItems(), cartItem]); this.cartItems.set([...this.cartItems(), cartItem]);
this.addingItems.delete(itemID); this.addingItems.delete(itemID);
}, },

View File

@@ -9,11 +9,18 @@ export interface Language {
enabled: boolean; enabled: boolean;
} }
export interface Currency {
code: string;
symbol: string;
name: string;
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class LanguageService { export class LanguageService {
private currentLanguageSignal = signal<string>('ru'); private currentLanguageSignal = signal<string>('ru');
private currentCurrencySignal = signal<string>('RUB');
languages: Language[] = [ languages: Language[] = [
{ code: 'ru', name: 'Русский', flag: '🇷🇺', flagSvg: '/flags/ru.svg', enabled: true }, { code: 'ru', name: 'Русский', flag: '🇷🇺', flagSvg: '/flags/ru.svg', enabled: true },
@@ -21,7 +28,15 @@ export class LanguageService {
{ code: 'hy', name: 'Հայերեն', flag: '🇦🇲', flagSvg: '/flags/arm.svg', enabled: true } { code: 'hy', name: 'Հայերեն', flag: '🇦🇲', flagSvg: '/flags/arm.svg', enabled: true }
]; ];
currencies: Currency[] = [
{ code: 'RUB', symbol: '₽', name: 'Рубль' },
{ code: 'USD', symbol: '$', name: 'Dollar' },
{ code: 'EUR', symbol: '€', name: 'Euro' },
{ code: 'AMD', symbol: '֏', name: 'Դրամ' },
];
currentLanguage = this.currentLanguageSignal.asReadonly(); currentLanguage = this.currentLanguageSignal.asReadonly();
currentCurrency = this.currentCurrencySignal.asReadonly();
constructor(private router: Router) { constructor(private router: Router) {
// Load saved language from localStorage // Load saved language from localStorage
@@ -29,6 +44,11 @@ export class LanguageService {
if (savedLang && this.languages.find(l => l.code === savedLang && l.enabled)) { if (savedLang && this.languages.find(l => l.code === savedLang && l.enabled)) {
this.currentLanguageSignal.set(savedLang); 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 { 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 */ /** Change language and navigate to the same page with the new prefix */
switchLanguage(langCode: string): void { switchLanguage(langCode: string): void {
const lang = this.languages.find(l => l.code === langCode); 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 { Meta, Title } from '@angular/platform-browser';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
import { Item } from '../models'; import { Item } from '../models';
@@ -10,6 +10,7 @@ import { getDiscountedPrice, getMainImage } from '../utils/item.utils';
export class SeoService { export class SeoService {
private meta = inject(Meta); private meta = inject(Meta);
private title = inject(Title); private title = inject(Title);
private doc = inject(DOCUMENT);
private readonly siteUrl = `https://${environment.domain}`; private readonly siteUrl = `https://${environment.domain}`;
private readonly siteName = environment.brandFullName; private readonly siteName = environment.brandFullName;
@@ -18,13 +19,14 @@ export class SeoService {
* Set Open Graph & Twitter Card meta tags for a product/item page. * Set Open Graph & Twitter Card meta tags for a product/item page.
*/ */
setItemMeta(item: Item): void { setItemMeta(item: Item): void {
const price = item.discount > 0 ? getDiscountedPrice(item) : item.price; const price = item.discount > 0 ? getDiscountedPrice(item) : (item.price ?? 0);
const imageUrl = this.resolveUrl(getMainImage(item)); const imageUrl = this.resolveUrl(getMainImage(item));
const itemUrl = `${this.siteUrl}/item/${item.itemID}`; const itemUrl = `${this.siteUrl}/item/${item.itemID}`;
const description = this.truncate(this.stripHtml(item.description), 160); const description = this.truncate(this.stripHtml(item.description || ''), 160);
const titleText = `${item.name}${this.siteName}`; const titleText = `${item.name || 'Product'}${this.siteName}`;
this.title.setTitle(titleText); this.title.setTitle(titleText);
this.setCanonical(itemUrl);
this.setOrUpdate([ this.setOrUpdate([
// Open Graph // Open Graph
@@ -81,6 +83,7 @@ export class SeoService {
// Remove product-specific tags // Remove product-specific tags
this.meta.removeTag("property='product:price:amount'"); this.meta.removeTag("property='product:price:amount'");
this.meta.removeTag("property='product:price:currency'"); this.meta.removeTag("property='product:price:currency'");
this.removeCanonical();
} }
private setOrUpdate(tags: Array<{ property?: string; name?: string; content: string }>): void { 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 || ''; if (!text || text.length <= maxLength) return text || '';
return text.substring(0, maxLength - 1) + '…'; 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,106 @@
import { Item } from '../models'; import { Item } from '../models';
import { Category } from '../models/category.model';
export function getDiscountedPrice(item: Item): number { 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 { 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'; return item.photos?.[0]?.url || '/assets/images/placeholder.svg';
} }
export function trackByItemId(index: number, item: Item): number { export function getAllImages(item: Item): string[] {
return item.itemID; 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 (already normalized to frontend codes)
const translation = item.translations?.[lang];
if (translation && translation[field]) {
return translation[field]!;
}
// 2. Check names[]/descriptions[] arrays (may have API codes: RU/EN/AM)
// Note: API has typo "valuue" in some responses — handle both
if (field === 'name' && item.names?.length) {
const entry = item.names.find(n => n.language === lang || n.language === lang.toUpperCase() || (lang === 'hy' && n.language === 'AM'));
const val = entry?.value || (entry as any)?.valuue || '';
if (val) return val;
}
// 3. Fallback to base field
if (field === 'name') return item.name;
if (field === 'simpleDescription') return item.simpleDescription || item.description || '';
return '';
}
/**
* Get translated category name for the current language.
*/
export function getTranslatedCategoryName(cat: Category, lang: string): string {
const translation = cat.translations?.[lang];
if (translation?.name) return translation.name;
if (cat.names?.length) {
const entry = cat.names.find(n => n.language === lang || n.language === lang.toUpperCase() || (lang === 'hy' && n.language === 'AM'));
const val = entry?.value || (entry as any)?.valuue || '';
if (val) return val;
}
return cat.name || '';
} }

View File

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

View File

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

View File

@@ -10,9 +10,10 @@ export const environment = {
supportEmail: 'info@dexarmarket.ru', supportEmail: 'info@dexarmarket.ru',
domain: 'dexarmarket.ru', domain: 'dexarmarket.ru',
telegram: '@dexarmarket', telegram: '@dexarmarket',
telegramBot: 'dexarmarket_bot', telegramBot: 'DexarSupport_bot',
phones: { phones: {
russia: '+7 (926) 459-31-57', russia: '+7 (926) 459-31-57',
armenia: '+374 94 86 18 16' armenia: '+374 94 86 18 16'
} },
useMockData: false
}; };

View File

@@ -1,16 +1,17 @@
// Dexar Market Configuration // Dexar Market Configuration
export const environment = { export const environment = {
production: false, production: false,
useMockData: false, // Toggle to test with backOffice mock data
brandName: 'Dexarmarket', brandName: 'Dexarmarket',
brandFullName: 'Dexar Market', brandFullName: 'Dexar Market',
theme: 'dexar', theme: 'dexar',
apiUrl: 'https://api.dexarmarket.ru:445', apiUrl: '/api',
logo: '/assets/images/dexar-logo.svg', logo: '/assets/images/dexar-logo.svg',
contactEmail: 'info@dexarmarket.ru', contactEmail: 'info@dexarmarket.ru',
supportEmail: 'info@dexarmarket.ru', supportEmail: 'info@dexarmarket.ru',
domain: 'dexarmarket.ru', domain: 'dexarmarket.ru',
telegram: '@dexarmarket', telegram: '@dexarmarket',
telegramBot: 'dexarmarket_bot', telegramBot: 'DexarSupport_bot',
phones: { phones: {
russia: '+7 (926) 459-31-57', russia: '+7 (926) 459-31-57',
armenia: '+374 94 86 18 16' armenia: '+374 94 86 18 16'

View File

@@ -140,3 +140,58 @@ a, button, input, textarea, select {
.p-3 { padding: 1.5rem; } .p-3 { padding: 1.5rem; }
.p-4 { padding: 2rem; } .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 --configuration=novo --host 127.0.0.1 --port 4010" --name novo-market
pm2 start "ng serve --host 127.0.0.1 --port 3000" --name dexar-market pm2 start "ng serve --host 127.0.0.1 --port 4001" --name dexar-market