38 Commits

Author SHA1 Message Date
sdarbinyan
a8b415b4bd delivery 2026-06-22 06:56:37 +04:00
sdarbinyan
394ac5ec9d visible and count 2026-06-22 01:45:23 +04:00
sdarbinyan
4fb918f5e4 cleaned up 2026-06-21 23:42:39 +04:00
sdarbinyan
3b802b7c7b delivery 2026-06-21 23:13:01 +04:00
sdarbinyan
1b2a5af2be test 2026-06-21 01:45:05 +04:00
sdarbinyan
6410321895 price 2026-06-20 15:16:25 +04:00
sdarbinyan
51445a7341 telegram desktop 2026-06-20 15:09:15 +04:00
sdarbinyan
56df8632cb styles 2026-06-20 15:08:10 +04:00
sdarbinyan
824bed199c version 2026-06-20 15:05:09 +04:00
sdarbinyan
b5728f1238 test 3 2026-06-20 14:50:16 +04:00
sdarbinyan
04814aeeda reset 2026-06-20 14:40:22 +04:00
sdarbinyan
9386fbc2f8 currency for market 2026-06-20 14:00:28 +04:00
sdarbinyan
a06b654103 logo fix 2026-06-20 13:33:52 +04:00
sdarbinyan
9aaff4d80a removed parazite 2026-06-19 16:13:54 +04:00
sdarbinyan
7a06843bf5 fixes 2026-06-19 15:01:54 +04:00
sdarbinyan
1decc08f77 userId 2026-06-19 12:43:25 +04:00
sdarbinyan
c0cfbcbcbb changed type 2026-06-19 02:00:34 +04:00
sdarbinyan
688c225911 removed parasite 2026-06-19 01:57:27 +04:00
sdarbinyan
3e79304e5c timer 2026-06-18 18:32:36 +04:00
sdarbinyan
e7d8ec8c63 chek 2026-06-18 18:30:20 +04:00
sdarbinyan
1e3cd99c69 redirect 2026-06-18 18:29:39 +04:00
sdarbinyan
3ab67cbe2d empty commit 2026-06-18 16:36:10 +04:00
sdarbinyan
b3c056980d removed mail 2026-06-18 16:35:34 +04:00
sdarbinyan
fb3bb6c77c submited 2026-06-18 15:09:56 +04:00
sdarbinyan
bdc330c885 chagned status 2026-06-18 13:11:05 +04:00
sdarbinyan
31da7f85cf header layout fix 2026-06-10 17:49:22 +04:00
sdarbinyan
69e63fc5f3 fixed cards 2026-06-10 17:40:14 +04:00
sdarbinyan
fe6fc2cb74 changes for ofert 2026-06-10 15:35:23 +04:00
sdarbinyan
80cc90d347 api changes 2026-06-06 22:38:01 +04:00
sdarbinyan
9b5c2dd95c api 2026-06-06 19:25:00 +04:00
sdarbinyan
58e0869916 api changed 2026-06-06 16:16:37 +04:00
sdarbinyan
14bdd3bcd0 api change 2026-06-05 18:23:24 +04:00
sdarbinyan
a10216a392 polling 2026-06-05 17:57:18 +04:00
sdarbinyan
e53c8230e6 payment 2026-06-02 02:12:08 +04:00
sdarbinyan
c6bc05560e change 2026-06-02 01:46:12 +04:00
sdarbinyan
63b0e18396 api change 2026-06-02 00:57:36 +04:00
sdarbinyan
1bec150822 Merge branch 'main' of https://sources.vitanova.network/sdarbinyan/marketplaces 2026-06-01 00:47:57 +04:00
sdarbinyan
4d8dc6b59c api auth 2026-06-01 00:47:26 +04:00
112 changed files with 3689 additions and 1524 deletions

View File

@@ -40,6 +40,10 @@
{ {
"replace": "src/environments/environment.ts", "replace": "src/environments/environment.ts",
"with": "src/environments/environment.production.ts" "with": "src/environments/environment.production.ts"
},
{
"replace": "src/app/interceptors/mock-data.interceptor.ts",
"with": "src/app/interceptors/mock-data.interceptor.production.ts"
} }
], ],
"styles": [ "styles": [
@@ -49,7 +53,7 @@
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
"maximumWarning": "500kB", "maximumWarning": "600kB",
"maximumError": "1MB" "maximumError": "1MB"
}, },
{ {
@@ -92,6 +96,10 @@
{ {
"replace": "src/app/brands/brand-routes.ts", "replace": "src/app/brands/brand-routes.ts",
"with": "src/app/brands/brand-routes.novo.ts" "with": "src/app/brands/brand-routes.novo.ts"
},
{
"replace": "src/app/interceptors/mock-data.interceptor.ts",
"with": "src/app/interceptors/mock-data.interceptor.production.ts"
} }
], ],
"index": "src/index.novo.html", "index": "src/index.novo.html",
@@ -124,7 +132,7 @@
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
"maximumWarning": "500kB", "maximumWarning": "600kB",
"maximumError": "1MB" "maximumError": "1MB"
}, },
{ {
@@ -158,6 +166,10 @@
{ {
"replace": "src/app/brands/brand-routes.ts", "replace": "src/app/brands/brand-routes.ts",
"with": "src/app/brands/brand-routes.lavero.ts" "with": "src/app/brands/brand-routes.lavero.ts"
},
{
"replace": "src/app/interceptors/mock-data.interceptor.ts",
"with": "src/app/interceptors/mock-data.interceptor.production.ts"
} }
], ],
"index": "src/index.lavero.html", "index": "src/index.lavero.html",
@@ -190,7 +202,7 @@
"budgets": [ "budgets": [
{ {
"type": "initial", "type": "initial",
"maximumWarning": "500kB", "maximumWarning": "600kB",
"maximumError": "1MB" "maximumError": "1MB"
}, },
{ {

View File

@@ -0,0 +1,327 @@
# Telegram UserAuth Backend Contract
This document extracts the existing Telegram login flow into a repo-neutral contract for reuse in other projects.
The UI behavior, payloads, polling cadence, and session model stay the same. Only route names and cookie naming are generalized.
## Endpoint Renaming
| Current app contract | Reusable contract |
|---|---|
| `GET /auth/session` | `GET /userauth/session` |
| `POST /auth/qr/create` | `POST /userauth/qr/create` |
| `GET /auth/qr/poll?token=...` | `GET /userauth/qr/poll?token=...` |
| `POST /auth/qr/confirm` | `POST /userauth/qr/confirm` |
| `GET /auth/telegram/callback` | `GET /userauth/telegram/callback` |
| `POST /auth/logout` | `POST /userauth/logout` |
| `POST /websession/{sessionId}` | `POST /usersession/{sessionId}` |
| Cookie `dx_session` | Cookie `userauth_session` |
## Flow Summary
There are two supported flows.
### 1. Direct login from button
1. Frontend opens `https://t.me/{botUsername}?start=auth_{callbackUrl}`.
2. Telegram bot creates a session and sends the user a login button.
3. The button points to `GET /userauth/telegram/callback?token={sessionId}`.
4. Backend sets `userauth_session` cookie and redirects back to the storefront.
5. Frontend calls `GET /userauth/session` and becomes authenticated.
### 2. QR login from desktop
1. Frontend opens dialog.
2. Frontend calls `POST /userauth/qr/create`.
3. Backend returns `{ token, url }` where `url` is a Telegram deep link.
4. Frontend renders a QR from that URL.
5. User scans QR and bot calls `POST /userauth/qr/confirm`.
6. Frontend polls `GET /userauth/qr/poll?token=...` every 3 seconds.
7. When status becomes `confirmed`, backend returns session payload and sets the cookie.
8. Frontend syncs local cart using `POST /usersession/{sessionId}`.
## Session Shape
The frontend expects this exact response shape for the authenticated session.
```json
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"telegramUserId": 123456789,
"username": "ivan_petrov",
"displayName": "Ivan Petrov",
"active": true,
"expiresAt": "2026-05-21T14:30:00Z"
}
```
| Field | Type | Required | Notes |
|---|---|---|---|
| `sessionId` | string | yes | Session identifier used in cart sync |
| `telegramUserId` | number | yes | Telegram user ID |
| `username` | string or null | no | Telegram username |
| `displayName` | string | yes | User-facing full name |
| `active` | boolean | yes | `false` means expired session |
| `expiresAt` | ISO 8601 string | yes | Used by frontend refresh scheduling |
Recommended TTL:
- Session TTL: 24 hours
- QR token TTL: 5 minutes
## HTTP Contract
### `POST /userauth/qr/create`
Creates a one-time QR login token when the dialog opens.
Request body:
```json
{}
```
Response `200`:
```json
{
"token": "dG9rZW4tYWJjMTIz",
"url": "https://t.me/userauth_bot?start=login_dG9rZW4tYWJjMTIz"
}
```
Requirements:
- Generate a cryptographically secure token.
- Save token with status `pending`.
- Return a Telegram deep link in `url`.
- Rate limit to 5 requests per minute per IP.
### `GET /userauth/qr/poll?token={token}`
Called every 3 seconds until confirmation or expiration.
Possible responses:
Pending:
```json
{ "status": "pending" }
```
Confirmed:
```json
{
"status": "confirmed",
"session": {
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"telegramUserId": 123456789,
"username": "ivan_petrov",
"displayName": "Ivan Petrov",
"active": true,
"expiresAt": "2026-05-21T14:30:00Z"
}
}
```
Expired:
```json
{ "status": "expired" }
```
Behavior:
- If confirmed, set cookie `userauth_session` in the response.
- Delete or invalidate the QR token after the first successful confirmed poll.
- If token is unknown or expired, return `status: "expired"`.
### `POST /userauth/qr/confirm`
Internal endpoint called by the Telegram bot after the user scans the QR code.
Required header:
```text
X-Bot-Secret: <shared secret between bot and backend>
```
Request body:
```json
{
"token": "dG9rZW4tYWJjMTIz",
"telegram_user": {
"id": 123456789,
"first_name": "Ivan",
"last_name": "Petrov",
"username": "ivan_petrov"
}
}
```
Response `200`:
```json
{ "status": "ok" }
```
Behavior:
- Validate `X-Bot-Secret`.
- Validate token exists and is still `pending`.
- Create a user session.
- Store session ID on the QR token.
- Mark QR token as `confirmed`.
### `GET /userauth/session`
Returns the currently active session based on the cookie.
Frontend behavior depends on this endpoint in two places:
- initial auth check on app startup
- fallback polling if QR token creation fails
Response `200`:
```json
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"telegramUserId": 123456789,
"username": "ivan_petrov",
"displayName": "Ivan Petrov",
"active": true,
"expiresAt": "2026-05-21T14:30:00Z"
}
```
Error handling:
- Any non-200 response is treated by the frontend as unauthenticated.
### `GET /userauth/telegram/callback?token={sessionId}`
Used for direct Telegram login from the primary button flow.
Behavior:
- Read the `token` query param.
- Resolve it to a valid active session.
- Set cookie `userauth_session`.
- Redirect user to the storefront URL.
### `POST /userauth/logout`
Clears the backend session and expires the cookie.
Request body:
```json
{}
```
Response `200`:
```json
{ "message": "ok" }
```
### `POST /usersession/{sessionId}`
Synchronizes local cart immediately after successful login.
Request body:
```json
[
{
"itemID": 123,
"quantity": 2,
"colour": "#ff0000",
"size": "XL",
"price": 1500
}
]
```
Notes:
- This payload is unchanged from the existing implementation.
- `price` is already discounted on the frontend side.
- The frontend skips the call if cart is empty.
## Telegram Deep Link Format
Direct login link format:
```text
https://t.me/{botUsername}?start=auth_{urlEncodedCallbackUrl}
```
QR login link format:
```text
https://t.me/{botUsername}?start=login_{qrToken}
```
Important limit:
- Telegram limits the `start` payload to 64 characters.
- A base64url encoding of 32 random bytes plus `login_` fits safely.
## Cookie Requirements
Use these cookie settings for the frontend to work correctly across site and API origins.
| Property | Value |
|---|---|
| Name | `userauth_session` |
| Path | `/` |
| HttpOnly | `true` |
| Secure | `true` |
| SameSite | `None` |
| MaxAge | `86400` |
| Domain | your shared parent domain, for example `.example.com` |
## CORS Requirements
Because the frontend sends credentials, backend must return an explicit origin.
Required headers:
```text
Access-Control-Allow-Origin: https://your-frontend.example
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
```
Do not use `*` for `Access-Control-Allow-Origin` together with credentials.
## Frontend Runtime Expectations
The current dialog behavior is fixed and should be preserved by backend responses.
- QR polling interval: every 3 seconds
- QR expiration on frontend: after 100 checks
- If QR creation fails, frontend falls back to direct login URL and session polling
- After login, frontend closes the dialog and re-checks session
## Minimal Backend Checklist
- Implement all six `userauth` endpoints and the `usersession` sync endpoint.
- Store sessions for 24 hours.
- Store QR tokens for 5 minutes.
- Protect `POST /userauth/qr/confirm` with `X-Bot-Secret`.
- Set `userauth_session` cookie on confirmed QR poll and direct callback.
- Return the exact session JSON shape.
- Support credentialed CORS.
## Bot Checklist
- Handle `/start login_{token}` and call `POST /userauth/qr/confirm`.
- Handle `/start auth_{callbackUrl}` and provide a button that opens the callback URL.
- Send success and expiration messages back to the user.
- Share the same `X-Bot-Secret` value with backend.

View File

@@ -0,0 +1,551 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Telegram Login Dialog</title>
<style>
:root {
--bg-page: linear-gradient(135deg, #f4f7fb 0%, #e8eef4 100%);
--bg-card: #ffffff;
--bg-hover: #f0f0f0;
--text-primary: #1a1a1a;
--text-secondary: #666666;
--accent-color: #497671;
--accent-light: rgba(73, 118, 113, 0.1);
--telegram: #2aabee;
--telegram-hover: #229ed9;
--border: #e8e8e8;
--shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
color: var(--text-primary);
background: var(--bg-page);
}
.page {
min-height: 100vh;
display: grid;
grid-template-columns: minmax(320px, 448px) minmax(320px, 560px);
gap: 32px;
padding: 40px 32px;
align-items: center;
justify-content: center;
}
.panel {
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: 28px;
padding: 24px;
box-shadow: 0 18px 50px rgba(38, 52, 73, 0.12);
backdrop-filter: blur(14px);
}
.info h1 {
margin: 0 0 12px;
font-size: 32px;
line-height: 1.1;
}
.info p {
margin: 0 0 18px;
color: var(--text-secondary);
line-height: 1.6;
}
.state-switcher {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 20px 0 24px;
}
.state-switcher button {
border: 1px solid #cfd8e3;
border-radius: 999px;
background: #fff;
color: var(--text-primary);
padding: 10px 14px;
font-size: 14px;
cursor: pointer;
transition: 0.2s ease;
}
.state-switcher button.active {
border-color: var(--accent-color);
background: var(--accent-light);
color: var(--accent-color);
}
.api-grid {
display: grid;
gap: 12px;
margin-top: 20px;
}
.api-card {
background: #fff;
border: 1px solid #eef2f7;
border-radius: 16px;
padding: 14px 16px;
}
.api-card strong {
display: block;
margin-bottom: 6px;
font-size: 14px;
}
.api-card code {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
background: #f3f7fb;
color: #21425f;
font-size: 13px;
}
.api-card p {
margin: 8px 0 0;
font-size: 13px;
}
.login-overlay {
position: relative;
min-height: 700px;
border-radius: 28px;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.2s ease;
padding: 16px;
}
.login-dialog {
position: relative;
background: var(--bg-card);
border-radius: 20px;
padding: 32px 28px;
max-width: 400px;
width: 100%;
text-align: center;
box-shadow: var(--shadow);
animation: scaleIn 0.25s ease;
}
.close-btn {
position: absolute;
top: 12px;
right: 12px;
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
background: var(--bg-hover);
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.close-btn:hover {
background: #e0e0e0;
color: #333;
}
.login-icon {
margin: 0 auto 16px;
width: 72px;
height: 72px;
border-radius: 50%;
background: var(--accent-light);
color: var(--accent-color);
display: flex;
align-items: center;
justify-content: center;
}
.login-dialog h2 {
margin: 0 0 8px;
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
}
.login-desc {
margin: 0 0 24px;
font-size: 14px;
color: var(--text-secondary);
line-height: 1.5;
}
.telegram-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
padding: 14px 24px;
border: none;
border-radius: 12px;
background: var(--telegram);
color: #fff;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.telegram-btn:hover {
background: var(--telegram-hover);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(42, 171, 238, 0.3);
}
.telegram-btn:active {
transform: translateY(0);
}
.tg-icon {
flex-shrink: 0;
}
.qr-section {
margin-top: 20px;
}
.qr-hint {
margin: 0 0 12px;
font-size: 13px;
color: #999;
}
.qr-container {
display: inline-flex;
padding: 12px;
background: #fff;
border-radius: 12px;
border: 1px solid var(--border);
}
.qr-container img {
display: block;
border-radius: 4px;
}
.qr-loading {
align-items: center;
justify-content: center;
width: 204px;
height: 204px;
}
.qr-loading .spinner,
.login-status .spinner {
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.qr-loading .spinner {
width: 32px;
height: 32px;
border: 3px solid #e0e0e0;
border-top-color: var(--accent-color);
}
.qr-expired {
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
width: 204px;
height: 204px;
cursor: pointer;
color: #999;
transition: color 0.2s ease;
}
.qr-expired:hover {
color: var(--accent-color);
}
.qr-expired span {
font-size: 13px;
}
.login-note {
margin: 16px 0 0;
font-size: 12px;
color: #999;
line-height: 1.4;
}
.login-status {
display: none;
align-items: center;
justify-content: center;
gap: 10px;
padding: 16px;
color: var(--text-secondary);
font-size: 14px;
}
.login-status .spinner {
width: 20px;
height: 20px;
border: 2px solid #e0e0e0;
border-top-color: var(--accent-color);
}
.dialog-content[data-state="checking"] .login-status {
display: flex;
}
.dialog-content[data-state="checking"] .action-block,
.dialog-content[data-state="loading"] .qr-ready,
.dialog-content[data-state="loading"] .qr-expired,
.dialog-content[data-state="expired"] .qr-ready,
.dialog-content[data-state="expired"] .qr-loading,
.dialog-content[data-state="error"] .qr-loading,
.dialog-content[data-state="error"] .qr-expired,
.dialog-content[data-state="checking"] .qr-section,
.dialog-content[data-state="checking"] .login-note {
display: none;
}
.dialog-content[data-state="ready"] .qr-loading,
.dialog-content[data-state="ready"] .qr-expired,
.dialog-content[data-state="ready"] .qr-error,
.dialog-content[data-state="loading"] .qr-ready,
.dialog-content[data-state="loading"] .qr-expired,
.dialog-content[data-state="loading"] .qr-error,
.dialog-content[data-state="expired"] .qr-loading,
.dialog-content[data-state="expired"] .qr-ready,
.dialog-content[data-state="expired"] .qr-error,
.dialog-content[data-state="error"] .qr-loading,
.dialog-content[data-state="error"] .qr-expired {
display: none;
}
.dialog-content[data-state="error"] .qr-ready {
display: inline-flex;
}
.metadata {
margin-top: 22px;
padding-top: 18px;
border-top: 1px solid #e9edf2;
font-size: 13px;
color: var(--text-secondary);
}
.metadata ul {
margin: 10px 0 0;
padding-left: 18px;
}
.metadata li + li {
margin-top: 6px;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 980px) {
.page {
grid-template-columns: 1fr;
padding: 24px 16px 32px;
}
.login-overlay {
min-height: 560px;
}
}
@media (max-width: 480px) {
.panel {
border-radius: 22px;
padding: 18px;
}
.login-dialog {
padding: 24px 20px;
border-radius: 16px;
}
.qr-container img {
width: 140px;
height: 140px;
}
.qr-loading,
.qr-expired {
width: 164px;
height: 164px;
}
}
</style>
</head>
<body>
<main class="page">
<section class="panel info">
<h1>Telegram Login Dialog</h1>
<p>
Standalone extraction of the current login popup: same layout, same visual states,
same QR flow, but with reusable neutral endpoint names for moving into a separate repo.
</p>
<div class="state-switcher" aria-label="Dialog state switcher">
<button class="active" data-state-btn="ready" type="button">Ready</button>
<button data-state-btn="loading" type="button">QR Loading</button>
<button data-state-btn="checking" type="button">Checking</button>
<button data-state-btn="expired" type="button">Expired</button>
<button data-state-btn="error" type="button">Fallback</button>
</div>
<div class="api-grid">
<div class="api-card">
<strong>Start QR session</strong>
<code>POST /userauth/qr/create</code>
<p>Returns a one-time token and Telegram deeplink for the QR image.</p>
</div>
<div class="api-card">
<strong>Poll QR confirmation</strong>
<code>GET /userauth/qr/poll?token=...</code>
<p>Returns pending, confirmed, or expired. On confirmed, also returns the user session.</p>
</div>
<div class="api-card">
<strong>Read current session</strong>
<code>GET /userauth/session</code>
<p>Cookie-based session lookup used for initial auth check and fallback polling.</p>
</div>
<div class="api-card">
<strong>Sync cart after login</strong>
<code>POST /usersession/{sessionId}</code>
<p>Existing cart payload is preserved. Only the namespace is generalized for reuse.</p>
</div>
</div>
<div class="metadata">
<strong>Behavior kept intact</strong>
<ul>
<li>Open Telegram directly from the primary button.</li>
<li>Show QR immediately and poll every 3 seconds.</li>
<li>Expire the QR after 100 checks and allow manual refresh.</li>
<li>Re-check cookie session if QR creation fails.</li>
</ul>
</div>
</section>
<section class="panel">
<div class="login-overlay">
<div class="login-dialog">
<button class="close-btn" type="button" aria-label="Close dialog">
<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>
</svg>
</button>
<div class="dialog-content" data-state="ready" id="dialog-content">
<div class="login-icon">
<svg width="48" height="48" 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"></path>
</svg>
</div>
<h2>Login required</h2>
<p class="login-desc">Please log in via Telegram to proceed with your order.</p>
<div class="login-status checking">
<div class="spinner"></div>
<span>Checking...</span>
</div>
<div class="action-block">
<button class="telegram-btn" type="button">
<svg class="tg-icon" width="22" height="22" 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"></path>
</svg>
Log in with Telegram
</button>
<div class="qr-section">
<p class="qr-hint">Or scan the QR code</p>
<div class="qr-container qr-loading">
<div class="spinner"></div>
</div>
<div class="qr-container qr-ready">
<img
src="https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=https%3A%2F%2Ft.me%2Fuserauth_bot%3Fstart%3Dlogin_sample_token"
alt="QR Code"
width="180"
height="180"
loading="eager"
/>
</div>
<div class="qr-container qr-expired" role="button" tabindex="0" aria-label="Refresh QR code">
<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>
<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"></path>
</svg>
<span>QR code expired. Click to refresh</span>
</div>
</div>
<p class="login-note">You will be redirected back after login.</p>
</div>
</div>
</div>
</div>
</section>
</main>
<script>
const content = document.getElementById('dialog-content');
const buttons = document.querySelectorAll('[data-state-btn]');
buttons.forEach((button) => {
button.addEventListener('click', () => {
const state = button.getAttribute('data-state-btn');
content.setAttribute('data-state', state);
buttons.forEach((candidate) => candidate.classList.remove('active'));
button.classList.add('active');
});
});
</script>
</body>
</html>

View File

@@ -1,144 +1,24 @@
bro we need to add another project, under another port. it must be the same as dexar bro please read carefully.
here are infos: this must be for all projects.
colors: #FD7300 #3AAA35 #E24B00 At first here is the API for only auth process:
1. lavero.store https://users.vitanova.network:456/ping
2. info@lavero.storre
3. logo - public\assets\images\lavero\lavero-logo.png and here are other stuff regarding it:
//Logout user by sessionID
r.DELETE("/users/sessions/:webSessionID", Logout)
//creates new session for user and send code for activation
r.POST("/users/sessions", newWebSession)
r.GET("/users/sessions/:webSessionID", getWebSession)
As you got all the info, keep all the structure of api above.
Now when the user clicks on login, we must show the QR and the link of the telegram bot, which we already have (btw sho me, so i see wheter it is true or not)
and add a query param "?start=GUID" and generate a guid there.
after we post it ad a websession, we have to get it like this " r.GET("/users/sessions/:webSessionID", getWebSession)" every 5 secs untill we get a status true
if we will be loged in, we have to keep that webSessionID in the cookies for an hour
1. if we open our website, we have to check the cookies and do a request for that websession
2. if we are not loged in, then we will loge in one more time
info for legal: any questions?
"ՆՇԱԹԵՐԹ
պետական գրանցման
մասին տեղեկությունների
«ԼԱՎԵՐՈ»
Սահմանափակ
պատասխանատվությամբ ընկերություն
Գրանցված է Հայաստանի Հանրապետության
արդարադատության նախարարության իրավաբանական
անձանց պետական ռեգիստրի գործակալության կողմից
Գրանցված է`
Աշխատակից՝
ՀՀ ԱՆ իրավաբանական անձանց պետական
ռեգիստրի գործակալություն
Ինքնաշխատ էլեկտրոնային գրանցում
Գրանցման ամսաթիվ՝
Գրանցման համար՝
ՀՎՀՀ՝
18.05.2026
999.110.1583686
03590442
Պետական գրանցման մասին տեղեկությունների նշաթերթը էլեկտրոնային եղանակով ձևավորված է ՀՀ ԱՆ
իրավաբանական անձանց պետական ռեգիստրի տեղեկատվական համակարգի կողմից և հանդիսանում է
«ԼԱՎԵՐՈ» Սահմանափակ պատասխանատվությամբ ընկերություն կանոնադրության անբաժանելի մասը։
Սույն կանոնադրության գրանցումը հավաստվում է համակարգի կողմից գեներացված RBBE-3D66
EF54-3DAE ծածկագրով քաղվածքով, որի իսկությունը և արդիականությունը կարող է ստուգվել
փաստաթղթերի վավերականության ստուգման միասնական համակարգում (e-verify.am):
Սույն կանոնադրությունը կազմված է 2026-05-18 և բաղկացած է 4 թերթից:
Հ Ա Ս Տ Ա Տ Վ Ա Ծ Է
«ԼԱՎԵՐՈ»
սահմանափակ
պատասխանատվությամբ
ընկերության հիմնադիրների
2026-05-18 կայացրած
Հիմնադիր ժողովի որոշմամբ
արձանագրություն թիվ` 1
տնօրեն` ԳԵՎՈՐԳ ՄԱԹԵՎՈՍՅԱՆ
Գ Ր Ա Ն Ց Վ Ա Ծ Է
ՀԱՅԱՍՏԱՆԻ ՀԱՆՐԱՊԵՏՈՒԹՅԱՆ
ԱՐԴԱՐԱԴԱՏՈՒԹՅԱՆ ՆԱԽԱՐԱՐՈՒԹՅԱՆ
ԻՐԱՎԱԲԱՆԱԿԱՆ ԱՆՁԱՆՑ
ՊԵՏԱԿԱՆ ՌԵԳԻՍՏՐԻ ԳՈՐԾԱԿԱԼՈՒԹՅԱՆ
ԿՈՂՄԻՑ
Աշխատակից՝ ՀՀ ԻԱ Պետական ռեգիստրի էլ․
համակարգ
Գրանցման հ/հ՝ - 999.110.1583686
Գրանցման ամսաթիվ՝ 18.05.2026
Հարկ վճարողի հաշվառման
համար (ՀՎՀՀ)` 03590442
«ԼԱՎԵՐՈ»
ՍԱՀՄԱՆԱՓԱԿ ՊԱՏԱՍԽԱՆԱՏՎՈՒԹՅԱՄԲ ԸՆԿԵՐՈՒԹՅԱՆ
ԿԱՆՈՆԱԴՐՈՒԹՅՈՒՆ
2026
Էջ 1 4-ից
Սույն կանոնադրությունը կազմված է 2026-05-18 և բաղկացած է 4 թերթից:
1. ԸՆԴՀԱՆՈՒՐ ԴՐՈՒՅԹՆԵՐ
1.1. «ԼԱՎԵՐՈ» սահմանափակ պատասխանատվությամբ ընկերությունը (հետագայում` Ընկերություն)
համարվում է շահույթ ստանալու նպատակով հիմնադրված առևտրային կազմակերպություն
հանդիսացող իրավաբանական անձ, որի կանոնադրական կապիտալը բաժանված է սույն
կանոնադրությամբ սահմանված չափերով բաժնեմասերի:
1.2. Ընկերությունն իր գործունեության ընթացքում ղեկավարվում է Հայաստանի Հանրապետության
Քաղաքացիական Օրենսդրությամբ, այլ իրավական ակտերով և սույն կանոնադրությամբ:
1.3. Ընկերության ֆիրմային անվանումն է`
հայերեն լրիվ՝ «ԼԱՎԵՐՈ» սահմանափակ պատասխանատվությամբ ընկերություն
կրճատ՝ «ԼԱՎԵՐՈ» ՍՊԸ
ռուսերեն լրիվ՝ «ЛАВЕРО» Общество с Ограничеенной Ответственностью
կրճատ՝ «ЛАВЕРО» ՕՕՕ
անգլերեն լրիվ՝ «LAVERO» LIMITED LIABILITY COMPANY
կրճատ՝ «LAVERO» LLC
1.4. Ընկերության գտնվելու վայրը և իրավաբանական (փոստային) հասցեն է` ԿՈՏԱՅՔ, ԱԲՈՎՅԱՆ, ՎԵՐԻՆ
ՊՏՂՆԻ, 3-րդ փողոց, 28, Տ Փ/Դ՝ 2228:
2. ԸՆԿԵՐՈՒԹՅԱՆ ՄԱՍՆԱԿԻՑՆԵՐԸ, ՆՐԱՆՑ ԻՐԱՎՈՒՆՔՆԵՐՆ ՈՒ ՊԱՐՏԱԿԱՆՈՒԹՅՈՒՆՆԵՐԸ
2.1. Անձը համարվում է ընկերության մասնակից իրավաբանական անձանց պետական գրանցումն
իրականացնող մարմնի կողմից Ընկերության մասնակիցների գրանցամատյանում նրա որպես այդպիսին
գրանցվելու պահից:
2.2. Ընկերության կանոնադրական կապիտալում բաժնեմասը փոխանցելը.
2.2.1.Ընկերության մասնակիցն իրավունք ունի իր բաժնեմասը (դրա մասը) վաճառել կամ այլ` օրենքով
չարգելված ձևով, օտարել Ընկերության մեկ կամ մի քանի մասնակիցների, ինչպես նաև երրորդ անձանց:
Ընդ որում, մյուս մասնակիցներն ունեն այդ բաժնեմասը նույն գնով ձեռքբերման նախապատվության
իրավունք՝ Ընկերության կանոնադրական կապիտալում իրենց բաժնեմասերին համամասնորեն: Եթե
Ընկերության մյուս մասնակիցները չեն օգտագործել բաժնեմասը (դրա մասը) գնելու իրենց
նախապատվության իրավունքը կամ այն օգտագործել են մասամբ, ապա Ընկերությունն ունի այդ
բաժնեմասը նույն գնով ձեռքբերման նախապատվության իրավունք:
2.2.2.Երրորդ անձանց օտարման անհնարինության դեպքում մասնակցի պահանջով Ընկերությունն ինքը
պարտավոր է ձեռք բերել մասնակցի բաժնեմասը, այն իրացնել մեկ տարվա ընթացքում այլ
մասնակիցների կամ երրորդ անձանց կամ որոշում ընդունել բաժնեմասի չբաշխված մասի մարման
ճանապարհով Ընկերության կանոնադրական կապիտալի նվազեցման մասին:
2.2.3.Նախապատվության իրավունքի (այլ մասնակիցների կողմից վաճառվող բաժնեմասերի ձեռքբերման)
իրականացման ժամկետը սահմանվում է մեկ ամիս:
2.3. Ընկերությունից դուրս եկող մասնակցի բաժնեմասը դուրս գալու դիմումը ներկայացնելու պահից
փոխանցվում է ընկերությանը:
2.4. Ընկերության մասնակիցները կարող են տեղեկություններ ստանալ, ընկերության գործունեության մասին
գաղտնի փաստաթղթերից բացի, ծանոթանալ հաշվապահական հաշվառման, հաշվետվության,
ընկերության արտադրատնտեսական գործունեության այլ փաստաթղթերի հետ:
2.5. Ընկերության մասնակիցները պարտավոր են` Ընկերության մասնակիցների ընդհանուր ժողովի սահմանված կարգի համաձայն կատարել
ներդրումներ Ընկերության կանոնադրական կապիտալում:
3. ԸՆԿԵՐՈՒԹՅԱՆ ԿԱՆՈՆԱԴՐԱԿԱՆ ԿԱՊԻՏԱԼԸ
3.1. Ընկերության կանոնադրական կապիտալը կազմում է 1000 դրամ:
Կանոնադրական կապիտալը սահմանում է պարտատերերի շահերը երաշխավորող ընկերության գույքի
նվազագույն չափը:
Ընկերության 100% բաժնեմասերը տեղաբաշխված են, լրիվ վճարված և պատկանում են սույն
կանոնադրության հավելվածում նշված մասնակցին (մասնակիցներին):
Էջ 2 4-ից
Սույն կանոնադրությունը կազմված է 2026-05-18 և բաղկացած է 4 թերթից:
4. ԸՆԿԵՐՈՒԹՅԱՆ ԿԱՌԱՎԱՐՈՒՄԸ
4.1. Ընկերության կառավարման մարմիններն են Ընկերության մասնակիցների ընդհանուր ժողովը և
Ընկերության գործադիր մարմինը՝ տնօրենը:
4.2. Ընկերության կառավարման բարձրագույն մարմինը Ընկերության մասնակիցների ընդհանուր ժողովն է,
որն ունի Ընկերության կառավարման և գործունեության ցանկացած հարցի վերջնական լուծման
իրավունք:
4.3. Ընկերության մասնակիցների հերթական ընդհանուր ժողովը հրավիրում է Ընկերության տնօրենի կողմից
տարեկան մեկ անգամ ֆինանսական տարվա ավարտից ոչ շուտ, քան երկու ամիս ոչ ուշ, քան չորս ամիս
անց: Ընկերությունը պարտավոր է ամեն տարի գումարել մասնակիցների հերթական ընդհանուր ժողովը`
Ընկերության գործունեության տարեկան արդյունքները հաստատելու համար:
4.4. Ընկերության ընթացիկ գործունեության ղեկավարումն իրականացնում է տնօրենը, որն ընտրվում է
Ընկերության մասնակիցների ընդհանուր ժողովի կողմից:
Տնօրենը իրավունք ունի առանց լիազորության գործարքներ կատարել Ընկերության անունից, եթե
գործարքի գումարը չի գերազանցում ընկերության զուտ ակտիվների մեծության 25%-ից:
Էջ 3 4-ից
Սույն կանոնադրությունը կազմված է 2026-05-18 և բաղկացած է 4 թերթից:
«ԼԱՎԵՐՈ»
ՍԱՀՄԱՆԱՓԱԿ ՊԱՏԱՍԽԱՆԱՏՎՈՒԹՅԱՄԲ ԸՆԿԵՐՈՒԹՅԱՆ
մասնակիցների ցուցակ
Մասնակցի քաղաքացիությունը, անունը, ազգանունը (պետությունը
որտեղ հիմնադրվել է, անվանումը) անձնագրի(գրանցման) տվյալները
հասցեն (գտնվելու վայրը)
Բաժնեմասի
չափը - %
Բաժնեմասը
ՀՀ դրամով
ԳԵՎՈՐԳ ՄԱԹԵՎՈՍՅԱՆ ՀԱՄԼԵՏԻ
Քաղաքացիություն՝ ՀԱՅԱՍՏԱՆ
ՀԾՀ` 2723060837
ՀԱՅԱՍՏԱՆ ԿՈՏԱՅՔ ԱԲՈՎՅԱՆ ՎԵՐԻՆ ՊՏՂՆԻ 3 Փ. Տ 28 2228
100.0 % 1000.0
Էջ 4 4-ից"

968
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,24 +12,21 @@
"build:dexar": "ng build --configuration=production", "build:dexar": "ng build --configuration=production",
"build:novo": "ng build --configuration=novo-production", "build:novo": "ng build --configuration=novo-production",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
"test": "ng test",
"lavero": "ng serve --configuration=lavero --port 4202 --proxy-config proxy.conf.lavero.json", "lavero": "ng serve --configuration=lavero --port 4202 --proxy-config proxy.conf.lavero.json",
"start:lavero": "ng serve --configuration=lavero --port 4202", "start:lavero": "ng serve --configuration=lavero --port 4202",
"build:lavero": "ng build --configuration=lavero-production" "build:lavero": "ng build --configuration=lavero-production"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^21.1.5", "@angular/animations": "21.1.5",
"@angular/cdk": "^21.1.5", "@angular/cdk": "21.1.5",
"@angular/common": "^21.0.6", "@angular/common": "21.1.5",
"@angular/compiler": "^21.0.6", "@angular/compiler": "21.1.5",
"@angular/core": "^21.0.6", "@angular/core": "21.1.5",
"@angular/forms": "^21.0.6", "@angular/forms": "21.1.5",
"@angular/material": "^21.1.5", "@angular/platform-browser": "21.1.5",
"@angular/platform-browser": "^21.0.6", "@angular/router": "21.1.5",
"@angular/platform-browser-dynamic": "^21.1.5", "@angular/service-worker": "21.1.5",
"@angular/router": "^21.0.6",
"@angular/service-worker": "^21.0.6",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primeng": "^21.0.3", "primeng": "^21.0.3",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
@@ -37,16 +34,9 @@
"zone.js": "~0.16.0" "zone.js": "~0.16.0"
}, },
"devDependencies": { "devDependencies": {
"@angular/build": "^21.0.6", "@angular/build": "21.1.5",
"@angular/cli": "^21.0.6", "@angular/cli": "21.1.5",
"@angular/compiler-cli": "^21.0.6", "@angular/compiler-cli": "21.1.5",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.13.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.9.3" "typescript": "~5.9.3"
} }
} }

View File

@@ -18,6 +18,10 @@
} }
<router-outlet></router-outlet> <router-outlet></router-outlet>
</main> </main>
@defer (on viewport) {
<app-footer></app-footer> <app-footer></app-footer>
<app-telegram-login /> } @placeholder {
<div class="footer-placeholder" aria-hidden="true"></div>
}
<!-- <app-telegram-login /> -->
} }

View File

@@ -5,6 +5,10 @@
flex-direction: column; flex-direction: column;
} }
.footer-placeholder {
min-height: 1px;
}
.server-check-overlay, .server-check-overlay,
.server-error-overlay { .server-error-overlay {
display: flex; display: flex;

View File

@@ -2,11 +2,10 @@
import { Component, OnInit, signal, ApplicationRef, inject, DestroyRef } from '@angular/core'; import { Component, OnInit, signal, ApplicationRef, inject, DestroyRef } from '@angular/core';
import { Router, RouterOutlet, NavigationEnd } from '@angular/router'; import { Router, RouterOutlet, NavigationEnd } from '@angular/router';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { HttpClient } from '@angular/common/http';
import { HeaderComponent } from './components/header/header.component'; import { HeaderComponent } from './components/header/header.component';
import { FooterComponent } from './components/footer/footer.component'; import { FooterComponent } from './components/footer/footer.component';
import { BackButtonComponent } from './components/back-button/back-button.component'; import { BackButtonComponent } from './components/back-button/back-button.component';
import { TelegramLoginComponent } from './components/telegram-login/telegram-login.component';
import { ApiService } from './services';
import { interval, concat } from 'rxjs'; import { interval, concat } from 'rxjs';
import { filter, first } from 'rxjs/operators'; import { filter, first } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -17,7 +16,7 @@ import { TranslateService } from './i18n/translate.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
imports: [RouterOutlet, HeaderComponent, FooterComponent, BackButtonComponent, TelegramLoginComponent, TranslatePipe], imports: [RouterOutlet, HeaderComponent, FooterComponent, BackButtonComponent, TranslatePipe],
templateUrl: './app.html', templateUrl: './app.html',
styleUrl: './app.scss' styleUrl: './app.scss'
}) })
@@ -28,7 +27,7 @@ export class App implements OnInit {
serverAvailable = signal(false); serverAvailable = signal(false);
private destroyRef = inject(DestroyRef); private destroyRef = inject(DestroyRef);
private apiService = inject(ApiService); private http = inject(HttpClient);
private titleService = inject(Title); private titleService = inject(Title);
private swUpdate = inject(SwUpdate); private swUpdate = inject(SwUpdate);
private appRef = inject(ApplicationRef); private appRef = inject(ApplicationRef);
@@ -56,7 +55,7 @@ export class App implements OnInit {
private checkServerHealth(): void { private checkServerHealth(): void {
this.checkingServer.set(true); this.checkingServer.set(true);
this.apiService.ping() this.http.get<{ message: string }>(`${environment.apiUrl}/ping`)
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({ .subscribe({
next: () => { next: () => {
@@ -92,13 +91,5 @@ export class App implements OnInit {
console.error('Update check failed:', err); console.error('Update check failed:', err);
} }
}); });
this.swUpdate.versionUpdates
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(event => {
if (event.type === 'VERSION_READY') {
console.log('New app version ready');
}
});
} }
} }

View File

@@ -12,19 +12,19 @@
<p>We are a rapidly growing marketplace connecting sellers and buyers from different countries. Our platform creates convenient conditions for safe trading of various goods and services.</p> <p>We are a rapidly growing marketplace connecting sellers and buyers from different countries. Our platform creates convenient conditions for safe trading of various goods and services.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🎯</div> <div class="card-icon">🎯</div>
<h3>Our Mission</h3> <h3>Our Mission</h3>
<p>To create a simple and profitable ecosystem for businesses and buyers, where everyone finds the best deals.</p> <p>To create a simple and profitable ecosystem for businesses and buyers, where everyone finds the best deals.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🌍</div> <div class="card-icon">🌍</div>
<h3>Geography</h3> <h3>Geography</h3>
<p>We operate in Russia, Armenia, UAE, Turkey, China, Kazakhstan, Kyrgyzstan, and other countries.</p> <p>We operate in Russia, Armenia, UAE, Turkey, China, Kazakhstan, Kyrgyzstan, and other countries.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💼</div> <div class="card-icon">💼</div>
<h3>For Business</h3> <h3>For Business</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -35,7 +35,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛍️</div> <div class="card-icon">🛍️</div>
<h3>For Buyers</h3> <h3>For Buyers</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -46,7 +46,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔒</div> <div class="card-icon">🔒</div>
<h3>Our Values</h3> <h3>Our Values</h3>
<div class="features-list"> <div class="features-list">
@@ -86,7 +86,7 @@
<p><strong>Address:</strong> ARMENIA, KOTAYK, ABOVYAN, VERIN PTGHNI, 3rd Street, 28</p> <p><strong>Address:</strong> ARMENIA, KOTAYK, ABOVYAN, VERIN PTGHNI, 3rd Street, 28</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Contact Us</h3> <h3>Contact Us</h3>
<a href="mailto:info@lavero.store" class="contact-email">info@lavero.store</a> <a href="mailto:info@lavero.store" class="contact-email">info@lavero.store</a>

View File

@@ -12,19 +12,19 @@
<p>Մենք դինամիկ զարգացող մարկեթփլեյս ենք, որը միավորում է վաճառողներին և գնորդներին տարբեր երկրներից։ Մեր հարթակը ստեղծում է հարմար պայմաններ տարբեր ապրանքների և ծառայությունների անվտանգ առևտրի համար։</p> <p>Մենք դինամիկ զարգացող մարկեթփլեյս ենք, որը միավորում է վաճառողներին և գնորդներին տարբեր երկրներից։ Մեր հարթակը ստեղծում է հարմար պայմաններ տարբեր ապրանքների և ծառայությունների անվտանգ առևտրի համար։</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🎯</div> <div class="card-icon">🎯</div>
<h3>Մեր առաքելությունը</h3> <h3>Մեր առաքելությունը</h3>
<p>Ստեղծել պարզ և շահավետ էկոհամակարգ բիզնեսի և գնորդների համար, որտեղ բոլորը գտնեն լավագույն առաջարկները։</p> <p>Ստեղծել պարզ և շահավետ էկոհամակարգ բիզնեսի և գնորդների համար, որտեղ բոլորը գտնեն լավագույն առաջարկները։</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🌍</div> <div class="card-icon">🌍</div>
<h3>Աշխարհագրություն</h3> <h3>Աշխարհագրություն</h3>
<p>Մենք աշխատում ենք Ռուսաստանում, Հայաստանում, ԱՀԷ-ում, Թուրքիայում, Չինաստանում, Ղազախստանում, Ղրղզստանում և այլ երկրներում։</p> <p>Մենք աշխատում ենք Ռուսաստանում, Հայաստանում, ԱՀԷ-ում, Թուրքիայում, Չինաստանում, Ղազախստանում, Ղրղզստանում և այլ երկրներում։</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💼</div> <div class="card-icon">💼</div>
<h3>Բիզնեսի համար</h3> <h3>Բիզնեսի համար</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -35,7 +35,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛍️</div> <div class="card-icon">🛍️</div>
<h3>Գնորդների համար</h3> <h3>Գնորդների համար</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -46,7 +46,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔒</div> <div class="card-icon">🔒</div>
<h3>Մեր արժեքները</h3> <h3>Մեր արժեքները</h3>
<div class="features-list"> <div class="features-list">
@@ -86,7 +86,7 @@
<p><strong>Hasцe՝</strong> ՀАЙАСТАН, КОТАЙК, АБОВЯН, ВЕРИН ПТГНИ, 3-рд ПOЛOC, 28</p> <p><strong>Hasцe՝</strong> ՀАЙАСТАН, КОТАЙК, АБОВЯН, ВЕРИН ПТГНИ, 3-рд ПOЛOC, 28</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Կապվել մեզ հետ</h3> <h3>Կապվել մեզ հետ</h3>
<a href="mailto:info@lavero.store" class="contact-email">info@lavero.store</a> <a href="mailto:info@lavero.store" class="contact-email">info@lavero.store</a>

View File

@@ -12,19 +12,19 @@
<p>Мы - динамично развивающийся маркетплейс, объединяющий продавцов и покупателей из разных стран. Наша платформа создает удобные условия для безопасной торговли различными товарами и услугами.</p> <p>Мы - динамично развивающийся маркетплейс, объединяющий продавцов и покупателей из разных стран. Наша платформа создает удобные условия для безопасной торговли различными товарами и услугами.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🎯</div> <div class="card-icon">🎯</div>
<h3>Наша миссия</h3> <h3>Наша миссия</h3>
<p>Создавать простую и выгодную экосистему для бизнеса и покупателей, где каждый находит лучшие предложения.</p> <p>Создавать простую и выгодную экосистему для бизнеса и покупателей, где каждый находит лучшие предложения.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🌍</div> <div class="card-icon">🌍</div>
<h3>География</h3> <h3>География</h3>
<p>Мы работаем в России, Армении, ОАЭ, Турции, Китае, Казахстане, Кыргызстане и других странах.</p> <p>Мы работаем в России, Армении, ОАЭ, Турции, Китае, Казахстане, Кыргызстане и других странах.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💼</div> <div class="card-icon">💼</div>
<h3>Для бизнеса</h3> <h3>Для бизнеса</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -35,7 +35,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛍️</div> <div class="card-icon">🛍️</div>
<h3>Для покупателей</h3> <h3>Для покупателей</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -46,7 +46,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔒</div> <div class="card-icon">🔒</div>
<h3>Наши ценности</h3> <h3>Наши ценности</h3>
<div class="features-list"> <div class="features-list">
@@ -86,7 +86,7 @@
<p><strong>Адрес:</strong> АРМЕНИЯ, КОТАЙК, АБОВЯН, ВЕРИН ПТГНИ, ул. 3-я, 28</p> <p><strong>Адрес:</strong> АРМЕНИЯ, КОТАЙК, АБОВЯН, ВЕРИН ПТГНИ, ул. 3-я, 28</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Связаться с нами</h3> <h3>Связаться с нами</h3>
<a href="mailto:info@lavero.store" class="contact-email">info@lavero.store</a> <a href="mailto:info@lavero.store" class="contact-email">info@lavero.store</a>

View File

@@ -6,33 +6,33 @@
</div> </div>
<div class="lavero-cards"> <div class="lavero-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🏢</div> <div class="card-icon">🏢</div>
<h3>Organization</h3> <h3>Organization</h3>
<p class="org-name">LLC «ELECTROMOTORS»</p> <p class="org-name">LLC «ELECTROMOTORS»</p>
<p><strong>TIN:</strong> 03590442</p> <p><strong>TIN:</strong> 03590442</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Phone</h3> <h3>Phone</h3>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p> <p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">✉️</div> <div class="card-icon">✉️</div>
<h3>Email</h3> <h3>Email</h3>
<p><a href="mailto:info@lavero.store">info&#64;lavero.store</a></p> <p><a href="mailto:info@lavero.store">info&#64;lavero.store</a></p>
<p class="note">Response within 24 hours</p> <p class="note">Response within 24 hours</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📍</div> <div class="card-icon">📍</div>
<h3>Address</h3> <h3>Address</h3>
<p>Armenia, 0501, Aragatsotn region, Talin, 12 Gaya St.</p> <p>Armenia, 0501, Aragatsotn region, Talin, 12 Gaya St.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h3>Working Hours</h3> <h3>Working Hours</h3>
<p><strong>Support:</strong> 9:00 - 21:00</p> <p><strong>Support:</strong> 9:00 - 21:00</p>

View File

@@ -6,33 +6,33 @@
</div> </div>
<div class="lavero-cards"> <div class="lavero-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🏢</div> <div class="card-icon">🏢</div>
<h3>Կազմակերպություն</h3> <h3>Կազմակերպություն</h3>
<p class="org-name">ՍՊԸ «ԷԼԵԿՏՌՈՄՈՏՈՌՍ»</p> <p class="org-name">ՍՊԸ «ԷԼԵԿՏՌՈՄՈՏՈՌՍ»</p>
<p><strong>ՀՎՀՀ՝</strong> 03590442</p> <p><strong>ՀՎՀՀ՝</strong> 03590442</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Հեռախոս</h3> <h3>Հեռախոս</h3>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p> <p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">✉️</div> <div class="card-icon">✉️</div>
<h3>Էլ. փոստ</h3> <h3>Էլ. փոստ</h3>
<p><a href="mailto:info@lavero.store">info&#64;lavero.store</a></p> <p><a href="mailto:info@lavero.store">info&#64;lavero.store</a></p>
<p class="note">Պատասխանը 24 ժամվա ընթացքում</p> <p class="note">Պատասխանը 24 ժամվա ընթացքում</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📍</div> <div class="card-icon">📍</div>
<h3>Հասցե</h3> <h3>Հասցե</h3>
<p>Հայաստան, 0501, Արագածոտնի մարզ, ք. Տալին, Գայայի փող. 12</p> <p>Հայաստան, 0501, Արագածոտնի մարզ, ք. Տալին, Գայայի փող. 12</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h3>Աշխատանքային ժամեր</h3> <h3>Աշխատանքային ժամեր</h3>
<p><strong>Աջակցություն՝</strong> 9:00 - 21:00</p> <p><strong>Աջակցություն՝</strong> 9:00 - 21:00</p>

View File

@@ -6,33 +6,33 @@
</div> </div>
<div class="lavero-cards"> <div class="lavero-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🏢</div> <div class="card-icon">🏢</div>
<h3>Организация</h3> <h3>Организация</h3>
<p class="org-name">ООО «ЛАВЕРО»</p> <p class="org-name">ООО «ЛАВЕРО»</p>
<p><strong>ИНН:</strong> 03590442</p> <p><strong>ИНН:</strong> 03590442</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Телефон</h3> <h3>Телефон</h3>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p> <p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">✉️</div> <div class="card-icon">✉️</div>
<h3>Email</h3> <h3>Email</h3>
<p><a href="mailto:info@lavero.store">info&#64;lavero.store</a></p> <p><a href="mailto:info@lavero.store">info&#64;lavero.store</a></p>
<p class="note">Ответ в течение 24 часов</p> <p class="note">Ответ в течение 24 часов</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📍</div> <div class="card-icon">📍</div>
<h3>Адрес</h3> <h3>Адрес</h3>
<p>АРМЕНИЯ, КОТАЙК, АБОВЯН, ВЕРИН ПТГНИ, ул. 3-я, 28</p> <p>АРМЕНИЯ, КОТАЙК, АБОВЯН, ВЕРИН ПТГНИ, ул. 3-я, 28</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h3>Часы работы</h3> <h3>Часы работы</h3>
<p><strong>Поддержка:</strong> 9:00 - 21:00</p> <p><strong>Поддержка:</strong> 9:00 - 21:00</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="lavero-cards"> <div class="lavero-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📧</div> <div class="card-icon">📧</div>
<h3>Digital Products</h3> <h3>Digital Products</h3>
<div class="features-list"> <div class="features-list">
@@ -18,7 +18,7 @@
<p class="note important" style="margin-top: 12px;">⚠️ The platform is not responsible for digital products. The seller is responsible for quality and functionality.</p> <p class="note important" style="margin-top: 12px;">⚠️ The platform is not responsible for digital products. The seller is responsible for quality and functionality.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📦</div> <div class="card-icon">📦</div>
<h3>Physical Products</h3> <h3>Physical Products</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -32,7 +32,7 @@
<p class="note important" style="margin-top: 12px;">⚠️ The platform is not responsible for the actions of shipping companies. Delivery is handled by СДЭК, Почта России, Boxberry, DPD, and other carriers.</p> <p class="note important" style="margin-top: 12px;">⚠️ The platform is not responsible for the actions of shipping companies. Delivery is handled by СДЭК, Почта России, Boxberry, DPD, and other carriers.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💰</div> <div class="card-icon">💰</div>
<h3>Delivery Cost</h3> <h3>Delivery Cost</h3>
<div class="delivery-cost"> <div class="delivery-cost">
@@ -48,7 +48,7 @@
<p class="note">Exact cost is calculated at checkout</p> <p class="note">Exact cost is calculated at checkout</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔍</div> <div class="card-icon">🔍</div>
<h3>Tracking</h3> <h3>Tracking</h3>
<p>After shipping, you will receive a tracking number by email. Track your package on the delivery service website or in your account.</p> <p>After shipping, you will receive a tracking number by email. Track your package on the delivery service website or in your account.</p>
@@ -66,7 +66,7 @@
<p class="note important">If there are issues - file a report with the courier</p> <p class="note important">If there are issues - file a report with the courier</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Questions about delivery?</h3> <h3>Questions about delivery?</h3>
<p>Contact the seller or us:</p> <p>Contact the seller or us:</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="lavero-cards"> <div class="lavero-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📧</div> <div class="card-icon">📧</div>
<h3>Թվային ապրանքներ</h3> <h3>Թվային ապրանքներ</h3>
<div class="features-list"> <div class="features-list">
@@ -18,7 +18,7 @@
<p class="note important" style="margin-top: 12px;">⚠️ Հարթակը պատասխանատվություն չի կրում թվային ապրանքների համար։ Որակի և գործունակության համար պատասխանատու է վաճառողը։</p> <p class="note important" style="margin-top: 12px;">⚠️ Հարթակը պատասխանատվություն չի կրում թվային ապրանքների համար։ Որակի և գործունակության համար պատասխանատու է վաճառողը։</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📦</div> <div class="card-icon">📦</div>
<h3>Ֆիզիկական ապրանքներ</h3> <h3>Ֆիզիկական ապրանքներ</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -32,7 +32,7 @@
<p class="note important" style="margin-top: 12px;">⚠️ Հարթակը պատասխանատվություն չի կրում տրանսպորտային ընկերությունների գործողությունների համար։ Առաքման համար պատասխանատու են СДЭК, Почта России, Boxberry, DPD և այլ փոխադրողներ։</p> <p class="note important" style="margin-top: 12px;">⚠️ Հարթակը պատասխանատվություն չի կրում տրանսպորտային ընկերությունների գործողությունների համար։ Առաքման համար պատասխանատու են СДЭК, Почта России, Boxberry, DPD և այլ փոխադրողներ։</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💰</div> <div class="card-icon">💰</div>
<h3>Առաքման արժեքը</h3> <h3>Առաքման արժեքը</h3>
<div class="delivery-cost"> <div class="delivery-cost">
@@ -48,7 +48,7 @@
<p class="note">Ճիշտ արժեքը հաշվարկվում է ձևակերպման ժամանակ</p> <p class="note">Ճիշտ արժեքը հաշվարկվում է ձևակերպման ժամանակ</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔍</div> <div class="card-icon">🔍</div>
<h3>Հետագծում</h3> <h3>Հետագծում</h3>
<p>Ուղարկմանից հետո դուք կստանաք թրեք-համար email-ով։ Հետևեք ծանրութը առաքման ծառայության կայքում կամ անձնական էջում։</p> <p>Ուղարկմանից հետո դուք կստանաք թրեք-համար email-ով։ Հետևեք ծանրութը առաքման ծառայության կայքում կամ անձնական էջում։</p>
@@ -66,7 +66,7 @@
<p class="note important">Եթե խնդիրներ կան - կազմեք ակտ սուրհանդեսի հետ</p> <p class="note important">Եթե խնդիրներ կան - կազմեք ակտ սուրհանդեսի հետ</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Առաքման հարցեր՞</h3> <h3>Առաքման հարցեր՞</h3>
<p>Կապվեք վաճառողի կամ մեզ հետ՝</p> <p>Կապվեք վաճառողի կամ մեզ հետ՝</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="lavero-cards"> <div class="lavero-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📧</div> <div class="card-icon">📧</div>
<h3>Цифровые товары</h3> <h3>Цифровые товары</h3>
<div class="features-list"> <div class="features-list">
@@ -18,7 +18,7 @@
<p class="note important" style="margin-top: 12px;">⚠️ Платформа не несет ответственности за цифровые товары. За качество и работоспособность отвечает продавец.</p> <p class="note important" style="margin-top: 12px;">⚠️ Платформа не несет ответственности за цифровые товары. За качество и работоспособность отвечает продавец.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📦</div> <div class="card-icon">📦</div>
<h3>Физические товары</h3> <h3>Физические товары</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -32,7 +32,7 @@
<p class="note important" style="margin-top: 12px;">⚠️ Платформа не несет ответственности за действия транспортных компаний. За доставку отвечают СДЭК, Почта России, Boxberry, DPD и другие перевозчики.</p> <p class="note important" style="margin-top: 12px;">⚠️ Платформа не несет ответственности за действия транспортных компаний. За доставку отвечают СДЭК, Почта России, Boxberry, DPD и другие перевозчики.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💰</div> <div class="card-icon">💰</div>
<h3>Стоимость доставки</h3> <h3>Стоимость доставки</h3>
<div class="delivery-cost"> <div class="delivery-cost">
@@ -48,7 +48,7 @@
<p class="note">Точная стоимость рассчитывается при оформлении</p> <p class="note">Точная стоимость рассчитывается при оформлении</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔍</div> <div class="card-icon">🔍</div>
<h3>Отслеживание</h3> <h3>Отслеживание</h3>
<p>После отправки вы получите трек-номер на email. Отслеживайте посылку на сайте службы доставки или в личном кабинете.</p> <p>После отправки вы получите трек-номер на email. Отслеживайте посылку на сайте службы доставки или в личном кабинете.</p>
@@ -66,7 +66,7 @@
<p class="note important">Если есть проблемы - составьте акт с курьером</p> <p class="note important">Если есть проблемы - составьте акт с курьером</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Вопросы по доставке?</h3> <h3>Вопросы по доставке?</h3>
<p>Свяжитесь с продавцом или нами:</p> <p>Свяжитесь с продавцом или нами:</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="lavero-cards"> <div class="lavero-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛍️</div> <div class="card-icon">🛍️</div>
<h3>How to place an order?</h3> <h3>How to place an order?</h3>
<div class="process-steps-compact"> <div class="process-steps-compact">
@@ -17,7 +17,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💳</div> <div class="card-icon">💳</div>
<h3>Payment Methods</h3> <h3>Payment Methods</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -29,7 +29,7 @@
<p class="note">*Depends on the seller</p> <p class="note">*Depends on the seller</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🚚</div> <div class="card-icon">🚚</div>
<h3>Delivery</h3> <h3>Delivery</h3>
<div class="delivery-info"> <div class="delivery-info">
@@ -44,13 +44,13 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">↩️</div> <div class="card-icon">↩️</div>
<h3>Product Returns</h3> <h3>Product Returns</h3>
<p>You can return a product in good condition within 7 days, provided it has not been used and the packaging is intact.</p> <p>You can return a product in good condition within 7 days, provided it has not been used and the packaging is intact.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔒</div> <div class="card-icon">🔒</div>
<h3>Security</h3> <h3>Security</h3>
<div class="features-list"> <div class="features-list">
@@ -60,7 +60,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">⏱️</div> <div class="card-icon">⏱️</div>
<h3>Order Processing</h3> <h3>Order Processing</h3>
<p>Your order is processed immediately after payment. The seller ships the product within 1-3 business days.</p> <p>Your order is processed immediately after payment. The seller ships the product within 1-3 business days.</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="lavero-cards"> <div class="lavero-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛍️</div> <div class="card-icon">🛍️</div>
<h3>Ինչպես կատարել պատվեր՞</h3> <h3>Ինչպես կատարել պատվեր՞</h3>
<div class="process-steps-compact"> <div class="process-steps-compact">
@@ -17,7 +17,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💳</div> <div class="card-icon">💳</div>
<h3>Վճարման եղանակներ</h3> <h3>Վճարման եղանակներ</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -29,7 +29,7 @@
<p class="note">*Կախված է վաճառողից</p> <p class="note">*Կախված է վաճառողից</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🚚</div> <div class="card-icon">🚚</div>
<h3>Առաքում</h3> <h3>Առաքում</h3>
<div class="delivery-info"> <div class="delivery-info">
@@ -44,13 +44,13 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">↩️</div> <div class="card-icon">↩️</div>
<h3>Ապրանքի վերադարձ</h3> <h3>Ապրանքի վերադարձ</h3>
<p>Կարելի է վերադարձել որակյալ ապրանքը 7 օրվա ընթացքում, եթե այն չի օգտագործվել և փաթեթավորումը պահպանված է։</p> <p>Կարելի է վերադարձել որակյալ ապրանքը 7 օրվա ընթացքում, եթե այն չի օգտագործվել և փաթեթավորումը պահպանված է։</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔒</div> <div class="card-icon">🔒</div>
<h3>Անվտանգություն</h3> <h3>Անվտանգություն</h3>
<div class="features-list"> <div class="features-list">
@@ -60,7 +60,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">⏱️</div> <div class="card-icon">⏱️</div>
<h3>Պատվերի մշակում</h3> <h3>Պատվերի մշակում</h3>
<p>Պատվերը մշակվում է վճարումից անմիջապես հետո։ Վաճառողը ապրանքը ուղարկում է 1-3 աշխատանքային օրվա ընթացքում։</p> <p>Պատվերը մշակվում է վճարումից անմիջապես հետո։ Վաճառողը ապրանքը ուղարկում է 1-3 աշխատանքային օրվա ընթացքում։</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="lavero-cards"> <div class="lavero-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛍️</div> <div class="card-icon">🛍️</div>
<h3>Как сделать заказ?</h3> <h3>Как сделать заказ?</h3>
<div class="process-steps-compact"> <div class="process-steps-compact">
@@ -17,7 +17,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💳</div> <div class="card-icon">💳</div>
<h3>Способы оплаты</h3> <h3>Способы оплаты</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -29,7 +29,7 @@
<p class="note">*Зависит от продавца</p> <p class="note">*Зависит от продавца</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🚚</div> <div class="card-icon">🚚</div>
<h3>Доставка</h3> <h3>Доставка</h3>
<div class="delivery-info"> <div class="delivery-info">
@@ -44,13 +44,13 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">↩️</div> <div class="card-icon">↩️</div>
<h3>Возврат товара</h3> <h3>Возврат товара</h3>
<p>Можно вернуть качественный товар в течение 7 дней, если он не использовался и сохранена упаковка.</p> <p>Можно вернуть качественный товар в течение 7 дней, если он не использовался и сохранена упаковка.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔒</div> <div class="card-icon">🔒</div>
<h3>Безопасность</h3> <h3>Безопасность</h3>
<div class="features-list"> <div class="features-list">
@@ -60,7 +60,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">⏱️</div> <div class="card-icon">⏱️</div>
<h3>Обработка заказа</h3> <h3>Обработка заказа</h3>
<p>Заказ обрабатывается сразу после оплаты. Продавец отправляет товар в течение 1-3 рабочих дней.</p> <p>Заказ обрабатывается сразу после оплаты. Продавец отправляет товар в течение 1-3 рабочих дней.</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="lavero-cards"> <div class="lavero-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🏷️</div> <div class="card-icon">🏷️</div>
<h3>Warranty Periods</h3> <h3>Warranty Periods</h3>
<div class="warranty-periods"> <div class="warranty-periods">
@@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h3>Warranty Conditions</h3> <h3>Warranty Conditions</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -37,7 +37,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛠️</div> <div class="card-icon">🛠️</div>
<h3>Your Rights for Defects</h3> <h3>Your Rights for Defects</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -48,7 +48,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">⏱️</div> <div class="card-icon">⏱️</div>
<h3>Repair Timeframe</h3> <h3>Repair Timeframe</h3>
<p>Maximum 45 days by law. If the deadline is violated, you can request a replacement or a refund.</p> <p>Maximum 45 days by law. If the deadline is violated, you can request a replacement or a refund.</p>
@@ -67,7 +67,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📝</div> <div class="card-icon">📝</div>
<h3>How to File a Claim</h3> <h3>How to File a Claim</h3>
<div class="process-steps-compact"> <div class="process-steps-compact">
@@ -79,7 +79,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Need Help?</h3> <h3>Need Help?</h3>
<p>In case of disputes:</p> <p>In case of disputes:</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="lavero-cards"> <div class="lavero-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🏷️</div> <div class="card-icon">🏷️</div>
<h3>Երաշխիքի ժամկետներ</h3> <h3>Երաշխիքի ժամկետներ</h3>
<div class="warranty-periods"> <div class="warranty-periods">
@@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h3>Երաշխիքի պայմաններ</h3> <h3>Երաշխիքի պայմաններ</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -37,7 +37,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛠️</div> <div class="card-icon">🛠️</div>
<h3>Ձեր իրավունքները թերության դեպքում</h3> <h3>Ձեր իրավունքները թերության դեպքում</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -48,7 +48,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">⏱️</div> <div class="card-icon">⏱️</div>
<h3>Նորոգման ժամկետ</h3> <h3>Նորոգման ժամկետ</h3>
<p>Օրենքով առավելագույնը 45 օր։ Եթե ժամկետը խախտվի ՝ կարող եք պահանջել փոխարինում կամ գումարի վերադարձ։</p> <p>Օրենքով առավելագույնը 45 օր։ Եթե ժամկետը խախտվի ՝ կարող եք պահանջել փոխարինում կամ գումարի վերադարձ։</p>
@@ -67,7 +67,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📝</div> <div class="card-icon">📝</div>
<h3>Ինչպես դիմել հայտ</h3> <h3>Ինչպես դիմել հայտ</h3>
<div class="process-steps-compact"> <div class="process-steps-compact">
@@ -79,7 +79,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Օգնությու՞ն պետք է՞</h3> <h3>Օգնությու՞ն պետք է՞</h3>
<p>Վեճերի դեպքում՝</p> <p>Վեճերի դեպքում՝</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="lavero-cards"> <div class="lavero-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🏷️</div> <div class="card-icon">🏷️</div>
<h3>Сроки гарантии</h3> <h3>Сроки гарантии</h3>
<div class="warranty-periods"> <div class="warranty-periods">
@@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h3>Условия гарантии</h3> <h3>Условия гарантии</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -37,7 +37,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛠️</div> <div class="card-icon">🛠️</div>
<h3>Ваши права при браке</h3> <h3>Ваши права при браке</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -48,7 +48,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">⏱️</div> <div class="card-icon">⏱️</div>
<h3>Срок ремонта</h3> <h3>Срок ремонта</h3>
<p>Максимум 45 дней по закону. Если срок нарушен - можно требовать замену или возврат денег.</p> <p>Максимум 45 дней по закону. Если срок нарушен - можно требовать замену или возврат денег.</p>
@@ -67,7 +67,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📝</div> <div class="card-icon">📝</div>
<h3>Как подать заявку</h3> <h3>Как подать заявку</h3>
<div class="process-steps-compact"> <div class="process-steps-compact">
@@ -79,7 +79,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Нужна помощь?</h3> <h3>Нужна помощь?</h3>
<p>При возникновении споров:</p> <p>При возникновении споров:</p>

View File

@@ -53,7 +53,7 @@
<p>2.3. All payments are processed through certified payment systems in compliance with PCI DSS security standards.</p> <p>2.3. All payments are processed through certified payment systems in compliance with PCI DSS security standards.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">⚙️</div> <div class="card-icon">⚙️</div>
<h2>3. Payment Process</h2> <h2>3. Payment Process</h2>
<p>3.1. The order payment procedure includes the following steps:</p> <p>3.1. The order payment procedure includes the following steps:</p>
@@ -69,7 +69,7 @@
<p>3.3. The Buyer's payment obligation is considered fulfilled from the moment the funds are received in the payment system account.</p> <p>3.3. The Buyer's payment obligation is considered fulfilled from the moment the funds are received in the payment system account.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🛡️</div> <div class="card-icon">🛡️</div>
<h2>4. Payment Security</h2> <h2>4. Payment Security</h2>
<p>4.1. All payments are processed through a secure HTTPS connection using TLS 1.2 protocol and above.</p> <p>4.1. All payments are processed through a secure HTTPS connection using TLS 1.2 protocol and above.</p>
@@ -84,7 +84,7 @@
<p>4.4. In case of suspicious activity, the payment system has the right to request additional identity verification of the Buyer.</p> <p>4.4. In case of suspicious activity, the payment system has the right to request additional identity verification of the Buyer.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>5. Payment Confirmation</h2> <h2>5. Payment Confirmation</h2>
<p>5.1. After successful payment, the Buyer receives a confirmation to the email address provided during checkout.</p> <p>5.1. After successful payment, the Buyer receives a confirmation to the email address provided during checkout.</p>
@@ -124,7 +124,7 @@
<p>6.4. The Marketplace does not charge a fee for processing refunds. Payment system and bank fees may apply in accordance with their tariffs.</p> <p>6.4. The Marketplace does not charge a fee for processing refunds. Payment system and bank fees may apply in accordance with their tariffs.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>7. Failed Payments</h2> <h2>7. Failed Payments</h2>
<p>7.1. A payment may be declined for the following reasons:</p> <p>7.1. A payment may be declined for the following reasons:</p>

View File

@@ -53,7 +53,7 @@
<p>2.3. Բոլոր վճարումները մշակվում են սերտիֆիկացված վճարման համակարգերի միջոցով՝ PCI DSS անվտանգության ստանդարտներին համապատասխան։</p> <p>2.3. Բոլոր վճարումները մշակվում են սերտիֆիկացված վճարման համակարգերի միջոցով՝ PCI DSS անվտանգության ստանդարտներին համապատասխան։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">⚙️</div> <div class="card-icon">⚙️</div>
<h2>3. Վճարման գործընթացը</h2> <h2>3. Վճարման գործընթացը</h2>
<p>3.1. Պատվերի վճարման գործընթացը ներառում է հետևյալ քայլերը՝</p> <p>3.1. Պատվերի վճարման գործընթացը ներառում է հետևյալ քայլերը՝</p>
@@ -69,7 +69,7 @@
<p>3.3. Գնորդի վճարման պարտավորությունը համարվում է կատարված վճարման համակարգի հաշվին դրամական միջոցների մուտքագրման պահից։</p> <p>3.3. Գնորդի վճարման պարտավորությունը համարվում է կատարված վճարման համակարգի հաշվին դրամական միջոցների մուտքագրման պահից։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🛡️</div> <div class="card-icon">🛡️</div>
<h2>4. Վճարումների անվտանգություն</h2> <h2>4. Վճարումների անվտանգություն</h2>
<p>4.1. Բոլոր վճարումները մշակվում են պաշտպանված HTTPS կապակցով՝ TLS 1.2 և ավելի բարձր արթանագրի օգտագործմամբ։</p> <p>4.1. Բոլոր վճարումները մշակվում են պաշտպանված HTTPS կապակցով՝ TLS 1.2 և ավելի բարձր արթանագրի օգտագործմամբ։</p>
@@ -84,7 +84,7 @@
<p>4.4. Կասկածելի գործունեության դեպքում վճարման համակարգը իրավունք ունի պահանջել Գնորդի ինքնության լրացուցիչ ստուգում։</p> <p>4.4. Կասկածելի գործունեության դեպքում վճարման համակարգը իրավունք ունի պահանջել Գնորդի ինքնության լրացուցիչ ստուգում։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>5. Վճարման հաստատում</h2> <h2>5. Վճարման հաստատում</h2>
<p>5.1. Հաջող վճարմանից հետո Գնորդը ստանում է հաստատում պատվերի ձևակերպման թելադրված էլ. փոստի հասցեին։</p> <p>5.1. Հաջող վճարմանից հետո Գնորդը ստանում է հաստատում պատվերի ձևակերպման թելադրված էլ. փոստի հասցեին։</p>
@@ -124,7 +124,7 @@
<p>6.4. Միջոցների վերադարձի մշակման համար Մարկեթպլեյսը միջնորդավճար չի գանձում։ Վճարման համակարգերի և բանկերի միջնորդավճարները կարող են կիրառվել դրանց սակագներին համապատասխան։</p> <p>6.4. Միջոցների վերադարձի մշակման համար Մարկեթպլեյսը միջնորդավճար չի գանձում։ Վճարման համակարգերի և բանկերի միջնորդավճարները կարող են կիրառվել դրանց սակագներին համապատասխան։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>7. Անհաջող վճարումներ</h2> <h2>7. Անհաջող վճարումներ</h2>
<p>7.1. Վճարումը կարող է մերժվել հետևյալ պատճառներով՝</p> <p>7.1. Վճարումը կարող է մերժվել հետևյալ պատճառներով՝</p>

View File

@@ -53,7 +53,7 @@
<p>2.3. Все платежи обрабатываются через сертифицированные платежные системы с соблюдением стандартов безопасности PCI DSS.</p> <p>2.3. Все платежи обрабатываются через сертифицированные платежные системы с соблюдением стандартов безопасности PCI DSS.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">⚙️</div> <div class="card-icon">⚙️</div>
<h2>3. Процесс оплаты</h2> <h2>3. Процесс оплаты</h2>
<p>3.1. Процедура оплаты заказа включает следующие этапы:</p> <p>3.1. Процедура оплаты заказа включает следующие этапы:</p>
@@ -69,7 +69,7 @@
<p>3.3. Обязательство Покупателя по оплате считается исполненным с момента поступления денежных средств на счет платежной системы.</p> <p>3.3. Обязательство Покупателя по оплате считается исполненным с момента поступления денежных средств на счет платежной системы.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🛡️</div> <div class="card-icon">🛡️</div>
<h2>4. Безопасность платежей</h2> <h2>4. Безопасность платежей</h2>
<p>4.1. Все платежи обрабатываются через защищенное HTTPS-соединение с использованием протокола TLS 1.2 и выше.</p> <p>4.1. Все платежи обрабатываются через защищенное HTTPS-соединение с использованием протокола TLS 1.2 и выше.</p>
@@ -84,7 +84,7 @@
<p>4.4. В случае подозрительной активности платежная система имеет право запросить дополнительную верификацию личности Покупателя.</p> <p>4.4. В случае подозрительной активности платежная система имеет право запросить дополнительную верификацию личности Покупателя.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>5. Подтверждение оплаты</h2> <h2>5. Подтверждение оплаты</h2>
<p>5.1. После успешной оплаты Покупатель получает подтверждение на указанный при оформлении заказа адрес электронной почты.</p> <p>5.1. После успешной оплаты Покупатель получает подтверждение на указанный при оформлении заказа адрес электронной почты.</p>
@@ -124,7 +124,7 @@
<p>6.4. За обработку возврата средств Маркетплейс комиссию не взимает. Комиссии платежных систем и банков могут применяться в соответствии с их тарифами.</p> <p>6.4. За обработку возврата средств Маркетплейс комиссию не взимает. Комиссии платежных систем и банков могут применяться в соответствии с их тарифами.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>7. Неуспешные платежи</h2> <h2>7. Неуспешные платежи</h2>
<p>7.1. Платеж может быть отклонен по следующим причинам:</p> <p>7.1. Платеж может быть отклонен по следующим причинам:</p>

View File

@@ -104,7 +104,7 @@
<p>3.1.5. An agreement between the Operator and a third party, where 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 based on the concluded agreement.</p> <p>3.1.5. An agreement between the Operator and a third party, where 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 based on the concluded agreement.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">👥</div> <div class="card-icon">👥</div>
<h2>4. CATEGORIES OF PERSONAL DATA SUBJECTS</h2> <h2>4. CATEGORIES OF PERSONAL DATA SUBJECTS</h2>
@@ -122,7 +122,7 @@
</ul> </ul>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📝</div> <div class="card-icon">📝</div>
<h2>5. CATEGORIES OF PROCESSED PERSONAL DATA</h2> <h2>5. CATEGORIES OF PROCESSED PERSONAL DATA</h2>
@@ -172,7 +172,7 @@
<p>Include conditions for processing, storage, cross-border data transfer, work with publicly available sources, special and biometric data, as well as the procedure for obtaining clarifications in accordance with the legislation of the Russian Federation.</p> <p>Include conditions for processing, storage, cross-border data transfer, work with publicly available sources, special and biometric data, as well as the procedure for obtaining clarifications in accordance with the legislation of the Russian Federation.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>7. RIGHTS OF THE PERSONAL DATA SUBJECT</h2> <h2>7. RIGHTS OF THE PERSONAL DATA SUBJECT</h2>
@@ -194,7 +194,7 @@
<p>The Subject also has the right to protect their rights, recover damages, and receive compensation for moral harm.</p> <p>The Subject also has the right to protect their rights, recover damages, and receive compensation for moral harm.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📋</div> <div class="card-icon">📋</div>
<h2>8. OPERATOR'S OBLIGATIONS</h2> <h2>8. OPERATOR'S OBLIGATIONS</h2>
@@ -205,7 +205,7 @@
<p>8.3. The Operator bears other obligations established by FZ-152.</p> <p>8.3. The Operator bears other obligations established by FZ-152.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🛡️</div> <div class="card-icon">🛡️</div>
<h2>9. SECURITY ASSURANCE</h2> <h2>9. SECURITY ASSURANCE</h2>
@@ -222,14 +222,14 @@
</ul> </ul>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">⚠️</div> <div class="card-icon">⚠️</div>
<h2>10. LIABILITY</h2> <h2>10. LIABILITY</h2>
<p>10.1. Persons guilty of violating the norms of processing and protection of Personal Data bear liability in accordance with the legislation of the Russian Federation.</p> <p>10.1. Persons guilty of violating the norms of processing and protection of Personal Data bear liability in accordance with the legislation of the Russian Federation.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🎯</div> <div class="card-icon">🎯</div>
<h2>11. PURPOSES OF PROCESSING</h2> <h2>11. PURPOSES OF PROCESSING</h2>
@@ -248,7 +248,7 @@
</ul> </ul>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🍪</div> <div class="card-icon">🍪</div>
<h2>12. AUTOMATICALLY COLLECTED INFORMATION</h2> <h2>12. AUTOMATICALLY COLLECTED INFORMATION</h2>

View File

@@ -104,7 +104,7 @@
<p>3.1.5. Договор между оператором и третьим лицом, где последнее поручает Оператору обработку персональных данных Субъекта Персональных данных или передает на основании заключенного договора персональные данные Субъекта Персональных данных.</p> <p>3.1.5. Договор между оператором и третьим лицом, где последнее поручает Оператору обработку персональных данных Субъекта Персональных данных или передает на основании заключенного договора персональные данные Субъекта Персональных данных.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">👥</div> <div class="card-icon">👥</div>
<h2>4. КАТЕГОРИИ СУБЪЕКТОВ ПЕРСОНАЛЬНЫХ ДАННЫХ</h2> <h2>4. КАТЕГОРИИ СУБЪЕКТОВ ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
@@ -122,7 +122,7 @@
</ul> </ul>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📝</div> <div class="card-icon">📝</div>
<h2>5. КАТЕГОРИИ ОБРАБАТЫВАЕМЫХ ПЕРСОНАЛЬНЫХ ДАННЫХ</h2> <h2>5. КАТЕГОРИИ ОБРАБАТЫВАЕМЫХ ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
@@ -172,7 +172,7 @@
<p>Включают условия обработки, хранения, трансграничной передачи данных, работы с общедоступными источниками, специальными и биометрическими данными, а также порядок получения разъяснений согласно законодательству РФ.</p> <p>Включают условия обработки, хранения, трансграничной передачи данных, работы с общедоступными источниками, специальными и биометрическими данными, а также порядок получения разъяснений согласно законодательству РФ.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>7. ПРАВА СУБЪЕКТА ПЕРСОНАЛЬНЫХ ДАННЫХ</h2> <h2>7. ПРАВА СУБЪЕКТА ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
@@ -194,7 +194,7 @@
<p>Субъект также имеет право на защиту своих прав, возмещение убытков и компенсацию морального вреда.</p> <p>Субъект также имеет право на защиту своих прав, возмещение убытков и компенсацию морального вреда.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📋</div> <div class="card-icon">📋</div>
<h2>8. ОБЯЗАННОСТИ ОПЕРАТОРА</h2> <h2>8. ОБЯЗАННОСТИ ОПЕРАТОРА</h2>
@@ -205,7 +205,7 @@
<p>8.3. Оператор несет иные обязанности, установленные ФЗ-152.</p> <p>8.3. Оператор несет иные обязанности, установленные ФЗ-152.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🛡️</div> <div class="card-icon">🛡️</div>
<h2>9. ОБЕСПЕЧЕНИЕ БЕЗОПАСНОСТИ</h2> <h2>9. ОБЕСПЕЧЕНИЕ БЕЗОПАСНОСТИ</h2>
@@ -222,14 +222,14 @@
</ul> </ul>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">⚠️</div> <div class="card-icon">⚠️</div>
<h2>10. ОТВЕТСТВЕННОСТЬ</h2> <h2>10. ОТВЕТСТВЕННОСТЬ</h2>
<p>10.1. Лица, виновные в нарушении норм обработки и защиты Персональных данных, несут ответственность согласно законодательству РФ.</p> <p>10.1. Лица, виновные в нарушении норм обработки и защиты Персональных данных, несут ответственность согласно законодательству РФ.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🎯</div> <div class="card-icon">🎯</div>
<h2>11. ЦЕЛИ ОБРАБОТКИ</h2> <h2>11. ЦЕЛИ ОБРАБОТКИ</h2>
@@ -248,7 +248,7 @@
</ul> </ul>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🍪</div> <div class="card-icon">🍪</div>
<h2>12. АВТОМАТИЧЕСКИ СОБИРАЕМАЯ ИНФОРМАЦИЯ</h2> <h2>12. АВТОМАТИЧЕСКИ СОБИРАЕМАЯ ИНФОРМАЦИЯ</h2>

View File

@@ -36,12 +36,16 @@
<p>1.9. Promotional campaigns may have special rules.</p> <p>1.9. Promotional campaigns may have special rules.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🎯</div> <div class="card-icon">🎯</div>
<h2>2. Subject of the Agreement</h2> <h2>2. Subject of the Agreement</h2>
<p>2.1. The purpose is to provide users with the ability to purchase goods and services presented on the resource.</p> <p>2.1. The purpose is to provide users with the ability to purchase goods and services presented on the resource.</p>
<p>2.2. The agreement regulates the use of the website and provided features.</p> <p>2.2. The agreement regulates the use of the website and provided features.</p>
<p>2.3. It applies to all types of goods, services, and products on the website.</p> <p>2.3. It applies to all types of goods, services, and products on the website.</p>
<p>2.4. The Marketplace is an agent acting under agency agreements with Sellers (Third Parties). The Marketplace does not acquire ownership rights to the Goods/Services sold through the Platform and only provides the technical ability to conclude a transaction between the Seller and the Buyer.</p>
<p>2.5. The Seller is an independent business entity and bears full responsibility for the quality, safety, conformity of the Goods/Services to the stated characteristics, and fulfillment of obligations to the Buyer (warranties, returns, claims).</p>
<p>2.6. The Marketplace is not a party to the sale and purchase agreement between the Seller and the Buyer. All claims regarding the Goods/Services are submitted by the Buyer directly to the Seller.</p>
<p>2.7. The Marketplace's agency remuneration is withheld from the amount paid by the Buyer and does not increase the price for the Buyer beyond the price set by the Seller.</p>
</section> </section>
<section class="info-card wide"> <section class="info-card wide">
@@ -57,7 +61,7 @@
<p><strong>3.8. Applicable laws:</strong> Governed by Federal Law "On Consumer Rights Protection" No. 2300-1 and the Civil Code of the Russian Federation.</p> <p><strong>3.8. Applicable laws:</strong> Governed by Federal Law "On Consumer Rights Protection" No. 2300-1 and the Civil Code of the Russian Federation.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📱</div> <div class="card-icon">📱</div>
<h2>4. Registration and Personal Account</h2> <h2>4. Registration and Personal Account</h2>
<p><strong>4.1. Procedure:</strong> Registration is not required for ordering but provides access to the personal account.</p> <p><strong>4.1. Procedure:</strong> Registration is not required for ordering but provides access to the personal account.</p>
@@ -114,7 +118,7 @@
<p><strong>Rights:</strong> Opt out of advertising messages through the website or by writing to <a href="mailto:info@lavero.store">info@lavero.store</a>.</p> <p><strong>Rights:</strong> Opt out of advertising messages through the website or by writing to <a href="mailto:info@lavero.store">info@lavero.store</a>.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">©</div> <div class="card-icon">©</div>
<h2>7. Exclusive Rights to Content</h2> <h2>7. Exclusive Rights to Content</h2>
<p><strong>7.1. Intellectual property:</strong> All content (design, texts, graphics, video, software, databases) is subject to the copyright of the owner and rights holders.</p> <p><strong>7.1. Intellectual property:</strong> All content (design, texts, graphics, video, software, databases) is subject to the copyright of the owner and rights holders.</p>
@@ -122,7 +126,7 @@
<p><strong>7.3. Personal use:</strong> For personal non-commercial use while preserving copyright notices.</p> <p><strong>7.3. Personal use:</strong> For personal non-commercial use while preserving copyright notices.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🔗</div> <div class="card-icon">🔗</div>
<h2>8. Third-Party Websites and Content</h2> <h2>8. Third-Party Websites and Content</h2>
<p><strong>8.1. External links:</strong> The website may contain links to third-party resources. The website owner is not responsible for their content.</p> <p><strong>8.1. External links:</strong> The website may contain links to third-party resources. The website owner is not responsible for their content.</p>
@@ -218,14 +222,14 @@
<p><strong>13.7. Return method:</strong> The return method is specified in the application.</p> <p><strong>13.7. Return method:</strong> The return method is specified in the application.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>14. Term of the Agreement</h2> <h2>14. Term of the Agreement</h2>
<p><strong>14.1. Commencement and termination:</strong> The agreement is active from the moment of acceptance by the user until revocation of acceptance.</p> <p><strong>14.1. Commencement and termination:</strong> The agreement is active from the moment of acceptance by the user until revocation of acceptance.</p>
<p><strong>14.2. Right of revocation:</strong> The owner may revoke the offer pursuant to Art. 436 of the Civil Code of the Russian Federation.</p> <p><strong>14.2. Right of revocation:</strong> The owner may revoke the offer pursuant to Art. 436 of the Civil Code of the Russian Federation.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>15. Dispute Resolution Procedure</h2> <h2>15. Dispute Resolution Procedure</h2>
<p><strong>15.1. Voluntary settlement:</strong> Mandatory pre-trial dispute resolution procedure.</p> <p><strong>15.1. Voluntary settlement:</strong> Mandatory pre-trial dispute resolution procedure.</p>
@@ -247,12 +251,12 @@
<p><strong>16.3. Legislation:</strong> Issues are resolved under the legislation of Armenia.</p> <p><strong>16.3. Legislation:</strong> Issues are resolved under the legislation of Armenia.</p>
<p><strong>16.4. The term "Legislation":</strong> Refers to the laws of Armenia.</p> <p><strong>16.4. The term "Legislation":</strong> Refers to the laws of Armenia.</p>
<p><strong>16.5. Free services:</strong> Do not imply the application of consumer protection regulations.</p> <p><strong>16.5. Free services:</strong> Do not imply the application of consumer protection regulations.</p>
<p><strong>16.6. Absence of relationships:</strong> Do not establish agency, partnership, or employment relationships.</p> <p><strong>16.6. Absence of relationships:</strong> Except for the agency model under clause 2.4, the terms do not establish partnership, joint venture, or employment relationships.</p>
<p><strong>16.7. Invalidity of clauses:</strong> Does not affect the legal force of the remaining provisions.</p> <p><strong>16.7. Invalidity of clauses:</strong> Does not affect the legal force of the remaining provisions.</p>
<p><strong>16.8. Response to violations:</strong> Non-intervention does not prevent subsequent protective measures.</p> <p><strong>16.8. Response to violations:</strong> Non-intervention does not prevent subsequent protective measures.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h2>Contact Us</h2> <h2>Contact Us</h2>
<p>Questions about the agreement:</p> <p>Questions about the agreement:</p>

View File

@@ -36,12 +36,16 @@
<p>1.9. Գովազդային ակցիաները կարող են ունենալ հատուկ կանոններ։</p> <p>1.9. Գովազդային ակցիաները կարող են ունենալ հատուկ կանոններ։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🎯</div> <div class="card-icon">🎯</div>
<h2>2. Համաձայնագրի առարկան</h2> <h2>2. Համաձայնագրի առարկան</h2>
<p>2.1. Նպատակն է օգտագործողներին հնարավորություն տալ ռեսուրսում ներկայացված ապրանքներ և ծառայություններ ձեռք բերելու։</p> <p>2.1. Նպատակն է օգտագործողներին հնարավորություն տալ ռեսուրսում ներկայացված ապրանքներ և ծառայություններ ձեռք բերելու։</p>
<p>2.2. Համաձայնագիրը կարգավորում է կայքի օգտագործման և տրամադրվող գործառույթների կարգը։</p> <p>2.2. Համաձայնագիրը կարգավորում է կայքի օգտագործման և տրամադրվող գործառույթների կարգը։</p>
<p>2.3. Գործում է կայքում ներկայացված ապրանքների՝ ծառայությունների և պրոդուկտների բոլոր տեսակների նկատմամբ։</p> <p>2.3. Գործում է կայքում ներկայացված ապրանքների՝ ծառայությունների և պրոդուկտների բոլոր տեսակների նկատմամբ։</p>
<p>2.4. Մարկետփլեյսը հանդիսանում է գործակալ, որը գործում է Վաճառողների (Երրորդ անձանց) հետ կնքված գործակալության պայմանագրի հիման վրա։ Մարկետփլեյսը չի ձեռք բերում Հարթակի միջոցով իրացվող Ապրանքների/Ծառայությունների նկատմամբ սեփականության իրավունք, այլ միայն ապահովում է Վաճառողի և Գնորդի միջև գործարք կնքելու տեխնիկական հնարավորությունը։</p>
<p>2.5. Վաճառողը հանդիսանում է ինքնուրույն տնտեսվարող սուբյեկտ և կրում է ամբողջական պատասխանատվություն Ապրանքի/Ծառայության որակի, անվտանգության, հայտարարված բնութագրերին համապատասխանության, ինչպես նաև Գնորդի նկատմամբ պարտավորությունների կատարման համար (երաշխիքներ, վերադարձ, պահանջներ)։</p>
<p>2.6. Մարկետփլեյսը չի հանդիսանում Վաճառողի և Գնորդի միջև առուվաճառքի պայմանագրի կողմ։ Ապրանքի/Ծառայության վերաբերյալ բոլոր պահանջները Գնորդը ներկայացնում է անմիջապես Վաճառողին։</p>
<p>2.7. Մարկետփլեյսի գործակալական վարձատրությունը պահվում է Գնորդի վճարած գումարից և չի ավելացնում Գնորդի համար գինը Վաճառողի սահմանած գնից ավել։</p>
</section> </section>
<section class="info-card wide"> <section class="info-card wide">
@@ -57,7 +61,7 @@
<p><strong>3.8. Կիրառվող օրենքներ՝</strong> Կարգավորվում են ՌԴ «Սպառողների իրավունքների պաշտպանության մասին» թիվ 2300-1 և ՌԴ քաղաքացիական օրենսգիրքով։</p> <p><strong>3.8. Կիրառվող օրենքներ՝</strong> Կարգավորվում են ՌԴ «Սպառողների իրավունքների պաշտպանության մասին» թիվ 2300-1 և ՌԴ քաղաքացիական օրենսգիրքով։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📱</div> <div class="card-icon">📱</div>
<h2>4. Գրանցում և անձնական հաշիվ</h2> <h2>4. Գրանցում և անձնական հաշիվ</h2>
<p><strong>4.1. Ըթացակարգ՝</strong> Գրանցումը պարտադիր չէ պատվերի համար՝ սակայն հնարավորություն է տալիս անձնական հաշիվ մուտք գործելու։</p> <p><strong>4.1. Ըթացակարգ՝</strong> Գրանցումը պարտադիր չէ պատվերի համար՝ սակայն հնարավորություն է տալիս անձնական հաշիվ մուտք գործելու։</p>
@@ -114,7 +118,7 @@
<p><strong>Իրավունքներ՝</strong> Հրաժարվել գովազդային հաղորդագրություններից կայքի միճոցով կամ նամակով <a href="mailto:info@lavero.store">info@lavero.store</a>։</p> <p><strong>Իրավունքներ՝</strong> Հրաժարվել գովազդային հաղորդագրություններից կայքի միճոցով կամ նամակով <a href="mailto:info@lavero.store">info@lavero.store</a>։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">©</div> <div class="card-icon">©</div>
<h2>7. Բովանդակության բացառիկ իրավունքներ</h2> <h2>7. Բովանդակության բացառիկ իրավունքներ</h2>
<p><strong>7.1. Մտավոր սեփականություն՝</strong> Բոլոր բովանդակությունը (դիզայն՝ տեքստեր՝ գրաֆիկա՝ տեսանյութ՝ ծրագրեր՝ տվյալների բազաներ) սեփականատիրոջ և իրավատերերի հեղինակային իրավունքների առարկան է։</p> <p><strong>7.1. Մտավոր սեփականություն՝</strong> Բոլոր բովանդակությունը (դիզայն՝ տեքստեր՝ գրաֆիկա՝ տեսանյութ՝ ծրագրեր՝ տվյալների բազաներ) սեփականատիրոջ և իրավատերերի հեղինակային իրավունքների առարկան է։</p>
@@ -122,7 +126,7 @@
<p><strong>7.3. Անձնական օգտագործում՝</strong> Անձնական ոչ առևտրային օգտագործման համար՝ հեղինակային իրավունքների նիշերի պահպանմամբ։</p> <p><strong>7.3. Անձնական օգտագործում՝</strong> Անձնական ոչ առևտրային օգտագործման համար՝ հեղինակային իրավունքների նիշերի պահպանմամբ։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🔗</div> <div class="card-icon">🔗</div>
<h2>8. Երրորդ կողմերի կայքեր և բովանդակություն</h2> <h2>8. Երրորդ կողմերի կայքեր և բովանդակություն</h2>
<p><strong>8.1. Արտաքին հղումներ՝</strong> Կայքը կարող է պարունակել երրորդ կողմի ռեսուրսների հղումներ։ Կայքի սեփականատերը պատասխանատու չէ դրանց բովանդակության համար։</p> <p><strong>8.1. Արտաքին հղումներ՝</strong> Կայքը կարող է պարունակել երրորդ կողմի ռեսուրսների հղումներ։ Կայքի սեփականատերը պատասխանատու չէ դրանց բովանդակության համար։</p>
@@ -218,14 +222,14 @@
<p><strong>13.7. Վերադարձի մեխանիզմ՝</strong> Վերադարձի եղանակը նշվում է դիմումում։</p> <p><strong>13.7. Վերադարձի մեխանիզմ՝</strong> Վերադարձի եղանակը նշվում է դիմումում։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>14. Համաձայնագրի գործողության ժամկետը</h2> <h2>14. Համաձայնագրի գործողության ժամկետը</h2>
<p><strong>14.1. Սկիզբ և դադարեցում՝</strong> Համաձայնագիրը գործում է օգտագործողի ընդունման պահից մինչև ընդունման հետկանչում։</p> <p><strong>14.1. Սկիզբ և դադարեցում՝</strong> Համաձայնագիրը գործում է օգտագործողի ընդունման պահից մինչև ընդունման հետկանչում։</p>
<p><strong>14.2. Հետկանչման իրավունք՝</strong> Սեփականատերը կարող է օֆերտան հետ կանչել ՌԴ 436 հոդվածի համաձայն։</p> <p><strong>14.2. Հետկանչման իրավունք՝</strong> Սեփականատերը կարող է օֆերտան հետ կանչել ՌԴ 436 հոդվածի համաձայն։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>15. Վեճերի լուծման կարգը</h2> <h2>15. Վեճերի լուծման կարգը</h2>
<p><strong>15.1. Կամավոր կարգավորում՝</strong> Պարտադիր նախադատական վեճերի լուծման ընթացակարգ։</p> <p><strong>15.1. Կամավոր կարգավորում՝</strong> Պարտադիր նախադատական վեճերի լուծման ընթացակարգ։</p>
@@ -247,12 +251,12 @@
<p><strong>16.3. Օրենսդրություն՝</strong> Հարցերը լուծվում են Հայաստանի օրենսդրությամբ։</p> <p><strong>16.3. Օրենսդրություն՝</strong> Հարցերը լուծվում են Հայաստանի օրենսդրությամբ։</p>
<p><strong>16.4. «Օրենսդրություն» տերմին՝</strong> Նշանակում է Հայաստանի օրենքները։</p> <p><strong>16.4. «Օրենսդրություն» տերմին՝</strong> Նշանակում է Հայաստանի օրենքները։</p>
<p><strong>16.5. Անվճար ծառայություններ՝</strong> Չեն ենթադրում սպառողների իրավունքների պաշտպանության նորմերի կիրառում։</p> <p><strong>16.5. Անվճար ծառայություններ՝</strong> Չեն ենթադրում սպառողների իրավունքների պաշտպանության նորմերի կիրառում։</p>
<p><strong>16.6. Հարաբերությունների բացակայություն՝</strong> Չեն հաստատում գործակալային՝ գործընկերային կամ աշխատանքային հարաբերություններ։</p> <p><strong>16.6. Հարաբերությունների բացակայություն՝</strong> Բացի 2.4 կետով նախատեսված գործակալական մոդելից՝ պայմանները չեն հաստատում գործընկերային, համատեղ ձեռնարկության կամ աշխատանքային հարաբերություններ։</p>
<p><strong>16.7. Կետերի անվավերություն՝</strong> Չի ազդում մնացած դրույթների իրավական ուժի վրա։</p> <p><strong>16.7. Կետերի անվավերություն՝</strong> Չի ազդում մնացած դրույթների իրավական ուժի վրա։</p>
<p><strong>16.8. Խախտումների նկատմամբ արձագանք՝</strong> Անգործունեությունը չի խանգարում հետագա պաշտպանության միջոցներին։</p> <p><strong>16.8. Խախտումների նկատմամբ արձագանք՝</strong> Անգործունեությունը չի խանգարում հետագա պաշտպանության միջոցներին։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h2>Կապ</h2> <h2>Կապ</h2>
<p>Համաձայնագրի վերաբերյալ հարցեր՝</p> <p>Համաձայնագրի վերաբերյալ հարցեր՝</p>

View File

@@ -36,12 +36,16 @@
<p>1.9. Промо-кампании могут иметь специальные правила.</p> <p>1.9. Промо-кампании могут иметь специальные правила.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🎯</div> <div class="card-icon">🎯</div>
<h2>2. Предмет соглашения</h2> <h2>2. Предмет соглашения</h2>
<p>2.1. Целью является предоставление пользователям возможности покупать товары и услуги, представленные на ресурсе.</p> <p>2.1. Целью является предоставление пользователям возможности покупать товары и услуги, представленные на ресурсе.</p>
<p>2.2. Соглашение регулирует порядок использования сайта и предоставляемых функций.</p> <p>2.2. Соглашение регулирует порядок использования сайта и предоставляемых функций.</p>
<p>2.3. Действие распространяется на все типы товаров, услуг и продуктов на сайте.</p> <p>2.3. Действие распространяется на все типы товаров, услуг и продуктов на сайте.</p>
<p>2.4. Маркетплейс является агентом, действующим на основании договора агентирования с Продавцами (Третьими лицами). Маркетплейс не приобретает право собственности на Товары/Услуги, реализуемые через Платформу, а лишь обеспечивает техническую возможность заключения сделки между Продавцом и Покупателем.</p>
<p>2.5. Продавец является самостоятельным хозяйствующим субъектом и несёт полную ответственность за качество, безопасность, соответствие Товара/Услуги заявленным характеристикам, а также за исполнение обязательств перед Покупателем (гарантии, возврат, претензии).</p>
<p>2.6. Маркетплейс не выступает стороной договора купли-продажи между Продавцом и Покупателем. Все претензии по Товару/Услуге Покупатель предъявляет непосредственно Продавцу.</p>
<p>2.7. Агентское вознаграждение Маркетплейса удерживается из суммы, уплаченной Покупателем, и не увеличивает цену для Покупателя сверх установленной Продавцом.</p>
</section> </section>
<section class="info-card wide"> <section class="info-card wide">
@@ -57,7 +61,7 @@
<p><strong>3.8. Применение законов:</strong> Регулируются ФЗ "О защите прав потребителей" №2300-1 и ГК РФ.</p> <p><strong>3.8. Применение законов:</strong> Регулируются ФЗ "О защите прав потребителей" №2300-1 и ГК РФ.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📱</div> <div class="card-icon">📱</div>
<h2>4. Регистрация и личный кабинет</h2> <h2>4. Регистрация и личный кабинет</h2>
<p><strong>4.1. Процедура:</strong> Регистрация необязательна для заказа, но открывает доступ к личному кабинету.</p> <p><strong>4.1. Процедура:</strong> Регистрация необязательна для заказа, но открывает доступ к личному кабинету.</p>
@@ -114,7 +118,7 @@
<p><strong>Права:</strong> Отказаться от рекламных сообщений через сайт или письмом на <a href="mailto:info@lavero.store">info@lavero.store</a>.</p> <p><strong>Права:</strong> Отказаться от рекламных сообщений через сайт или письмом на <a href="mailto:info@lavero.store">info@lavero.store</a>.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">©</div> <div class="card-icon">©</div>
<h2>7. Исключительные права на контент</h2> <h2>7. Исключительные права на контент</h2>
<p><strong>7.1. Интеллектуальная собственность:</strong> Весь контент (дизайн, тексты, графика, видео, программы, базы данных) является объектом авторских прав владельца и правообладателей.</p> <p><strong>7.1. Интеллектуальная собственность:</strong> Весь контент (дизайн, тексты, графика, видео, программы, базы данных) является объектом авторских прав владельца и правообладателей.</p>
@@ -122,7 +126,7 @@
<p><strong>7.3. Личное использование:</strong> Для личного некоммерческого использования при сохранении обозначений авторских прав.</p> <p><strong>7.3. Личное использование:</strong> Для личного некоммерческого использования при сохранении обозначений авторских прав.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🔗</div> <div class="card-icon">🔗</div>
<h2>8. Сайты и контент третьих лиц</h2> <h2>8. Сайты и контент третьих лиц</h2>
<p><strong>8.1. Внешние ссылки:</strong> Сайт может содержать ссылки на сторонние ресурсы. Ответственность за их содержание не относится к владельцу сайта.</p> <p><strong>8.1. Внешние ссылки:</strong> Сайт может содержать ссылки на сторонние ресурсы. Ответственность за их содержание не относится к владельцу сайта.</p>
@@ -218,14 +222,14 @@
<p><strong>13.7. Механизм возврата:</strong> Способ возврата указывается в заявлении.</p> <p><strong>13.7. Механизм возврата:</strong> Способ возврата указывается в заявлении.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>14. Срок действия соглашения</h2> <h2>14. Срок действия соглашения</h2>
<p><strong>14.1. Начало и прекращение:</strong> Соглашение активно с момента принятия пользователем до отзыва акцепта.</p> <p><strong>14.1. Начало и прекращение:</strong> Соглашение активно с момента принятия пользователем до отзыва акцепта.</p>
<p><strong>14.2. Право отзыва:</strong> Владелец может отозвать оферту согласно ст. 436 ГК РФ.</p> <p><strong>14.2. Право отзыва:</strong> Владелец может отозвать оферту согласно ст. 436 ГК РФ.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>15. Порядок разрешения споров</h2> <h2>15. Порядок разрешения споров</h2>
<p><strong>15.1. Добровольное урегулирование:</strong> Обязательный досудебный порядок рассмотрения споров.</p> <p><strong>15.1. Добровольное урегулирование:</strong> Обязательный досудебный порядок рассмотрения споров.</p>
@@ -247,12 +251,12 @@
<p><strong>16.3. Законодательство:</strong> Вопросы разрешаются по законодательству Армении.</p> <p><strong>16.3. Законодательство:</strong> Вопросы разрешаются по законодательству Армении.</p>
<p><strong>16.4. Термин "Законодательство":</strong> Подразумевает законы Армении.</p> <p><strong>16.4. Термин "Законодательство":</strong> Подразумевает законы Армении.</p>
<p><strong>16.5. Бесплатные услуги:</strong> Не предполагают применение норм о защите прав потребителей.</p> <p><strong>16.5. Бесплатные услуги:</strong> Не предполагают применение норм о защите прав потребителей.</p>
<p><strong>16.6. Отсутствие отношений:</strong> Не устанавливают агентских связей, партнёрства или трудовых отношений.</p> <p><strong>16.6. Отсутствие отношений:</strong> Кроме агентской модели по п. 2.4, условия не устанавливают партнёрства, совместного предприятия или трудовых отношений.</p>
<p><strong>16.7. Недействительность пунктов:</strong> Не влияет на законную силу остальных положений.</p> <p><strong>16.7. Недействительность пунктов:</strong> Не влияет на законную силу остальных положений.</p>
<p><strong>16.8. Реакция на нарушения:</strong> Невмешательство не препятствует последующим мерам защиты.</p> <p><strong>16.8. Реакция на нарушения:</strong> Невмешательство не препятствует последующим мерам защиты.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h2>Контакты</h2> <h2>Контакты</h2>
<p>Вопросы по соглашению:</p> <p>Вопросы по соглашению:</p>

View File

@@ -16,7 +16,7 @@
<p>1.5. Digital products are returned under special rules prescribed by legislation.</p> <p>1.5. Digital products are returned under special rules prescribed by legislation.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📅</div> <div class="card-icon">📅</div>
<h2>2. Time Frames</h2> <h2>2. Time Frames</h2>
<p><strong>2.1. Quality products:</strong></p> <p><strong>2.1. Quality products:</strong></p>
@@ -112,7 +112,7 @@
<p>Our platform is always ready to help resolve disputes.</p> <p>Our platform is always ready to help resolve disputes.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">💰</div> <div class="card-icon">💰</div>
<h2>6. Payment Refund</h2> <h2>6. Payment Refund</h2>
<p>Funds are refunded to the same payment method:</p> <p>Funds are refunded to the same payment method:</p>
@@ -134,14 +134,14 @@
<p>The customer has the right to claim compensation for defective products and shipping costs.</p> <p>The customer has the right to claim compensation for defective products and shipping costs.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🔄</div> <div class="card-icon">🔄</div>
<h2>7. Product Exchange</h2> <h2>7. Product Exchange</h2>
<p>If a non-defective product does not match in dimensions, size, or specifications, the customer may exchange it with the supplier for a suitable option.</p> <p>If a non-defective product does not match in dimensions, size, or specifications, the customer may exchange it with the supplier for a suitable option.</p>
<p>If the desired model is unavailable, the customer has the right to cancel the order and receive a refund.</p> <p>If the desired model is unavailable, the customer has the right to cancel the order and receive a refund.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🛡️</div> <div class="card-icon">🛡️</div>
<h2>8. Warranty Service</h2> <h2>8. Warranty Service</h2>
<p>Warranty information is displayed on the product page or available from the seller.</p> <p>Warranty information is displayed on the product page or available from the seller.</p>
@@ -149,7 +149,7 @@
<p>Warranty service does not cover physical damage, improper use, or unauthorized modifications.</p> <p>Warranty service does not cover physical damage, improper use, or unauthorized modifications.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">⚖️</div> <div class="card-icon">⚖️</div>
<h2>9. Responsibilities of Parties</h2> <h2>9. Responsibilities of Parties</h2>
<p>The supplier is responsible for product quality and timely refunds.</p> <p>The supplier is responsible for product quality and timely refunds.</p>

View File

@@ -16,7 +16,7 @@
<p>1.5. Թվային ապրանքները վերադարձվում են օրենսդրությամբ նախատեսված հատուկ կանոններով։</p> <p>1.5. Թվային ապրանքները վերադարձվում են օրենսդրությամբ նախատեսված հատուկ կանոններով։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📅</div> <div class="card-icon">📅</div>
<h2>2. Ժամանակային սահմաններ</h2> <h2>2. Ժամանակային սահմաններ</h2>
<p><strong>2.1. Որակյալ ապրանքներ՝</strong></p> <p><strong>2.1. Որակյալ ապրանքներ՝</strong></p>
@@ -112,7 +112,7 @@
<p>Մեր հարթակը միշտ պատրաստ է օգնել վիճելի իրավիճակների կարգավորման հարցում։</p> <p>Մեր հարթակը միշտ պատրաստ է օգնել վիճելի իրավիճակների կարգավորման հարցում։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">💰</div> <div class="card-icon">💰</div>
<h2>6. Վճարման փոխհատուցում</h2> <h2>6. Վճարման փոխհատուցում</h2>
<p>Գումարը վերադարձվում է նույն վճարման եղանակով՝</p> <p>Գումարը վերադարձվում է նույն վճարման եղանակով՝</p>
@@ -134,14 +134,14 @@
<p>Գնորդը իրավունք ունի պահանջել փոխհատուցում անորակ ապրանքների և առաքման ծախսերի համար։</p> <p>Գնորդը իրավունք ունի պահանջել փոխհատուցում անորակ ապրանքների և առաքման ծախսերի համար։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🔄</div> <div class="card-icon">🔄</div>
<h2>7. Ապրանքի փոխանակում</h2> <h2>7. Ապրանքի փոխանակում</h2>
<p>Եթե որակյալ ապրանքը չի համապատասխանում չափսերով, չափորոշներով կամ հատկանիշներով, գնորդը կարող է այն փոխանակել մատակարարի մոտ համապատասխան տարբերակով։</p> <p>Եթե որակյալ ապրանքը չի համապատասխանում չափսերով, չափորոշներով կամ հատկանիշներով, գնորդը կարող է այն փոխանակել մատակարարի մոտ համապատասխան տարբերակով։</p>
<p>Համապատասխան մոդելի բացակայության դեպքում գնորդը կարող է հրաժարվել պատվերից և ստանալ փոխհատուցում։</p> <p>Համապատասխան մոդելի բացակայության դեպքում գնորդը կարող է հրաժարվել պատվերից և ստանալ փոխհատուցում։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🛡️</div> <div class="card-icon">🛡️</div>
<h2>8. Երաշխիքային սպասարկում</h2> <h2>8. Երաշխիքային սպասարկում</h2>
<p>Երաշխիքի պայմանները նշված են ապրանքի էջում կամ հասանելի են վաճառողից։</p> <p>Երաշխիքի պայմանները նշված են ապրանքի էջում կամ հասանելի են վաճառողից։</p>
@@ -149,7 +149,7 @@
<p>Երաշխիքային սպասարկումը չի տարածվում ֆիզիկական վնասների, ոչ պատշաճ օգտագործման կամ ինքնուրույն միջամտության դեպքում։</p> <p>Երաշխիքային սպասարկումը չի տարածվում ֆիզիկական վնասների, ոչ պատշաճ օգտագործման կամ ինքնուրույն միջամտության դեպքում։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">⚖️</div> <div class="card-icon">⚖️</div>
<h2>9. Կողմերի պարտականություններ</h2> <h2>9. Կողմերի պարտականություններ</h2>
<p>Մատակարարը պատասխանատու է ապրանքի որակի և ժամանակային գումարի վերադարձի համար։</p> <p>Մատակարարը պատասխանատու է ապրանքի որակի և ժամանակային գումարի վերադարձի համար։</p>

View File

@@ -16,7 +16,7 @@
<p>1.5. Электронные товары возвращаются по специальным правилам, прописанным в законодательстве.</p> <p>1.5. Электронные товары возвращаются по специальным правилам, прописанным в законодательстве.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📅</div> <div class="card-icon">📅</div>
<h2>2. Временные рамки</h2> <h2>2. Временные рамки</h2>
<p><strong>2.1. Качественная продукция:</strong></p> <p><strong>2.1. Качественная продукция:</strong></p>
@@ -112,7 +112,7 @@
<p>Наша площадка всегда готова помочь в урегулировании спорных ситуаций.</p> <p>Наша площадка всегда готова помочь в урегулировании спорных ситуаций.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">💰</div> <div class="card-icon">💰</div>
<h2>6. Компенсация платежа</h2> <h2>6. Компенсация платежа</h2>
<p>Деньги возмещаются на тот же способ оплаты:</p> <p>Деньги возмещаются на тот же способ оплаты:</p>
@@ -134,14 +134,14 @@
<p>Клиент имеет право потребовать компенсацию за некачественную продукцию и расходы по пересылке.</p> <p>Клиент имеет право потребовать компенсацию за некачественную продукцию и расходы по пересылке.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🔄</div> <div class="card-icon">🔄</div>
<h2>7. Обмен продукции</h2> <h2>7. Обмен продукции</h2>
<p>При несоответствии исправного изделия по габаритам, размерам или параметрам, клиент может заменить его у поставщика на подходящий вариант.</p> <p>При несоответствии исправного изделия по габаритам, размерам или параметрам, клиент может заменить его у поставщика на подходящий вариант.</p>
<p>При отсутствии нужной модели клиент вправе отказаться от заказа и получить компенсацию.</p> <p>При отсутствии нужной модели клиент вправе отказаться от заказа и получить компенсацию.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🛡️</div> <div class="card-icon">🛡️</div>
<h2>8. Сервисное обслуживание</h2> <h2>8. Сервисное обслуживание</h2>
<p>Информация о гарантийных условиях размещена в карточке товара или доступна у продавца.</p> <p>Информация о гарантийных условиях размещена в карточке товара или доступна у продавца.</p>
@@ -149,7 +149,7 @@
<p>Гарантийное обслуживание не распространяется на физические ущербы, неправильное применение или самостоятельное вмешательство.</p> <p>Гарантийное обслуживание не распространяется на физические ущербы, неправильное применение или самостоятельное вмешательство.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">⚖️</div> <div class="card-icon">⚖️</div>
<h2>9. Обязанности участников</h2> <h2>9. Обязанности участников</h2>
<p>Поставщик несет ответственность за качество продукции и своевременное возмещение платежей.</p> <p>Поставщик несет ответственность за качество продукции и своевременное возмещение платежей.</p>

View File

@@ -12,19 +12,19 @@
<p>We are a rapidly growing marketplace connecting sellers and buyers from different countries. Our platform creates convenient conditions for safe trading of various goods and services.</p> <p>We are a rapidly growing marketplace connecting sellers and buyers from different countries. Our platform creates convenient conditions for safe trading of various goods and services.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🎯</div> <div class="card-icon">🎯</div>
<h3>Our Mission</h3> <h3>Our Mission</h3>
<p>To create a simple and profitable ecosystem for businesses and buyers, where everyone finds the best deals.</p> <p>To create a simple and profitable ecosystem for businesses and buyers, where everyone finds the best deals.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🌍</div> <div class="card-icon">🌍</div>
<h3>Geography</h3> <h3>Geography</h3>
<p>We operate in Russia, Armenia, UAE, Turkey, China, Kazakhstan, Kyrgyzstan, and other countries.</p> <p>We operate in Russia, Armenia, UAE, Turkey, China, Kazakhstan, Kyrgyzstan, and other countries.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💼</div> <div class="card-icon">💼</div>
<h3>For Business</h3> <h3>For Business</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -35,7 +35,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛍️</div> <div class="card-icon">🛍️</div>
<h3>For Buyers</h3> <h3>For Buyers</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -46,7 +46,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔒</div> <div class="card-icon">🔒</div>
<h3>Our Values</h3> <h3>Our Values</h3>
<div class="features-list"> <div class="features-list">
@@ -87,7 +87,7 @@
<p><strong>Bank:</strong> To be confirmed</p> <p><strong>Bank:</strong> To be confirmed</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Contact Us</h3> <h3>Contact Us</h3>
<a href="mailto:info@novo.market" class="contact-email">info@novo.market</a> <a href="mailto:info@novo.market" class="contact-email">info@novo.market</a>

View File

@@ -12,19 +12,19 @@
<p>Մենք դինամիկ զարգացող մարկեթփլեյս ենք, որը միավորում է վաճառողներին և գնորդներին տարբեր երկրներից։ Մեր հարթակը ստեղծում է հարմար պայմաններ տարբեր ապրանքների և ծառայությունների անվտանգ առևտրի համար։</p> <p>Մենք դինամիկ զարգացող մարկեթփլեյս ենք, որը միավորում է վաճառողներին և գնորդներին տարբեր երկրներից։ Մեր հարթակը ստեղծում է հարմար պայմաններ տարբեր ապրանքների և ծառայությունների անվտանգ առևտրի համար։</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🎯</div> <div class="card-icon">🎯</div>
<h3>Մեր առաքելությունը</h3> <h3>Մեր առաքելությունը</h3>
<p>Ստեղծել պարզ և շահավետ էկոհամակարգ բիզնեսի և գնորդների համար, որտեղ բոլորը գտնեն լավագույն առաջարկները։</p> <p>Ստեղծել պարզ և շահավետ էկոհամակարգ բիզնեսի և գնորդների համար, որտեղ բոլորը գտնեն լավագույն առաջարկները։</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🌍</div> <div class="card-icon">🌍</div>
<h3>Աշխարհագրություն</h3> <h3>Աշխարհագրություն</h3>
<p>Մենք աշխատում ենք Ռուսաստանում, Հայաստանում, ԱՀԷ-ում, Թուրքիայում, Չինաստանում, Ղազախստանում, Ղրղզստանում և այլ երկրներում։</p> <p>Մենք աշխատում ենք Ռուսաստանում, Հայաստանում, ԱՀԷ-ում, Թուրքիայում, Չինաստանում, Ղազախստանում, Ղրղզստանում և այլ երկրներում։</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💼</div> <div class="card-icon">💼</div>
<h3>Բիզնեսի համար</h3> <h3>Բիզնեսի համար</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -35,7 +35,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛍️</div> <div class="card-icon">🛍️</div>
<h3>Գնորդների համար</h3> <h3>Գնորդների համար</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -46,7 +46,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔒</div> <div class="card-icon">🔒</div>
<h3>Մեր արժեքները</h3> <h3>Մեր արժեքները</h3>
<div class="features-list"> <div class="features-list">
@@ -87,7 +87,7 @@
<p><strong>Բանկ՝</strong> Ճշտում է</p> <p><strong>Բանկ՝</strong> Ճշտում է</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Կապվել մեզ հետ</h3> <h3>Կապվել մեզ հետ</h3>
<a href="mailto:info@novo.market" class="contact-email">info@novo.market</a> <a href="mailto:info@novo.market" class="contact-email">info@novo.market</a>

View File

@@ -12,19 +12,19 @@
<p>Мы - динамично развивающийся маркетплейс, объединяющий продавцов и покупателей из разных стран. Наша платформа создает удобные условия для безопасной торговли различными товарами и услугами.</p> <p>Мы - динамично развивающийся маркетплейс, объединяющий продавцов и покупателей из разных стран. Наша платформа создает удобные условия для безопасной торговли различными товарами и услугами.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🎯</div> <div class="card-icon">🎯</div>
<h3>Наша миссия</h3> <h3>Наша миссия</h3>
<p>Создавать простую и выгодную экосистему для бизнеса и покупателей, где каждый находит лучшие предложения.</p> <p>Создавать простую и выгодную экосистему для бизнеса и покупателей, где каждый находит лучшие предложения.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🌍</div> <div class="card-icon">🌍</div>
<h3>География</h3> <h3>География</h3>
<p>Мы работаем в России, Армении, ОАЭ, Турции, Китае, Казахстане, Кыргызстане и других странах.</p> <p>Мы работаем в России, Армении, ОАЭ, Турции, Китае, Казахстане, Кыргызстане и других странах.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💼</div> <div class="card-icon">💼</div>
<h3>Для бизнеса</h3> <h3>Для бизнеса</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -35,7 +35,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛍️</div> <div class="card-icon">🛍️</div>
<h3>Для покупателей</h3> <h3>Для покупателей</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -46,7 +46,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔒</div> <div class="card-icon">🔒</div>
<h3>Наши ценности</h3> <h3>Наши ценности</h3>
<div class="features-list"> <div class="features-list">
@@ -87,7 +87,7 @@
<p><strong>Банк:</strong> Уточняется</p> <p><strong>Банк:</strong> Уточняется</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Связаться с нами</h3> <h3>Связаться с нами</h3>
<a href="mailto:info@novo.market" class="contact-email">info@novo.market</a> <a href="mailto:info@novo.market" class="contact-email">info@novo.market</a>

View File

@@ -6,33 +6,33 @@
</div> </div>
<div class="novo-cards"> <div class="novo-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🏢</div> <div class="card-icon">🏢</div>
<h3>Organization</h3> <h3>Organization</h3>
<p class="org-name">LLC «ELECTROMOTORS»</p> <p class="org-name">LLC «ELECTROMOTORS»</p>
<p><strong>TIN:</strong> 9909687443</p> <p><strong>TIN:</strong> 9909687443</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Phone</h3> <h3>Phone</h3>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p> <p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">✉️</div> <div class="card-icon">✉️</div>
<h3>Email</h3> <h3>Email</h3>
<p><a href="mailto:info@novo.market">info&#64;novo.market</a></p> <p><a href="mailto:info@novo.market">info&#64;novo.market</a></p>
<p class="note">Response within 24 hours</p> <p class="note">Response within 24 hours</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📍</div> <div class="card-icon">📍</div>
<h3>Address</h3> <h3>Address</h3>
<p>Armenia, 0501, Aragatsotn region, Talin, 12 Gaya St.</p> <p>Armenia, 0501, Aragatsotn region, Talin, 12 Gaya St.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h3>Working Hours</h3> <h3>Working Hours</h3>
<p><strong>Support:</strong> 9:00 - 21:00</p> <p><strong>Support:</strong> 9:00 - 21:00</p>

View File

@@ -6,33 +6,33 @@
</div> </div>
<div class="novo-cards"> <div class="novo-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🏢</div> <div class="card-icon">🏢</div>
<h3>Կազմակերպություն</h3> <h3>Կազմակերպություն</h3>
<p class="org-name">ՍՊԸ «ԷԼԵԿՏՌՈՄՈՏՈՌՍ»</p> <p class="org-name">ՍՊԸ «ԷԼԵԿՏՌՈՄՈՏՈՌՍ»</p>
<p><strong>ՀՎՀՀ՝</strong> 9909687443</p> <p><strong>ՀՎՀՀ՝</strong> 9909687443</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Հեռախոս</h3> <h3>Հեռախոս</h3>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p> <p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">✉️</div> <div class="card-icon">✉️</div>
<h3>Էլ. փոստ</h3> <h3>Էլ. փոստ</h3>
<p><a href="mailto:info@novo.market">info&#64;novo.market</a></p> <p><a href="mailto:info@novo.market">info&#64;novo.market</a></p>
<p class="note">Պատասխանը 24 ժամվա ընթացքում</p> <p class="note">Պատասխանը 24 ժամվա ընթացքում</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📍</div> <div class="card-icon">📍</div>
<h3>Հասցե</h3> <h3>Հասցե</h3>
<p>Հայաստան, 0501, Արագածոտնի մարզ, ք. Տալին, Գայայի փող. 12</p> <p>Հայաստան, 0501, Արագածոտնի մարզ, ք. Տալին, Գայայի փող. 12</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h3>Աշխատանքային ժամեր</h3> <h3>Աշխատանքային ժամեր</h3>
<p><strong>Աջակցություն՝</strong> 9:00 - 21:00</p> <p><strong>Աջակցություն՝</strong> 9:00 - 21:00</p>

View File

@@ -6,33 +6,33 @@
</div> </div>
<div class="novo-cards"> <div class="novo-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🏢</div> <div class="card-icon">🏢</div>
<h3>Организация</h3> <h3>Организация</h3>
<p class="org-name">ООО «ЭЛЕКТРОМОТОРС»</p> <p class="org-name">ООО «ЭЛЕКТРОМОТОРС»</p>
<p><strong>ИНН:</strong> 9909687443</p> <p><strong>ИНН:</strong> 9909687443</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Телефон</h3> <h3>Телефон</h3>
<p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p> <p><a [href]="env.phoneTel">{{ env.phones.support }}</a></p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">✉️</div> <div class="card-icon">✉️</div>
<h3>Email</h3> <h3>Email</h3>
<p><a href="mailto:info@novo.market">info&#64;novo.market</a></p> <p><a href="mailto:info@novo.market">info&#64;novo.market</a></p>
<p class="note">Ответ в течение 24 часов</p> <p class="note">Ответ в течение 24 часов</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📍</div> <div class="card-icon">📍</div>
<h3>Адрес</h3> <h3>Адрес</h3>
<p>Армения, 0501, Арагацотиская обл., г. Талин, ул. Гая, д. 12</p> <p>Армения, 0501, Арагацотиская обл., г. Талин, ул. Гая, д. 12</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h3>Часы работы</h3> <h3>Часы работы</h3>
<p><strong>Поддержка:</strong> 9:00 - 21:00</p> <p><strong>Поддержка:</strong> 9:00 - 21:00</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="novo-cards"> <div class="novo-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📧</div> <div class="card-icon">📧</div>
<h3>Digital Products</h3> <h3>Digital Products</h3>
<div class="features-list"> <div class="features-list">
@@ -18,7 +18,7 @@
<p class="note important" style="margin-top: 12px;">⚠️ The platform is not responsible for digital products. The seller is responsible for quality and functionality.</p> <p class="note important" style="margin-top: 12px;">⚠️ The platform is not responsible for digital products. The seller is responsible for quality and functionality.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📦</div> <div class="card-icon">📦</div>
<h3>Physical Products</h3> <h3>Physical Products</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -32,7 +32,7 @@
<p class="note important" style="margin-top: 12px;">⚠️ The platform is not responsible for the actions of shipping companies. Delivery is handled by СДЭК, Почта России, Boxberry, DPD, and other carriers.</p> <p class="note important" style="margin-top: 12px;">⚠️ The platform is not responsible for the actions of shipping companies. Delivery is handled by СДЭК, Почта России, Boxberry, DPD, and other carriers.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💰</div> <div class="card-icon">💰</div>
<h3>Delivery Cost</h3> <h3>Delivery Cost</h3>
<div class="delivery-cost"> <div class="delivery-cost">
@@ -48,7 +48,7 @@
<p class="note">Exact cost is calculated at checkout</p> <p class="note">Exact cost is calculated at checkout</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔍</div> <div class="card-icon">🔍</div>
<h3>Tracking</h3> <h3>Tracking</h3>
<p>After shipping, you will receive a tracking number by email. Track your package on the delivery service website or in your account.</p> <p>After shipping, you will receive a tracking number by email. Track your package on the delivery service website or in your account.</p>
@@ -66,7 +66,7 @@
<p class="note important">If there are issues - file a report with the courier</p> <p class="note important">If there are issues - file a report with the courier</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Questions about delivery?</h3> <h3>Questions about delivery?</h3>
<p>Contact the seller or us:</p> <p>Contact the seller or us:</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="novo-cards"> <div class="novo-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📧</div> <div class="card-icon">📧</div>
<h3>Թվային ապրանքներ</h3> <h3>Թվային ապրանքներ</h3>
<div class="features-list"> <div class="features-list">
@@ -18,7 +18,7 @@
<p class="note important" style="margin-top: 12px;">⚠️ Հարթակը պատասխանատվություն չի կրում թվային ապրանքների համար։ Որակի և գործունակության համար պատասխանատու է վաճառողը։</p> <p class="note important" style="margin-top: 12px;">⚠️ Հարթակը պատասխանատվություն չի կրում թվային ապրանքների համար։ Որակի և գործունակության համար պատասխանատու է վաճառողը։</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📦</div> <div class="card-icon">📦</div>
<h3>Ֆիզիկական ապրանքներ</h3> <h3>Ֆիզիկական ապրանքներ</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -32,7 +32,7 @@
<p class="note important" style="margin-top: 12px;">⚠️ Հարթակը պատասխանատվություն չի կրում տրանսպորտային ընկերությունների գործողությունների համար։ Առաքման համար պատասխանատու են СДЭК, Почта России, Boxberry, DPD և այլ փոխադրողներ։</p> <p class="note important" style="margin-top: 12px;">⚠️ Հարթակը պատասխանատվություն չի կրում տրանսպորտային ընկերությունների գործողությունների համար։ Առաքման համար պատասխանատու են СДЭК, Почта России, Boxberry, DPD և այլ փոխադրողներ։</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💰</div> <div class="card-icon">💰</div>
<h3>Առաքման արժեքը</h3> <h3>Առաքման արժեքը</h3>
<div class="delivery-cost"> <div class="delivery-cost">
@@ -48,7 +48,7 @@
<p class="note">Ճիշտ արժեքը հաշվարկվում է ձևակերպման ժամանակ</p> <p class="note">Ճիշտ արժեքը հաշվարկվում է ձևակերպման ժամանակ</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔍</div> <div class="card-icon">🔍</div>
<h3>Հետագծում</h3> <h3>Հետագծում</h3>
<p>Ուղարկմանից հետո դուք կստանաք թրեք-համար email-ով։ Հետևեք ծանրութը առաքման ծառայության կայքում կամ անձնական էջում։</p> <p>Ուղարկմանից հետո դուք կստանաք թրեք-համար email-ով։ Հետևեք ծանրութը առաքման ծառայության կայքում կամ անձնական էջում։</p>
@@ -66,7 +66,7 @@
<p class="note important">Եթե խնդիրներ կան - կազմեք ակտ սուրհանդեսի հետ</p> <p class="note important">Եթե խնդիրներ կան - կազմեք ակտ սուրհանդեսի հետ</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Առաքման հարցեր՞</h3> <h3>Առաքման հարցեր՞</h3>
<p>Կապվեք վաճառողի կամ մեզ հետ՝</p> <p>Կապվեք վաճառողի կամ մեզ հետ՝</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="novo-cards"> <div class="novo-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📧</div> <div class="card-icon">📧</div>
<h3>Цифровые товары</h3> <h3>Цифровые товары</h3>
<div class="features-list"> <div class="features-list">
@@ -18,7 +18,7 @@
<p class="note important" style="margin-top: 12px;">⚠️ Платформа не несет ответственности за цифровые товары. За качество и работоспособность отвечает продавец.</p> <p class="note important" style="margin-top: 12px;">⚠️ Платформа не несет ответственности за цифровые товары. За качество и работоспособность отвечает продавец.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📦</div> <div class="card-icon">📦</div>
<h3>Физические товары</h3> <h3>Физические товары</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -32,7 +32,7 @@
<p class="note important" style="margin-top: 12px;">⚠️ Платформа не несет ответственности за действия транспортных компаний. За доставку отвечают СДЭК, Почта России, Boxberry, DPD и другие перевозчики.</p> <p class="note important" style="margin-top: 12px;">⚠️ Платформа не несет ответственности за действия транспортных компаний. За доставку отвечают СДЭК, Почта России, Boxberry, DPD и другие перевозчики.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💰</div> <div class="card-icon">💰</div>
<h3>Стоимость доставки</h3> <h3>Стоимость доставки</h3>
<div class="delivery-cost"> <div class="delivery-cost">
@@ -48,7 +48,7 @@
<p class="note">Точная стоимость рассчитывается при оформлении</p> <p class="note">Точная стоимость рассчитывается при оформлении</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔍</div> <div class="card-icon">🔍</div>
<h3>Отслеживание</h3> <h3>Отслеживание</h3>
<p>После отправки вы получите трек-номер на email. Отслеживайте посылку на сайте службы доставки или в личном кабинете.</p> <p>После отправки вы получите трек-номер на email. Отслеживайте посылку на сайте службы доставки или в личном кабинете.</p>
@@ -66,7 +66,7 @@
<p class="note important">Если есть проблемы - составьте акт с курьером</p> <p class="note important">Если есть проблемы - составьте акт с курьером</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Вопросы по доставке?</h3> <h3>Вопросы по доставке?</h3>
<p>Свяжитесь с продавцом или нами:</p> <p>Свяжитесь с продавцом или нами:</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="novo-cards"> <div class="novo-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛍️</div> <div class="card-icon">🛍️</div>
<h3>How to place an order?</h3> <h3>How to place an order?</h3>
<div class="process-steps-compact"> <div class="process-steps-compact">
@@ -17,7 +17,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💳</div> <div class="card-icon">💳</div>
<h3>Payment Methods</h3> <h3>Payment Methods</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -29,7 +29,7 @@
<p class="note">*Depends on the seller</p> <p class="note">*Depends on the seller</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🚚</div> <div class="card-icon">🚚</div>
<h3>Delivery</h3> <h3>Delivery</h3>
<div class="delivery-info"> <div class="delivery-info">
@@ -44,13 +44,13 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">↩️</div> <div class="card-icon">↩️</div>
<h3>Product Returns</h3> <h3>Product Returns</h3>
<p>You can return a product in good condition within 7 days, provided it has not been used and the packaging is intact.</p> <p>You can return a product in good condition within 7 days, provided it has not been used and the packaging is intact.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔒</div> <div class="card-icon">🔒</div>
<h3>Security</h3> <h3>Security</h3>
<div class="features-list"> <div class="features-list">
@@ -60,7 +60,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">⏱️</div> <div class="card-icon">⏱️</div>
<h3>Order Processing</h3> <h3>Order Processing</h3>
<p>Your order is processed immediately after payment. The seller ships the product within 1-3 business days.</p> <p>Your order is processed immediately after payment. The seller ships the product within 1-3 business days.</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="novo-cards"> <div class="novo-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛍️</div> <div class="card-icon">🛍️</div>
<h3>Ինչպես կատարել պատվեր՞</h3> <h3>Ինչպես կատարել պատվեր՞</h3>
<div class="process-steps-compact"> <div class="process-steps-compact">
@@ -17,7 +17,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💳</div> <div class="card-icon">💳</div>
<h3>Վճարման եղանակներ</h3> <h3>Վճարման եղանակներ</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -29,7 +29,7 @@
<p class="note">*Կախված է վաճառողից</p> <p class="note">*Կախված է վաճառողից</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🚚</div> <div class="card-icon">🚚</div>
<h3>Առաքում</h3> <h3>Առաքում</h3>
<div class="delivery-info"> <div class="delivery-info">
@@ -44,13 +44,13 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">↩️</div> <div class="card-icon">↩️</div>
<h3>Ապրանքի վերադարձ</h3> <h3>Ապրանքի վերադարձ</h3>
<p>Կարելի է վերադարձել որակյալ ապրանքը 7 օրվա ընթացքում, եթե այն չի օգտագործվել և փաթեթավորումը պահպանված է։</p> <p>Կարելի է վերադարձել որակյալ ապրանքը 7 օրվա ընթացքում, եթե այն չի օգտագործվել և փաթեթավորումը պահպանված է։</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔒</div> <div class="card-icon">🔒</div>
<h3>Անվտանգություն</h3> <h3>Անվտանգություն</h3>
<div class="features-list"> <div class="features-list">
@@ -60,7 +60,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">⏱️</div> <div class="card-icon">⏱️</div>
<h3>Պատվերի մշակում</h3> <h3>Պատվերի մշակում</h3>
<p>Պատվերը մշակվում է վճարումից անմիջապես հետո։ Վաճառողը ապրանքը ուղարկում է 1-3 աշխատանքային օրվա ընթացքում։</p> <p>Պատվերը մշակվում է վճարումից անմիջապես հետո։ Վաճառողը ապրանքը ուղարկում է 1-3 աշխատանքային օրվա ընթացքում։</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="novo-cards"> <div class="novo-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛍️</div> <div class="card-icon">🛍️</div>
<h3>Как сделать заказ?</h3> <h3>Как сделать заказ?</h3>
<div class="process-steps-compact"> <div class="process-steps-compact">
@@ -17,7 +17,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💳</div> <div class="card-icon">💳</div>
<h3>Способы оплаты</h3> <h3>Способы оплаты</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -29,7 +29,7 @@
<p class="note">*Зависит от продавца</p> <p class="note">*Зависит от продавца</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🚚</div> <div class="card-icon">🚚</div>
<h3>Доставка</h3> <h3>Доставка</h3>
<div class="delivery-info"> <div class="delivery-info">
@@ -44,13 +44,13 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">↩️</div> <div class="card-icon">↩️</div>
<h3>Возврат товара</h3> <h3>Возврат товара</h3>
<p>Можно вернуть качественный товар в течение 7 дней, если он не использовался и сохранена упаковка.</p> <p>Можно вернуть качественный товар в течение 7 дней, если он не использовался и сохранена упаковка.</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🔒</div> <div class="card-icon">🔒</div>
<h3>Безопасность</h3> <h3>Безопасность</h3>
<div class="features-list"> <div class="features-list">
@@ -60,7 +60,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">⏱️</div> <div class="card-icon">⏱️</div>
<h3>Обработка заказа</h3> <h3>Обработка заказа</h3>
<p>Заказ обрабатывается сразу после оплаты. Продавец отправляет товар в течение 1-3 рабочих дней.</p> <p>Заказ обрабатывается сразу после оплаты. Продавец отправляет товар в течение 1-3 рабочих дней.</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="novo-cards"> <div class="novo-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🏷️</div> <div class="card-icon">🏷️</div>
<h3>Warranty Periods</h3> <h3>Warranty Periods</h3>
<div class="warranty-periods"> <div class="warranty-periods">
@@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h3>Warranty Conditions</h3> <h3>Warranty Conditions</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -37,7 +37,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛠️</div> <div class="card-icon">🛠️</div>
<h3>Your Rights for Defects</h3> <h3>Your Rights for Defects</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -48,7 +48,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">⏱️</div> <div class="card-icon">⏱️</div>
<h3>Repair Timeframe</h3> <h3>Repair Timeframe</h3>
<p>Maximum 45 days by law. If the deadline is violated, you can request a replacement or a refund.</p> <p>Maximum 45 days by law. If the deadline is violated, you can request a replacement or a refund.</p>
@@ -67,7 +67,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📝</div> <div class="card-icon">📝</div>
<h3>How to File a Claim</h3> <h3>How to File a Claim</h3>
<div class="process-steps-compact"> <div class="process-steps-compact">
@@ -79,7 +79,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Need Help?</h3> <h3>Need Help?</h3>
<p>In case of disputes:</p> <p>In case of disputes:</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="novo-cards"> <div class="novo-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🏷️</div> <div class="card-icon">🏷️</div>
<h3>Երաշխիքի ժամկետներ</h3> <h3>Երաշխիքի ժամկետներ</h3>
<div class="warranty-periods"> <div class="warranty-periods">
@@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h3>Երաշխիքի պայմաններ</h3> <h3>Երաշխիքի պայմաններ</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -37,7 +37,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛠️</div> <div class="card-icon">🛠️</div>
<h3>Ձեր իրավունքները թերության դեպքում</h3> <h3>Ձեր իրավունքները թերության դեպքում</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -48,7 +48,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">⏱️</div> <div class="card-icon">⏱️</div>
<h3>Նորոգման ժամկետ</h3> <h3>Նորոգման ժամկետ</h3>
<p>Օրենքով առավելագույնը 45 օր։ Եթե ժամկետը խախտվի ՝ կարող եք պահանջել փոխարինում կամ գումարի վերադարձ։</p> <p>Օրենքով առավելագույնը 45 օր։ Եթե ժամկետը խախտվի ՝ կարող եք պահանջել փոխարինում կամ գումարի վերադարձ։</p>
@@ -67,7 +67,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📝</div> <div class="card-icon">📝</div>
<h3>Ինչպես դիմել հայտ</h3> <h3>Ինչպես դիմել հայտ</h3>
<div class="process-steps-compact"> <div class="process-steps-compact">
@@ -79,7 +79,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Օգնությու՞ն պետք է՞</h3> <h3>Օգնությու՞ն պետք է՞</h3>
<p>Վեճերի դեպքում՝</p> <p>Վեճերի դեպքում՝</p>

View File

@@ -6,7 +6,7 @@
</div> </div>
<div class="novo-cards"> <div class="novo-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🏷️</div> <div class="card-icon">🏷️</div>
<h3>Сроки гарантии</h3> <h3>Сроки гарантии</h3>
<div class="warranty-periods"> <div class="warranty-periods">
@@ -25,7 +25,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h3>Условия гарантии</h3> <h3>Условия гарантии</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -37,7 +37,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🛠️</div> <div class="card-icon">🛠️</div>
<h3>Ваши права при браке</h3> <h3>Ваши права при браке</h3>
<ul class="compact-list"> <ul class="compact-list">
@@ -48,7 +48,7 @@
</ul> </ul>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">⏱️</div> <div class="card-icon">⏱️</div>
<h3>Срок ремонта</h3> <h3>Срок ремонта</h3>
<p>Максимум 45 дней по закону. Если срок нарушен - можно требовать замену или возврат денег.</p> <p>Максимум 45 дней по закону. Если срок нарушен - можно требовать замену или возврат денег.</p>
@@ -67,7 +67,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📝</div> <div class="card-icon">📝</div>
<h3>Как подать заявку</h3> <h3>Как подать заявку</h3>
<div class="process-steps-compact"> <div class="process-steps-compact">
@@ -79,7 +79,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h3>Нужна помощь?</h3> <h3>Нужна помощь?</h3>
<p>При возникновении споров:</p> <p>При возникновении споров:</p>

View File

@@ -6,14 +6,14 @@
</div> </div>
<div class="novo-cards"> <div class="novo-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🏢</div> <div class="card-icon">🏢</div>
<h3>Company Name</h3> <h3>Company Name</h3>
<p class="org-name">ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ЭЛЕКТРОМОТОРС»</p> <p class="org-name">ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ЭЛЕКТРОМОТОРС»</p>
<p class="org-short">Abbreviated: ООО «ЭЛЕКТРОМОТОРС»</p> <p class="org-short">Abbreviated: ООО «ЭЛЕКТРОМОТОРС»</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📍</div> <div class="card-icon">📍</div>
<h3>Registered Address</h3> <h3>Registered Address</h3>
<p>АРМЕНИЯ, 0501, АРАГАЦОТИСКАЯ ОБЛАСТЬ, ТАЛИН, ул. ГАЯ, д. 12</p> <p>АРМЕНИЯ, 0501, АРАГАЦОТИСКАЯ ОБЛАСТЬ, ТАЛИН, ул. ГАЯ, д. 12</p>
@@ -30,7 +30,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📋</div> <div class="card-icon">📋</div>
<h3>Details</h3> <h3>Details</h3>
<div class="requisites"> <div class="requisites">
@@ -45,7 +45,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💳</div> <div class="card-icon">💳</div>
<h3>Bank Details</h3> <h3>Bank Details</h3>
<div class="requisites"> <div class="requisites">
@@ -101,7 +101,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">👤</div> <div class="card-icon">👤</div>
<h3>Management</h3> <h3>Management</h3>
<p><strong>General Director</strong></p> <p><strong>General Director</strong></p>

View File

@@ -6,14 +6,14 @@
</div> </div>
<div class="novo-cards"> <div class="novo-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🏢</div> <div class="card-icon">🏢</div>
<h3>Անվանում</h3> <h3>Անվանում</h3>
<p class="org-name">ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ЭЛЕКТРОМОТОРС»</p> <p class="org-name">ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ЭЛЕКТРОМОТОРС»</p>
<p class="org-short">Կրճատ՝ ООО «ЭЛЕКТРОМОТОРС»</p> <p class="org-short">Կրճատ՝ ООО «ЭЛЕКТРОМОТОРС»</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📍</div> <div class="card-icon">📍</div>
<h3>Գրանցման հասցե</h3> <h3>Գրանցման հասցե</h3>
<p>АРМЕНИЯ, 0501, АРАГАЦОТИСКАЯ ОБЛАСТЬ, ТАЛИН, ул. ГАЯ, д. 12</p> <p>АРМЕНИЯ, 0501, АРАГАЦОТИСКАЯ ОБЛАСТЬ, ТАЛИН, ул. ГАЯ, д. 12</p>
@@ -30,7 +30,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📋</div> <div class="card-icon">📋</div>
<h3>Ռեկվիզիտներ</h3> <h3>Ռեկվիզիտներ</h3>
<div class="requisites"> <div class="requisites">
@@ -45,7 +45,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💳</div> <div class="card-icon">💳</div>
<h3>Բանկային տվյալներ</h3> <h3>Բանկային տվյալներ</h3>
<div class="requisites"> <div class="requisites">
@@ -101,7 +101,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">👤</div> <div class="card-icon">👤</div>
<h3>Ղեկավարություն</h3> <h3>Ղեկավարություն</h3>
<p><strong>Գլխավոր տնօրեն</strong></p> <p><strong>Գլխավոր տնօրեն</strong></p>

View File

@@ -6,14 +6,14 @@
</div> </div>
<div class="novo-cards"> <div class="novo-cards">
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">🏢</div> <div class="card-icon">🏢</div>
<h3>Наименование</h3> <h3>Наименование</h3>
<p class="org-name">ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ЭЛЕКТРОМОТОРС»</p> <p class="org-name">ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ЭЛЕКТРОМОТОРС»</p>
<p class="org-short">Сокращенно: ООО «ЭЛЕКТРОМОТОРС»</p> <p class="org-short">Сокращенно: ООО «ЭЛЕКТРОМОТОРС»</p>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📍</div> <div class="card-icon">📍</div>
<h3>Адрес регистрации</h3> <h3>Адрес регистрации</h3>
<p>АРМЕНИЯ, 0501, АРАГАЦОТИСКАЯ ОБЛАСТЬ, ТАЛИН, ул. ГАЯ, д. 12</p> <p>АРМЕНИЯ, 0501, АРАГАЦОТИСКАЯ ОБЛАСТЬ, ТАЛИН, ул. ГАЯ, д. 12</p>
@@ -30,7 +30,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">📋</div> <div class="card-icon">📋</div>
<h3>Реквизиты</h3> <h3>Реквизиты</h3>
<div class="requisites"> <div class="requisites">
@@ -45,7 +45,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">💳</div> <div class="card-icon">💳</div>
<h3>Банковские реквизиты</h3> <h3>Банковские реквизиты</h3>
<div class="requisites"> <div class="requisites">
@@ -101,7 +101,7 @@
</div> </div>
</div> </div>
<div class="info-card"> <div class="info-card wide">
<div class="card-icon">👤</div> <div class="card-icon">👤</div>
<h3>Руководство</h3> <h3>Руководство</h3>
<p><strong>Генеральный директор</strong></p> <p><strong>Генеральный директор</strong></p>

View File

@@ -53,7 +53,7 @@
<p>2.3. All payments are processed through certified payment systems in compliance with PCI DSS security standards.</p> <p>2.3. All payments are processed through certified payment systems in compliance with PCI DSS security standards.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">⚙️</div> <div class="card-icon">⚙️</div>
<h2>3. Payment Process</h2> <h2>3. Payment Process</h2>
<p>3.1. The order payment procedure includes the following steps:</p> <p>3.1. The order payment procedure includes the following steps:</p>
@@ -69,7 +69,7 @@
<p>3.3. The Buyer's payment obligation is considered fulfilled from the moment the funds are received in the payment system account.</p> <p>3.3. The Buyer's payment obligation is considered fulfilled from the moment the funds are received in the payment system account.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🛡️</div> <div class="card-icon">🛡️</div>
<h2>4. Payment Security</h2> <h2>4. Payment Security</h2>
<p>4.1. All payments are processed through a secure HTTPS connection using TLS 1.2 protocol and above.</p> <p>4.1. All payments are processed through a secure HTTPS connection using TLS 1.2 protocol and above.</p>
@@ -84,7 +84,7 @@
<p>4.4. In case of suspicious activity, the payment system has the right to request additional identity verification of the Buyer.</p> <p>4.4. In case of suspicious activity, the payment system has the right to request additional identity verification of the Buyer.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>5. Payment Confirmation</h2> <h2>5. Payment Confirmation</h2>
<p>5.1. After successful payment, the Buyer receives a confirmation to the email address provided during checkout.</p> <p>5.1. After successful payment, the Buyer receives a confirmation to the email address provided during checkout.</p>
@@ -124,7 +124,7 @@
<p>6.4. The Marketplace does not charge a fee for processing refunds. Payment system and bank fees may apply in accordance with their tariffs.</p> <p>6.4. The Marketplace does not charge a fee for processing refunds. Payment system and bank fees may apply in accordance with their tariffs.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>7. Failed Payments</h2> <h2>7. Failed Payments</h2>
<p>7.1. A payment may be declined for the following reasons:</p> <p>7.1. A payment may be declined for the following reasons:</p>

View File

@@ -53,7 +53,7 @@
<p>2.3. Բոլոր վճարումները մշակվում են սերտիֆիկացված վճարման համակարգերի միջոցով՝ PCI DSS անվտանգության ստանդարտներին համապատասխան։</p> <p>2.3. Բոլոր վճարումները մշակվում են սերտիֆիկացված վճարման համակարգերի միջոցով՝ PCI DSS անվտանգության ստանդարտներին համապատասխան։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">⚙️</div> <div class="card-icon">⚙️</div>
<h2>3. Վճարման գործընթացը</h2> <h2>3. Վճարման գործընթացը</h2>
<p>3.1. Պատվերի վճարման գործընթացը ներառում է հետևյալ քայլերը՝</p> <p>3.1. Պատվերի վճարման գործընթացը ներառում է հետևյալ քայլերը՝</p>
@@ -69,7 +69,7 @@
<p>3.3. Գնորդի վճարման պարտավորությունը համարվում է կատարված վճարման համակարգի հաշվին դրամական միջոցների մուտքագրման պահից։</p> <p>3.3. Գնորդի վճարման պարտավորությունը համարվում է կատարված վճարման համակարգի հաշվին դրամական միջոցների մուտքագրման պահից։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🛡️</div> <div class="card-icon">🛡️</div>
<h2>4. Վճարումների անվտանգություն</h2> <h2>4. Վճարումների անվտանգություն</h2>
<p>4.1. Բոլոր վճարումները մշակվում են պաշտպանված HTTPS կապակցով՝ TLS 1.2 և ավելի բարձր արթանագրի օգտագործմամբ։</p> <p>4.1. Բոլոր վճարումները մշակվում են պաշտպանված HTTPS կապակցով՝ TLS 1.2 և ավելի բարձր արթանագրի օգտագործմամբ։</p>
@@ -84,7 +84,7 @@
<p>4.4. Կասկածելի գործունեության դեպքում վճարման համակարգը իրավունք ունի պահանջել Գնորդի ինքնության լրացուցիչ ստուգում։</p> <p>4.4. Կասկածելի գործունեության դեպքում վճարման համակարգը իրավունք ունի պահանջել Գնորդի ինքնության լրացուցիչ ստուգում։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>5. Վճարման հաստատում</h2> <h2>5. Վճարման հաստատում</h2>
<p>5.1. Հաջող վճարմանից հետո Գնորդը ստանում է հաստատում պատվերի ձևակերպման թելադրված էլ. փոստի հասցեին։</p> <p>5.1. Հաջող վճարմանից հետո Գնորդը ստանում է հաստատում պատվերի ձևակերպման թելադրված էլ. փոստի հասցեին։</p>
@@ -124,7 +124,7 @@
<p>6.4. Միջոցների վերադարձի մշակման համար Մարկեթպլեյսը միջնորդավճար չի գանձում։ Վճարման համակարգերի և բանկերի միջնորդավճարները կարող են կիրառվել դրանց սակագներին համապատասխան։</p> <p>6.4. Միջոցների վերադարձի մշակման համար Մարկեթպլեյսը միջնորդավճար չի գանձում։ Վճարման համակարգերի և բանկերի միջնորդավճարները կարող են կիրառվել դրանց սակագներին համապատասխան։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>7. Անհաջող վճարումներ</h2> <h2>7. Անհաջող վճարումներ</h2>
<p>7.1. Վճարումը կարող է մերժվել հետևյալ պատճառներով՝</p> <p>7.1. Վճարումը կարող է մերժվել հետևյալ պատճառներով՝</p>

View File

@@ -53,7 +53,7 @@
<p>2.3. Все платежи обрабатываются через сертифицированные платежные системы с соблюдением стандартов безопасности PCI DSS.</p> <p>2.3. Все платежи обрабатываются через сертифицированные платежные системы с соблюдением стандартов безопасности PCI DSS.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">⚙️</div> <div class="card-icon">⚙️</div>
<h2>3. Процесс оплаты</h2> <h2>3. Процесс оплаты</h2>
<p>3.1. Процедура оплаты заказа включает следующие этапы:</p> <p>3.1. Процедура оплаты заказа включает следующие этапы:</p>
@@ -69,7 +69,7 @@
<p>3.3. Обязательство Покупателя по оплате считается исполненным с момента поступления денежных средств на счет платежной системы.</p> <p>3.3. Обязательство Покупателя по оплате считается исполненным с момента поступления денежных средств на счет платежной системы.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🛡️</div> <div class="card-icon">🛡️</div>
<h2>4. Безопасность платежей</h2> <h2>4. Безопасность платежей</h2>
<p>4.1. Все платежи обрабатываются через защищенное HTTPS-соединение с использованием протокола TLS 1.2 и выше.</p> <p>4.1. Все платежи обрабатываются через защищенное HTTPS-соединение с использованием протокола TLS 1.2 и выше.</p>
@@ -84,7 +84,7 @@
<p>4.4. В случае подозрительной активности платежная система имеет право запросить дополнительную верификацию личности Покупателя.</p> <p>4.4. В случае подозрительной активности платежная система имеет право запросить дополнительную верификацию личности Покупателя.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>5. Подтверждение оплаты</h2> <h2>5. Подтверждение оплаты</h2>
<p>5.1. После успешной оплаты Покупатель получает подтверждение на указанный при оформлении заказа адрес электронной почты.</p> <p>5.1. После успешной оплаты Покупатель получает подтверждение на указанный при оформлении заказа адрес электронной почты.</p>
@@ -124,7 +124,7 @@
<p>6.4. За обработку возврата средств Маркетплейс комиссию не взимает. Комиссии платежных систем и банков могут применяться в соответствии с их тарифами.</p> <p>6.4. За обработку возврата средств Маркетплейс комиссию не взимает. Комиссии платежных систем и банков могут применяться в соответствии с их тарифами.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>7. Неуспешные платежи</h2> <h2>7. Неуспешные платежи</h2>
<p>7.1. Платеж может быть отклонен по следующим причинам:</p> <p>7.1. Платеж может быть отклонен по следующим причинам:</p>

View File

@@ -104,7 +104,7 @@
<p>3.1.5. An agreement between the Operator and a third party, where 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 based on the concluded agreement.</p> <p>3.1.5. An agreement between the Operator and a third party, where 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 based on the concluded agreement.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">👥</div> <div class="card-icon">👥</div>
<h2>4. CATEGORIES OF PERSONAL DATA SUBJECTS</h2> <h2>4. CATEGORIES OF PERSONAL DATA SUBJECTS</h2>
@@ -122,7 +122,7 @@
</ul> </ul>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📝</div> <div class="card-icon">📝</div>
<h2>5. CATEGORIES OF PROCESSED PERSONAL DATA</h2> <h2>5. CATEGORIES OF PROCESSED PERSONAL DATA</h2>
@@ -172,7 +172,7 @@
<p>Include conditions for processing, storage, cross-border data transfer, work with publicly available sources, special and biometric data, as well as the procedure for obtaining clarifications in accordance with the legislation of the Russian Federation.</p> <p>Include conditions for processing, storage, cross-border data transfer, work with publicly available sources, special and biometric data, as well as the procedure for obtaining clarifications in accordance with the legislation of the Russian Federation.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>7. RIGHTS OF THE PERSONAL DATA SUBJECT</h2> <h2>7. RIGHTS OF THE PERSONAL DATA SUBJECT</h2>
@@ -194,7 +194,7 @@
<p>The Subject also has the right to protect their rights, recover damages, and receive compensation for moral harm.</p> <p>The Subject also has the right to protect their rights, recover damages, and receive compensation for moral harm.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📋</div> <div class="card-icon">📋</div>
<h2>8. OPERATOR'S OBLIGATIONS</h2> <h2>8. OPERATOR'S OBLIGATIONS</h2>
@@ -205,7 +205,7 @@
<p>8.3. The Operator bears other obligations established by FZ-152.</p> <p>8.3. The Operator bears other obligations established by FZ-152.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🛡️</div> <div class="card-icon">🛡️</div>
<h2>9. SECURITY ASSURANCE</h2> <h2>9. SECURITY ASSURANCE</h2>
@@ -222,14 +222,14 @@
</ul> </ul>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">⚠️</div> <div class="card-icon">⚠️</div>
<h2>10. LIABILITY</h2> <h2>10. LIABILITY</h2>
<p>10.1. Persons guilty of violating the norms of processing and protection of Personal Data bear liability in accordance with the legislation of the Russian Federation.</p> <p>10.1. Persons guilty of violating the norms of processing and protection of Personal Data bear liability in accordance with the legislation of the Russian Federation.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🎯</div> <div class="card-icon">🎯</div>
<h2>11. PURPOSES OF PROCESSING</h2> <h2>11. PURPOSES OF PROCESSING</h2>
@@ -248,7 +248,7 @@
</ul> </ul>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🍪</div> <div class="card-icon">🍪</div>
<h2>12. AUTOMATICALLY COLLECTED INFORMATION</h2> <h2>12. AUTOMATICALLY COLLECTED INFORMATION</h2>

View File

@@ -104,7 +104,7 @@
<p>3.1.5. Договор между оператором и третьим лицом, где последнее поручает Оператору обработку персональных данных Субъекта Персональных данных или передает на основании заключенного договора персональные данные Субъекта Персональных данных.</p> <p>3.1.5. Договор между оператором и третьим лицом, где последнее поручает Оператору обработку персональных данных Субъекта Персональных данных или передает на основании заключенного договора персональные данные Субъекта Персональных данных.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">👥</div> <div class="card-icon">👥</div>
<h2>4. КАТЕГОРИИ СУБЪЕКТОВ ПЕРСОНАЛЬНЫХ ДАННЫХ</h2> <h2>4. КАТЕГОРИИ СУБЪЕКТОВ ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
@@ -122,7 +122,7 @@
</ul> </ul>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📝</div> <div class="card-icon">📝</div>
<h2>5. КАТЕГОРИИ ОБРАБАТЫВАЕМЫХ ПЕРСОНАЛЬНЫХ ДАННЫХ</h2> <h2>5. КАТЕГОРИИ ОБРАБАТЫВАЕМЫХ ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
@@ -172,7 +172,7 @@
<p>Включают условия обработки, хранения, трансграничной передачи данных, работы с общедоступными источниками, специальными и биометрическими данными, а также порядок получения разъяснений согласно законодательству РФ.</p> <p>Включают условия обработки, хранения, трансграничной передачи данных, работы с общедоступными источниками, специальными и биометрическими данными, а также порядок получения разъяснений согласно законодательству РФ.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>7. ПРАВА СУБЪЕКТА ПЕРСОНАЛЬНЫХ ДАННЫХ</h2> <h2>7. ПРАВА СУБЪЕКТА ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
@@ -194,7 +194,7 @@
<p>Субъект также имеет право на защиту своих прав, возмещение убытков и компенсацию морального вреда.</p> <p>Субъект также имеет право на защиту своих прав, возмещение убытков и компенсацию морального вреда.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📋</div> <div class="card-icon">📋</div>
<h2>8. ОБЯЗАННОСТИ ОПЕРАТОРА</h2> <h2>8. ОБЯЗАННОСТИ ОПЕРАТОРА</h2>
@@ -205,7 +205,7 @@
<p>8.3. Оператор несет иные обязанности, установленные ФЗ-152.</p> <p>8.3. Оператор несет иные обязанности, установленные ФЗ-152.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🛡️</div> <div class="card-icon">🛡️</div>
<h2>9. ОБЕСПЕЧЕНИЕ БЕЗОПАСНОСТИ</h2> <h2>9. ОБЕСПЕЧЕНИЕ БЕЗОПАСНОСТИ</h2>
@@ -222,14 +222,14 @@
</ul> </ul>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">⚠️</div> <div class="card-icon">⚠️</div>
<h2>10. ОТВЕТСТВЕННОСТЬ</h2> <h2>10. ОТВЕТСТВЕННОСТЬ</h2>
<p>10.1. Лица, виновные в нарушении норм обработки и защиты Персональных данных, несут ответственность согласно законодательству РФ.</p> <p>10.1. Лица, виновные в нарушении норм обработки и защиты Персональных данных, несут ответственность согласно законодательству РФ.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🎯</div> <div class="card-icon">🎯</div>
<h2>11. ЦЕЛИ ОБРАБОТКИ</h2> <h2>11. ЦЕЛИ ОБРАБОТКИ</h2>
@@ -248,7 +248,7 @@
</ul> </ul>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🍪</div> <div class="card-icon">🍪</div>
<h2>12. АВТОМАТИЧЕСКИ СОБИРАЕМАЯ ИНФОРМАЦИЯ</h2> <h2>12. АВТОМАТИЧЕСКИ СОБИРАЕМАЯ ИНФОРМАЦИЯ</h2>

View File

@@ -36,12 +36,16 @@
<p>1.9. Promotional campaigns may have special rules.</p> <p>1.9. Promotional campaigns may have special rules.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🎯</div> <div class="card-icon">🎯</div>
<h2>2. Subject of the Agreement</h2> <h2>2. Subject of the Agreement</h2>
<p>2.1. The purpose is to provide users with the ability to purchase goods and services presented on the resource.</p> <p>2.1. The purpose is to provide users with the ability to purchase goods and services presented on the resource.</p>
<p>2.2. The agreement regulates the use of the website and provided features.</p> <p>2.2. The agreement regulates the use of the website and provided features.</p>
<p>2.3. It applies to all types of goods, services, and products on the website.</p> <p>2.3. It applies to all types of goods, services, and products on the website.</p>
<p>2.4. The Marketplace is an agent acting under agency agreements with Sellers (Third Parties). The Marketplace does not acquire ownership rights to the Goods/Services sold through the Platform and only provides the technical ability to conclude a transaction between the Seller and the Buyer.</p>
<p>2.5. The Seller is an independent business entity and bears full responsibility for the quality, safety, conformity of the Goods/Services to the stated characteristics, and fulfillment of obligations to the Buyer (warranties, returns, claims).</p>
<p>2.6. The Marketplace is not a party to the sale and purchase agreement between the Seller and the Buyer. All claims regarding the Goods/Services are submitted by the Buyer directly to the Seller.</p>
<p>2.7. The Marketplace's agency remuneration is withheld from the amount paid by the Buyer and does not increase the price for the Buyer beyond the price set by the Seller.</p>
</section> </section>
<section class="info-card wide"> <section class="info-card wide">
@@ -57,7 +61,7 @@
<p><strong>3.8. Applicable laws:</strong> Governed by Federal Law "On Consumer Rights Protection" No. 2300-1 and the Civil Code of the Russian Federation.</p> <p><strong>3.8. Applicable laws:</strong> Governed by Federal Law "On Consumer Rights Protection" No. 2300-1 and the Civil Code of the Russian Federation.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📱</div> <div class="card-icon">📱</div>
<h2>4. Registration and Personal Account</h2> <h2>4. Registration and Personal Account</h2>
<p><strong>4.1. Procedure:</strong> Registration is not required for ordering but provides access to the personal account.</p> <p><strong>4.1. Procedure:</strong> Registration is not required for ordering but provides access to the personal account.</p>
@@ -114,7 +118,7 @@
<p><strong>Rights:</strong> Opt out of advertising messages through the website or by writing to <a href="mailto:info@novo.market">info@novo.market</a>.</p> <p><strong>Rights:</strong> Opt out of advertising messages through the website or by writing to <a href="mailto:info@novo.market">info@novo.market</a>.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">©</div> <div class="card-icon">©</div>
<h2>7. Exclusive Rights to Content</h2> <h2>7. Exclusive Rights to Content</h2>
<p><strong>7.1. Intellectual property:</strong> All content (design, texts, graphics, video, software, databases) is subject to the copyright of the owner and rights holders.</p> <p><strong>7.1. Intellectual property:</strong> All content (design, texts, graphics, video, software, databases) is subject to the copyright of the owner and rights holders.</p>
@@ -122,7 +126,7 @@
<p><strong>7.3. Personal use:</strong> For personal non-commercial use while preserving copyright notices.</p> <p><strong>7.3. Personal use:</strong> For personal non-commercial use while preserving copyright notices.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🔗</div> <div class="card-icon">🔗</div>
<h2>8. Third-Party Websites and Content</h2> <h2>8. Third-Party Websites and Content</h2>
<p><strong>8.1. External links:</strong> The website may contain links to third-party resources. The website owner is not responsible for their content.</p> <p><strong>8.1. External links:</strong> The website may contain links to third-party resources. The website owner is not responsible for their content.</p>
@@ -218,14 +222,14 @@
<p><strong>13.7. Return method:</strong> The return method is specified in the application.</p> <p><strong>13.7. Return method:</strong> The return method is specified in the application.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>14. Term of the Agreement</h2> <h2>14. Term of the Agreement</h2>
<p><strong>14.1. Commencement and termination:</strong> The agreement is active from the moment of acceptance by the user until revocation of acceptance.</p> <p><strong>14.1. Commencement and termination:</strong> The agreement is active from the moment of acceptance by the user until revocation of acceptance.</p>
<p><strong>14.2. Right of revocation:</strong> The owner may revoke the offer pursuant to Art. 436 of the Civil Code of the Russian Federation.</p> <p><strong>14.2. Right of revocation:</strong> The owner may revoke the offer pursuant to Art. 436 of the Civil Code of the Russian Federation.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>15. Dispute Resolution Procedure</h2> <h2>15. Dispute Resolution Procedure</h2>
<p><strong>15.1. Voluntary settlement:</strong> Mandatory pre-trial dispute resolution procedure.</p> <p><strong>15.1. Voluntary settlement:</strong> Mandatory pre-trial dispute resolution procedure.</p>
@@ -247,12 +251,12 @@
<p><strong>16.3. Legislation:</strong> Issues are resolved under the legislation of Armenia.</p> <p><strong>16.3. Legislation:</strong> Issues are resolved under the legislation of Armenia.</p>
<p><strong>16.4. The term "Legislation":</strong> Refers to the laws of Armenia.</p> <p><strong>16.4. The term "Legislation":</strong> Refers to the laws of Armenia.</p>
<p><strong>16.5. Free services:</strong> Do not imply the application of consumer protection regulations.</p> <p><strong>16.5. Free services:</strong> Do not imply the application of consumer protection regulations.</p>
<p><strong>16.6. Absence of relationships:</strong> Do not establish agency, partnership, or employment relationships.</p> <p><strong>16.6. Absence of relationships:</strong> Except for the agency model under clause 2.4, the terms do not establish partnership, joint venture, or employment relationships.</p>
<p><strong>16.7. Invalidity of clauses:</strong> Does not affect the legal force of the remaining provisions.</p> <p><strong>16.7. Invalidity of clauses:</strong> Does not affect the legal force of the remaining provisions.</p>
<p><strong>16.8. Response to violations:</strong> Non-intervention does not prevent subsequent protective measures.</p> <p><strong>16.8. Response to violations:</strong> Non-intervention does not prevent subsequent protective measures.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h2>Contact Us</h2> <h2>Contact Us</h2>
<p>Questions about the agreement:</p> <p>Questions about the agreement:</p>

View File

@@ -36,12 +36,16 @@
<p>1.9. Գովազդային ակցիաները կարող են ունենալ հատուկ կանոններ։</p> <p>1.9. Գովազդային ակցիաները կարող են ունենալ հատուկ կանոններ։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🎯</div> <div class="card-icon">🎯</div>
<h2>2. Համաձայնագրի առարկան</h2> <h2>2. Համաձայնագրի առարկան</h2>
<p>2.1. Նպատակն է օգտագործողներին հնարավորություն տալ ռեսուրսում ներկայացված ապրանքներ և ծառայություններ ձեռք բերելու։</p> <p>2.1. Նպատակն է օգտագործողներին հնարավորություն տալ ռեսուրսում ներկայացված ապրանքներ և ծառայություններ ձեռք բերելու։</p>
<p>2.2. Համաձայնագիրը կարգավորում է կայքի օգտագործման և տրամադրվող գործառույթների կարգը։</p> <p>2.2. Համաձայնագիրը կարգավորում է կայքի օգտագործման և տրամադրվող գործառույթների կարգը։</p>
<p>2.3. Գործում է կայքում ներկայացված ապրանքների՝ ծառայությունների և պրոդուկտների բոլոր տեսակների նկատմամբ։</p> <p>2.3. Գործում է կայքում ներկայացված ապրանքների՝ ծառայությունների և պրոդուկտների բոլոր տեսակների նկատմամբ։</p>
<p>2.4. Մարկետփլեյսը հանդիսանում է գործակալ, որը գործում է Վաճառողների (Երրորդ անձանց) հետ կնքված գործակալության պայմանագրի հիման վրա։ Մարկետփլեյսը չի ձեռք բերում Հարթակի միջոցով իրացվող Ապրանքների/Ծառայությունների նկատմամբ սեփականության իրավունք, այլ միայն ապահովում է Վաճառողի և Գնորդի միջև գործարք կնքելու տեխնիկական հնարավորությունը։</p>
<p>2.5. Վաճառողը հանդիսանում է ինքնուրույն տնտեսվարող սուբյեկտ և կրում է ամբողջական պատասխանատվություն Ապրանքի/Ծառայության որակի, անվտանգության, հայտարարված բնութագրերին համապատասխանության, ինչպես նաև Գնորդի նկատմամբ պարտավորությունների կատարման համար (երաշխիքներ, վերադարձ, պահանջներ)։</p>
<p>2.6. Մարկետփլեյսը չի հանդիսանում Վաճառողի և Գնորդի միջև առուվաճառքի պայմանագրի կողմ։ Ապրանքի/Ծառայության վերաբերյալ բոլոր պահանջները Գնորդը ներկայացնում է անմիջապես Վաճառողին։</p>
<p>2.7. Մարկետփլեյսի գործակալական վարձատրությունը պահվում է Գնորդի վճարած գումարից և չի ավելացնում Գնորդի համար գինը Վաճառողի սահմանած գնից ավել։</p>
</section> </section>
<section class="info-card wide"> <section class="info-card wide">
@@ -57,7 +61,7 @@
<p><strong>3.8. Կիրառվող օրենքներ՝</strong> Կարգավորվում են ՌԴ «Սպառողների իրավունքների պաշտպանության մասին» թիվ 2300-1 և ՌԴ քաղաքացիական օրենսգիրքով։</p> <p><strong>3.8. Կիրառվող օրենքներ՝</strong> Կարգավորվում են ՌԴ «Սպառողների իրավունքների պաշտպանության մասին» թիվ 2300-1 և ՌԴ քաղաքացիական օրենսգիրքով։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📱</div> <div class="card-icon">📱</div>
<h2>4. Գրանցում և անձնական հաշիվ</h2> <h2>4. Գրանցում և անձնական հաշիվ</h2>
<p><strong>4.1. Ըթացակարգ՝</strong> Գրանցումը պարտադիր չէ պատվերի համար՝ սակայն հնարավորություն է տալիս անձնական հաշիվ մուտք գործելու։</p> <p><strong>4.1. Ըթացակարգ՝</strong> Գրանցումը պարտադիր չէ պատվերի համար՝ սակայն հնարավորություն է տալիս անձնական հաշիվ մուտք գործելու։</p>
@@ -114,7 +118,7 @@
<p><strong>Իրավունքներ՝</strong> Հրաժարվել գովազդային հաղորդագրություններից կայքի միճոցով կամ նամակով <a href="mailto:info@novo.market">info@novo.market</a>։</p> <p><strong>Իրավունքներ՝</strong> Հրաժարվել գովազդային հաղորդագրություններից կայքի միճոցով կամ նամակով <a href="mailto:info@novo.market">info@novo.market</a>։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">©</div> <div class="card-icon">©</div>
<h2>7. Բովանդակության բացառիկ իրավունքներ</h2> <h2>7. Բովանդակության բացառիկ իրավունքներ</h2>
<p><strong>7.1. Մտավոր սեփականություն՝</strong> Բոլոր բովանդակությունը (դիզայն՝ տեքստեր՝ գրաֆիկա՝ տեսանյութ՝ ծրագրեր՝ տվյալների բազաներ) սեփականատիրոջ և իրավատերերի հեղինակային իրավունքների առարկան է։</p> <p><strong>7.1. Մտավոր սեփականություն՝</strong> Բոլոր բովանդակությունը (դիզայն՝ տեքստեր՝ գրաֆիկա՝ տեսանյութ՝ ծրագրեր՝ տվյալների բազաներ) սեփականատիրոջ և իրավատերերի հեղինակային իրավունքների առարկան է։</p>
@@ -122,7 +126,7 @@
<p><strong>7.3. Անձնական օգտագործում՝</strong> Անձնական ոչ առևտրային օգտագործման համար՝ հեղինակային իրավունքների նիշերի պահպանմամբ։</p> <p><strong>7.3. Անձնական օգտագործում՝</strong> Անձնական ոչ առևտրային օգտագործման համար՝ հեղինակային իրավունքների նիշերի պահպանմամբ։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🔗</div> <div class="card-icon">🔗</div>
<h2>8. Երրորդ կողմերի կայքեր և բովանդակություն</h2> <h2>8. Երրորդ կողմերի կայքեր և բովանդակություն</h2>
<p><strong>8.1. Արտաքին հղումներ՝</strong> Կայքը կարող է պարունակել երրորդ կողմի ռեսուրսների հղումներ։ Կայքի սեփականատերը պատասխանատու չէ դրանց բովանդակության համար։</p> <p><strong>8.1. Արտաքին հղումներ՝</strong> Կայքը կարող է պարունակել երրորդ կողմի ռեսուրսների հղումներ։ Կայքի սեփականատերը պատասխանատու չէ դրանց բովանդակության համար։</p>
@@ -218,14 +222,14 @@
<p><strong>13.7. Վերադարձի մեխանիզմ՝</strong> Վերադարձի եղանակը նշվում է դիմումում։</p> <p><strong>13.7. Վերադարձի մեխանիզմ՝</strong> Վերադարձի եղանակը նշվում է դիմումում։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>14. Համաձայնագրի գործողության ժամկետը</h2> <h2>14. Համաձայնագրի գործողության ժամկետը</h2>
<p><strong>14.1. Սկիզբ և դադարեցում՝</strong> Համաձայնագիրը գործում է օգտագործողի ընդունման պահից մինչև ընդունման հետկանչում։</p> <p><strong>14.1. Սկիզբ և դադարեցում՝</strong> Համաձայնագիրը գործում է օգտագործողի ընդունման պահից մինչև ընդունման հետկանչում։</p>
<p><strong>14.2. Հետկանչման իրավունք՝</strong> Սեփականատերը կարող է օֆերտան հետ կանչել ՌԴ 436 հոդվածի համաձայն։</p> <p><strong>14.2. Հետկանչման իրավունք՝</strong> Սեփականատերը կարող է օֆերտան հետ կանչել ՌԴ 436 հոդվածի համաձայն։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>15. Վեճերի լուծման կարգը</h2> <h2>15. Վեճերի լուծման կարգը</h2>
<p><strong>15.1. Կամավոր կարգավորում՝</strong> Պարտադիր նախադատական վեճերի լուծման ընթացակարգ։</p> <p><strong>15.1. Կամավոր կարգավորում՝</strong> Պարտադիր նախադատական վեճերի լուծման ընթացակարգ։</p>
@@ -247,12 +251,12 @@
<p><strong>16.3. Օրենսդրություն՝</strong> Հարցերը լուծվում են Հայաստանի օրենսդրությամբ։</p> <p><strong>16.3. Օրենսդրություն՝</strong> Հարցերը լուծվում են Հայաստանի օրենսդրությամբ։</p>
<p><strong>16.4. «Օրենսդրություն» տերմին՝</strong> Նշանակում է Հայաստանի օրենքները։</p> <p><strong>16.4. «Օրենսդրություն» տերմին՝</strong> Նշանակում է Հայաստանի օրենքները։</p>
<p><strong>16.5. Անվճար ծառայություններ՝</strong> Չեն ենթադրում սպառողների իրավունքների պաշտպանության նորմերի կիրառում։</p> <p><strong>16.5. Անվճար ծառայություններ՝</strong> Չեն ենթադրում սպառողների իրավունքների պաշտպանության նորմերի կիրառում։</p>
<p><strong>16.6. Հարաբերությունների բացակայություն՝</strong> Չեն հաստատում գործակալային՝ գործընկերային կամ աշխատանքային հարաբերություններ։</p> <p><strong>16.6. Հարաբերությունների բացակայություն՝</strong> Բացի 2.4 կետով նախատեսված գործակալական մոդելից՝ պայմանները չեն հաստատում գործընկերային, համատեղ ձեռնարկության կամ աշխատանքային հարաբերություններ։</p>
<p><strong>16.7. Կետերի անվավերություն՝</strong> Չի ազդում մնացած դրույթների իրավական ուժի վրա։</p> <p><strong>16.7. Կետերի անվավերություն՝</strong> Չի ազդում մնացած դրույթների իրավական ուժի վրա։</p>
<p><strong>16.8. Խախտումների նկատմամբ արձագանք՝</strong> Անգործունեությունը չի խանգարում հետագա պաշտպանության միջոցներին։</p> <p><strong>16.8. Խախտումների նկատմամբ արձագանք՝</strong> Անգործունեությունը չի խանգարում հետագա պաշտպանության միջոցներին։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h2>Կապ</h2> <h2>Կապ</h2>
<p>Համաձայնագրի վերաբերյալ հարցեր՝</p> <p>Համաձայնագրի վերաբերյալ հարցեր՝</p>

View File

@@ -36,12 +36,16 @@
<p>1.9. Промо-кампании могут иметь специальные правила.</p> <p>1.9. Промо-кампании могут иметь специальные правила.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🎯</div> <div class="card-icon">🎯</div>
<h2>2. Предмет соглашения</h2> <h2>2. Предмет соглашения</h2>
<p>2.1. Целью является предоставление пользователям возможности покупать товары и услуги, представленные на ресурсе.</p> <p>2.1. Целью является предоставление пользователям возможности покупать товары и услуги, представленные на ресурсе.</p>
<p>2.2. Соглашение регулирует порядок использования сайта и предоставляемых функций.</p> <p>2.2. Соглашение регулирует порядок использования сайта и предоставляемых функций.</p>
<p>2.3. Действие распространяется на все типы товаров, услуг и продуктов на сайте.</p> <p>2.3. Действие распространяется на все типы товаров, услуг и продуктов на сайте.</p>
<p>2.4. Маркетплейс является агентом, действующим на основании договора агентирования с Продавцами (Третьими лицами). Маркетплейс не приобретает право собственности на Товары/Услуги, реализуемые через Платформу, а лишь обеспечивает техническую возможность заключения сделки между Продавцом и Покупателем.</p>
<p>2.5. Продавец является самостоятельным хозяйствующим субъектом и несёт полную ответственность за качество, безопасность, соответствие Товара/Услуги заявленным характеристикам, а также за исполнение обязательств перед Покупателем (гарантии, возврат, претензии).</p>
<p>2.6. Маркетплейс не выступает стороной договора купли-продажи между Продавцом и Покупателем. Все претензии по Товару/Услуге Покупатель предъявляет непосредственно Продавцу.</p>
<p>2.7. Агентское вознаграждение Маркетплейса удерживается из суммы, уплаченной Покупателем, и не увеличивает цену для Покупателя сверх установленной Продавцом.</p>
</section> </section>
<section class="info-card wide"> <section class="info-card wide">
@@ -57,7 +61,7 @@
<p><strong>3.8. Применение законов:</strong> Регулируются ФЗ "О защите прав потребителей" №2300-1 и ГК РФ.</p> <p><strong>3.8. Применение законов:</strong> Регулируются ФЗ "О защите прав потребителей" №2300-1 и ГК РФ.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📱</div> <div class="card-icon">📱</div>
<h2>4. Регистрация и личный кабинет</h2> <h2>4. Регистрация и личный кабинет</h2>
<p><strong>4.1. Процедура:</strong> Регистрация необязательна для заказа, но открывает доступ к личному кабинету.</p> <p><strong>4.1. Процедура:</strong> Регистрация необязательна для заказа, но открывает доступ к личному кабинету.</p>
@@ -114,7 +118,7 @@
<p><strong>Права:</strong> Отказаться от рекламных сообщений через сайт или письмом на <a href="mailto:info@novo.market">info@novo.market</a>.</p> <p><strong>Права:</strong> Отказаться от рекламных сообщений через сайт или письмом на <a href="mailto:info@novo.market">info@novo.market</a>.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">©</div> <div class="card-icon">©</div>
<h2>7. Исключительные права на контент</h2> <h2>7. Исключительные права на контент</h2>
<p><strong>7.1. Интеллектуальная собственность:</strong> Весь контент (дизайн, тексты, графика, видео, программы, базы данных) является объектом авторских прав владельца и правообладателей.</p> <p><strong>7.1. Интеллектуальная собственность:</strong> Весь контент (дизайн, тексты, графика, видео, программы, базы данных) является объектом авторских прав владельца и правообладателей.</p>
@@ -122,7 +126,7 @@
<p><strong>7.3. Личное использование:</strong> Для личного некоммерческого использования при сохранении обозначений авторских прав.</p> <p><strong>7.3. Личное использование:</strong> Для личного некоммерческого использования при сохранении обозначений авторских прав.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🔗</div> <div class="card-icon">🔗</div>
<h2>8. Сайты и контент третьих лиц</h2> <h2>8. Сайты и контент третьих лиц</h2>
<p><strong>8.1. Внешние ссылки:</strong> Сайт может содержать ссылки на сторонние ресурсы. Ответственность за их содержание не относится к владельцу сайта.</p> <p><strong>8.1. Внешние ссылки:</strong> Сайт может содержать ссылки на сторонние ресурсы. Ответственность за их содержание не относится к владельцу сайта.</p>
@@ -218,14 +222,14 @@
<p><strong>13.7. Механизм возврата:</strong> Способ возврата указывается в заявлении.</p> <p><strong>13.7. Механизм возврата:</strong> Способ возврата указывается в заявлении.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>14. Срок действия соглашения</h2> <h2>14. Срок действия соглашения</h2>
<p><strong>14.1. Начало и прекращение:</strong> Соглашение активно с момента принятия пользователем до отзыва акцепта.</p> <p><strong>14.1. Начало и прекращение:</strong> Соглашение активно с момента принятия пользователем до отзыва акцепта.</p>
<p><strong>14.2. Право отзыва:</strong> Владелец может отозвать оферту согласно ст. 436 ГК РФ.</p> <p><strong>14.2. Право отзыва:</strong> Владелец может отозвать оферту согласно ст. 436 ГК РФ.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon"></div> <div class="card-icon"></div>
<h2>15. Порядок разрешения споров</h2> <h2>15. Порядок разрешения споров</h2>
<p><strong>15.1. Добровольное урегулирование:</strong> Обязательный досудебный порядок рассмотрения споров.</p> <p><strong>15.1. Добровольное урегулирование:</strong> Обязательный досудебный порядок рассмотрения споров.</p>
@@ -247,12 +251,12 @@
<p><strong>16.3. Законодательство:</strong> Вопросы разрешаются по законодательству Армении.</p> <p><strong>16.3. Законодательство:</strong> Вопросы разрешаются по законодательству Армении.</p>
<p><strong>16.4. Термин "Законодательство":</strong> Подразумевает законы Армении.</p> <p><strong>16.4. Термин "Законодательство":</strong> Подразумевает законы Армении.</p>
<p><strong>16.5. Бесплатные услуги:</strong> Не предполагают применение норм о защите прав потребителей.</p> <p><strong>16.5. Бесплатные услуги:</strong> Не предполагают применение норм о защите прав потребителей.</p>
<p><strong>16.6. Отсутствие отношений:</strong> Не устанавливают агентских связей, партнёрства или трудовых отношений.</p> <p><strong>16.6. Отсутствие отношений:</strong> Кроме агентской модели по п. 2.4, условия не устанавливают партнёрства, совместного предприятия или трудовых отношений.</p>
<p><strong>16.7. Недействительность пунктов:</strong> Не влияет на законную силу остальных положений.</p> <p><strong>16.7. Недействительность пунктов:</strong> Не влияет на законную силу остальных положений.</p>
<p><strong>16.8. Реакция на нарушения:</strong> Невмешательство не препятствует последующим мерам защиты.</p> <p><strong>16.8. Реакция на нарушения:</strong> Невмешательство не препятствует последующим мерам защиты.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📞</div> <div class="card-icon">📞</div>
<h2>Контакты</h2> <h2>Контакты</h2>
<p>Вопросы по соглашению:</p> <p>Вопросы по соглашению:</p>

View File

@@ -16,7 +16,7 @@
<p>1.5. Digital products are returned under special rules prescribed by legislation.</p> <p>1.5. Digital products are returned under special rules prescribed by legislation.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📅</div> <div class="card-icon">📅</div>
<h2>2. Time Frames</h2> <h2>2. Time Frames</h2>
<p><strong>2.1. Quality products:</strong></p> <p><strong>2.1. Quality products:</strong></p>
@@ -112,7 +112,7 @@
<p>Our platform is always ready to help resolve disputes.</p> <p>Our platform is always ready to help resolve disputes.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">💰</div> <div class="card-icon">💰</div>
<h2>6. Payment Refund</h2> <h2>6. Payment Refund</h2>
<p>Funds are refunded to the same payment method:</p> <p>Funds are refunded to the same payment method:</p>
@@ -134,14 +134,14 @@
<p>The customer has the right to claim compensation for defective products and shipping costs.</p> <p>The customer has the right to claim compensation for defective products and shipping costs.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🔄</div> <div class="card-icon">🔄</div>
<h2>7. Product Exchange</h2> <h2>7. Product Exchange</h2>
<p>If a non-defective product does not match in dimensions, size, or specifications, the customer may exchange it with the supplier for a suitable option.</p> <p>If a non-defective product does not match in dimensions, size, or specifications, the customer may exchange it with the supplier for a suitable option.</p>
<p>If the desired model is unavailable, the customer has the right to cancel the order and receive a refund.</p> <p>If the desired model is unavailable, the customer has the right to cancel the order and receive a refund.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🛡️</div> <div class="card-icon">🛡️</div>
<h2>8. Warranty Service</h2> <h2>8. Warranty Service</h2>
<p>Warranty information is displayed on the product page or available from the seller.</p> <p>Warranty information is displayed on the product page or available from the seller.</p>
@@ -149,7 +149,7 @@
<p>Warranty service does not cover physical damage, improper use, or unauthorized modifications.</p> <p>Warranty service does not cover physical damage, improper use, or unauthorized modifications.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">⚖️</div> <div class="card-icon">⚖️</div>
<h2>9. Responsibilities of Parties</h2> <h2>9. Responsibilities of Parties</h2>
<p>The supplier is responsible for product quality and timely refunds.</p> <p>The supplier is responsible for product quality and timely refunds.</p>

View File

@@ -16,7 +16,7 @@
<p>1.5. Թվային ապրանքները վերադարձվում են օրենսդրությամբ նախատեսված հատուկ կանոններով։</p> <p>1.5. Թվային ապրանքները վերադարձվում են օրենսդրությամբ նախատեսված հատուկ կանոններով։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📅</div> <div class="card-icon">📅</div>
<h2>2. Ժամանակային սահմաններ</h2> <h2>2. Ժամանակային սահմաններ</h2>
<p><strong>2.1. Որակյալ ապրանքներ՝</strong></p> <p><strong>2.1. Որակյալ ապրանքներ՝</strong></p>
@@ -112,7 +112,7 @@
<p>Մեր հարթակը միշտ պատրաստ է օգնել վիճելի իրավիճակների կարգավորման հարցում։</p> <p>Մեր հարթակը միշտ պատրաստ է օգնել վիճելի իրավիճակների կարգավորման հարցում։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">💰</div> <div class="card-icon">💰</div>
<h2>6. Վճարման փոխհատուցում</h2> <h2>6. Վճարման փոխհատուցում</h2>
<p>Գումարը վերադարձվում է նույն վճարման եղանակով՝</p> <p>Գումարը վերադարձվում է նույն վճարման եղանակով՝</p>
@@ -134,14 +134,14 @@
<p>Գնորդը իրավունք ունի պահանջել փոխհատուցում անորակ ապրանքների և առաքման ծախսերի համար։</p> <p>Գնորդը իրավունք ունի պահանջել փոխհատուցում անորակ ապրանքների և առաքման ծախսերի համար։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🔄</div> <div class="card-icon">🔄</div>
<h2>7. Ապրանքի փոխանակում</h2> <h2>7. Ապրանքի փոխանակում</h2>
<p>Եթե որակյալ ապրանքը չի համապատասխանում չափսերով, չափորոշներով կամ հատկանիշներով, գնորդը կարող է այն փոխանակել մատակարարի մոտ համապատասխան տարբերակով։</p> <p>Եթե որակյալ ապրանքը չի համապատասխանում չափսերով, չափորոշներով կամ հատկանիշներով, գնորդը կարող է այն փոխանակել մատակարարի մոտ համապատասխան տարբերակով։</p>
<p>Համապատասխան մոդելի բացակայության դեպքում գնորդը կարող է հրաժարվել պատվերից և ստանալ փոխհատուցում։</p> <p>Համապատասխան մոդելի բացակայության դեպքում գնորդը կարող է հրաժարվել պատվերից և ստանալ փոխհատուցում։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🛡️</div> <div class="card-icon">🛡️</div>
<h2>8. Երաշխիքային սպասարկում</h2> <h2>8. Երաշխիքային սպասարկում</h2>
<p>Երաշխիքի պայմանները նշված են ապրանքի էջում կամ հասանելի են վաճառողից։</p> <p>Երաշխիքի պայմանները նշված են ապրանքի էջում կամ հասանելի են վաճառողից։</p>
@@ -149,7 +149,7 @@
<p>Երաշխիքային սպասարկումը չի տարածվում ֆիզիկական վնասների, ոչ պատշաճ օգտագործման կամ ինքնուրույն միջամտության դեպքում։</p> <p>Երաշխիքային սպասարկումը չի տարածվում ֆիզիկական վնասների, ոչ պատշաճ օգտագործման կամ ինքնուրույն միջամտության դեպքում։</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">⚖️</div> <div class="card-icon">⚖️</div>
<h2>9. Կողմերի պարտականություններ</h2> <h2>9. Կողմերի պարտականություններ</h2>
<p>Մատակարարը պատասխանատու է ապրանքի որակի և ժամանակային գումարի վերադարձի համար։</p> <p>Մատակարարը պատասխանատու է ապրանքի որակի և ժամանակային գումարի վերադարձի համար։</p>

View File

@@ -16,7 +16,7 @@
<p>1.5. Электронные товары возвращаются по специальным правилам, прописанным в законодательстве.</p> <p>1.5. Электронные товары возвращаются по специальным правилам, прописанным в законодательстве.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">📅</div> <div class="card-icon">📅</div>
<h2>2. Временные рамки</h2> <h2>2. Временные рамки</h2>
<p><strong>2.1. Качественная продукция:</strong></p> <p><strong>2.1. Качественная продукция:</strong></p>
@@ -112,7 +112,7 @@
<p>Наша площадка всегда готова помочь в урегулировании спорных ситуаций.</p> <p>Наша площадка всегда готова помочь в урегулировании спорных ситуаций.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">💰</div> <div class="card-icon">💰</div>
<h2>6. Компенсация платежа</h2> <h2>6. Компенсация платежа</h2>
<p>Деньги возмещаются на тот же способ оплаты:</p> <p>Деньги возмещаются на тот же способ оплаты:</p>
@@ -134,14 +134,14 @@
<p>Клиент имеет право потребовать компенсацию за некачественную продукцию и расходы по пересылке.</p> <p>Клиент имеет право потребовать компенсацию за некачественную продукцию и расходы по пересылке.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🔄</div> <div class="card-icon">🔄</div>
<h2>7. Обмен продукции</h2> <h2>7. Обмен продукции</h2>
<p>При несоответствии исправного изделия по габаритам, размерам или параметрам, клиент может заменить его у поставщика на подходящий вариант.</p> <p>При несоответствии исправного изделия по габаритам, размерам или параметрам, клиент может заменить его у поставщика на подходящий вариант.</p>
<p>При отсутствии нужной модели клиент вправе отказаться от заказа и получить компенсацию.</p> <p>При отсутствии нужной модели клиент вправе отказаться от заказа и получить компенсацию.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">🛡️</div> <div class="card-icon">🛡️</div>
<h2>8. Сервисное обслуживание</h2> <h2>8. Сервисное обслуживание</h2>
<p>Информация о гарантийных условиях размещена в карточке товара или доступна у продавца.</p> <p>Информация о гарантийных условиях размещена в карточке товара или доступна у продавца.</p>
@@ -149,7 +149,7 @@
<p>Гарантийное обслуживание не распространяется на физические ущербы, неправильное применение или самостоятельное вмешательство.</p> <p>Гарантийное обслуживание не распространяется на физические ущербы, неправильное применение или самостоятельное вмешательство.</p>
</section> </section>
<section class="info-card"> <section class="info-card wide">
<div class="card-icon">⚖️</div> <div class="card-icon">⚖️</div>
<h2>9. Обязанности участников</h2> <h2>9. Обязанности участников</h2>
<p>Поставщик несет ответственность за качество продукции и своевременное возмещение платежей.</p> <p>Поставщик несет ответственность за качество продукции и своевременное возмещение платежей.</p>

View File

@@ -0,0 +1,34 @@
<div class="delivery-selector">
@if (isDigital) {
<div class="delivery-chip delivery-chip--digital">{{ 'cart.digitalDelivery' | translate }}</div>
} @else if (options.length > 0) {
<label class="delivery-label" [for]="selectId">{{ 'cart.deliveryMethod' | translate }}</label>
<div class="delivery-control">
<select [id]="selectId" [ngModel]="selectedKey" (ngModelChange)="onSelectionChange($event)">
@if (required) {
<option value="">{{ 'cart.selectDelivery' | translate }}</option>
}
@for (option of options; track trackByOption($index, option)) {
<option [value]="optionKey(option)">{{ optionLabel(option) }}</option>
}
</select>
</div>
@if (selectedDelivery) {
<div class="delivery-meta">
@if (selectedDelivery.deliveryPlace) {
<span>{{ 'cart.deliveryPlace' | translate }}: {{ selectedDelivery.deliveryPlace }}</span>
}
@if (selectedDelivery.deliveryTime) {
<span>{{ 'cart.deliveryTime' | translate }}: {{ selectedDelivery.deliveryTime }}</span>
}
<span>
{{ 'cart.deliveryLabel' | translate }}:
{{ selectedDeliveryTotal | number:'1.2-2' }} {{ currency }}
</span>
</div>
}
}
</div>

View File

@@ -0,0 +1,81 @@
:host {
display: block;
}
.delivery-selector {
margin-top: 12px;
padding: 12px;
border-radius: 12px;
border: 1px solid #d3dad9;
background: #fafbfb;
}
.delivery-label {
display: block;
margin-bottom: 8px;
font-size: 0.8rem;
font-weight: 700;
color: #3f5f5c;
}
.delivery-control select {
width: 100%;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid #c9d5d3;
background: #fff;
color: #1f2937;
font-size: 0.9rem;
line-height: 1.4;
}
.delivery-control select:focus {
outline: none;
border-color: #497671;
box-shadow: 0 0 0 3px rgba(73, 118, 113, 0.12);
}
.delivery-meta {
display: grid;
gap: 4px;
margin-top: 10px;
font-size: 0.82rem;
color: #697777;
line-height: 1.45;
}
.delivery-chip {
display: inline-flex;
align-items: center;
padding: 6px 10px;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 700;
}
.delivery-chip--digital {
color: #2563eb;
background: rgba(37, 99, 235, 0.12);
}
:host-context(.cart-container.novo) .delivery-selector {
border-color: #d1fae5;
background: #f9fffc;
}
:host-context(.cart-container.novo) .delivery-label {
color: #047857;
}
:host-context(.cart-container.novo) .delivery-control select {
border-color: #bbf7d0;
}
:host-context(.cart-container.novo) .delivery-control select:focus {
border-color: #10b981;
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.12);
}
:host-context(.cart-container.novo) .delivery-meta {
color: #4b5563;
}

View File

@@ -0,0 +1,78 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { CartItem, DeliveryOption } from '../../models';
import { TranslatePipe } from '../../i18n/translate.pipe';
let nextDeliverySelectorId = 0;
@Component({
selector: 'app-delivery-selector',
standalone: true,
imports: [FormsModule, DecimalPipe, TranslatePipe],
templateUrl: './delivery-selector.component.html',
styleUrls: ['./delivery-selector.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DeliverySelectorComponent {
@Input({ required: true }) item: CartItem | null = null;
@Output() selectedDeliveryChange = new EventEmitter<DeliveryOption | null>();
readonly selectId = `delivery-select-${nextDeliverySelectorId++}`;
get options(): DeliveryOption[] {
return this.item?.deliveryOptions ?? [];
}
get selectedDelivery(): DeliveryOption | null {
return this.item?.selectedDelivery ?? null;
}
get currency(): string {
return this.item?.currency || 'RUB';
}
get required(): boolean {
return this.item?.deliveryMode !== 'digital'
&& this.options.length > 0
&& this.item?.deliverySelectionRequired !== false;
}
get isDigital(): boolean {
return this.item?.deliveryMode === 'digital';
}
get selectedKey(): string {
return this.selectedDelivery ? this.optionKey(this.selectedDelivery) : '';
}
get selectedDeliveryTotal(): number {
return (this.selectedDelivery?.deliveryPrice ?? 0) * (this.item?.quantity ?? 1);
}
optionKey(option: DeliveryOption): string {
return `${option.deliveryPlace}__${option.deliveryTime}__${option.deliveryPrice}`;
}
optionLabel(option: DeliveryOption): string {
const details = [option.deliveryPlace, option.deliveryTime].filter(Boolean);
details.push(`${option.deliveryPrice.toFixed(2)} ${this.currency}`);
return details.join(' • ');
}
trackByOption(index: number, option: DeliveryOption): string {
return `${index}-${this.optionKey(option)}`;
}
onSelectionChange(nextKey: string): void {
if (!nextKey) {
this.selectedDeliveryChange.emit(null);
return;
}
this.selectedDeliveryChange.emit(
this.options.find(option => this.optionKey(option) === nextKey) ?? null
);
}
}

View File

@@ -3,7 +3,7 @@
<header class="novo-header"> <header class="novo-header">
<div class="novo-header-container"> <div class="novo-header-container">
<div class="novo-left"> <div class="novo-left">
<a [routerLink]="'/' | langRoute" class="novo-logo" (click)="closeMenu()"> <a [attr.href]="homeUrl" class="novo-logo" (click)="navigateHome($event)">
<app-logo /> <app-logo />
<!-- <span class="novo-brand">{{ brandName }}</span> --> <!-- <span class="novo-brand">{{ brandName }}</span> -->
</a> </a>
@@ -24,6 +24,11 @@
{{ 'header.contacts' | translate }} {{ 'header.contacts' | translate }}
</a> </a>
</div> </div>
<div class="novo-mobile-controls">
<app-region-selector />
<app-language-selector />
</div>
</nav> </nav>
<div class="novo-right"> <div class="novo-right">
@@ -31,6 +36,7 @@
<app-language-selector /> <app-language-selector />
<a [routerLink]="'/cart' | langRoute" routerLinkActive="novo-cart-active" class="novo-cart" (click)="closeMenu()" [attr.aria-label]="'header.cart' | translate"> <a [routerLink]="'/cart' | langRoute" routerLinkActive="novo-cart-active" class="novo-cart" (click)="closeMenu()" [attr.aria-label]="'header.cart' | translate">
<span class="novo-cart-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> <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>
@@ -39,9 +45,13 @@
@if (cartItemCount() > 0) { @if (cartItemCount() > 0) {
<span class="novo-cart-badge">{{ cartItemCount() }}</span> <span class="novo-cart-badge">{{ cartItemCount() }}</span>
} }
</span>
@if (cartItemCount() > 0) {
<span class="novo-cart-total">{{ formatCartTotal(cartTotal()) }}</span>
}
</a> </a>
<button class="menu-toggle" (click)="toggleMenu()" [class.active]="menuOpen" [attr.aria-label]="menuOpen ? 'Close menu' : 'Open menu'" [attr.aria-expanded]="menuOpen"> <button class="menu-toggle novo-menu-toggle" (click)="toggleMenu()" [class.active]="menuOpen" [class.novo-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>
@@ -54,7 +64,7 @@
<header class="dexar-header"> <header class="dexar-header">
<div class="dexar-header-container"> <div class="dexar-header-container">
<!-- Logo --> <!-- Logo -->
<a [routerLink]="'/' | langRoute" class="dexar-logo" (click)="closeMenu()"> <a [attr.href]="homeUrl" class="dexar-logo" (click)="navigateHome($event)">
<app-logo /> <app-logo />
</a> </a>
@@ -97,6 +107,7 @@
<div class="dexar-actions"> <div class="dexar-actions">
<!-- Cart Button --> <!-- Cart Button -->
<a [routerLink]="'/cart' | langRoute" routerLinkActive="dexar-cart-active" class="dexar-cart-btn" (click)="closeMenu()"> <a [routerLink]="'/cart' | langRoute" routerLinkActive="dexar-cart-active" class="dexar-cart-btn" (click)="closeMenu()">
<span class="dexar-cart-icon">
<svg width="32" height="24" viewBox="0 0 48 32" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="32" height="24" viewBox="0 0 48 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0.5H36C42.3513 0.5 47.5 5.64873 47.5 12V20C47.5 26.3513 42.3513 31.5 36 31.5H12C5.64873 31.5 0.5 26.3513 0.5 20V12C0.5 5.64873 5.64873 0.5 12 0.5Z" fill="transparent" /> <path d="M12 0.5H36C42.3513 0.5 47.5 5.64873 47.5 12V20C47.5 26.3513 42.3513 31.5 36 31.5H12C5.64873 31.5 0.5 26.3513 0.5 20V12C0.5 5.64873 5.64873 0.5 12 0.5Z" fill="transparent" />
<path d="M12 0.5H36C42.3513 0.5 47.5 5.64873 47.5 12V20C47.5 26.3513 42.3513 31.5 36 31.5H12C5.64873 31.5 0.5 26.3513 0.5 20V12C0.5 5.64873 5.64873 0.5 12 0.5Z" stroke="#677B78" /> <path d="M12 0.5H36C42.3513 0.5 47.5 5.64873 47.5 12V20C47.5 26.3513 42.3513 31.5 36 31.5H12C5.64873 31.5 0.5 26.3513 0.5 20V12C0.5 5.64873 5.64873 0.5 12 0.5Z" stroke="#677B78" />
@@ -105,6 +116,10 @@
@if (cartItemCount() > 0) { @if (cartItemCount() > 0) {
<span class="dexar-cart-badge">{{ cartItemCount() }}</span> <span class="dexar-cart-badge">{{ cartItemCount() }}</span>
} }
</span>
@if (cartItemCount() > 0) {
<span class="dexar-cart-total">{{ formatCartTotal(cartTotal()) }}</span>
}
</a> </a>
<!-- Region Selector (desktop only) --> <!-- Region Selector (desktop only) -->
@@ -177,14 +192,13 @@
</svg> </svg>
</a> </a>
<!-- Region Selector in mobile menu --> <div class="dexar-mobile-controls">
<div class="dexar-mobile-lang"> <div class="dexar-mobile-lang">
<app-region-selector /> <app-region-selector />
</div> </div>
<!-- Language Selector in mobile menu -->
<div class="dexar-mobile-lang"> <div class="dexar-mobile-lang">
<app-language-selector /> <app-language-selector />
</div> </div>
</div> </div>
</div>
} }

View File

@@ -328,25 +328,41 @@
gap: 1.5rem; gap: 1.5rem;
} }
.novo-mobile-controls {
display: none;
}
.novo-cart { .novo-cart {
position: relative; display: flex;
flex-direction: column;
align-items: center;
gap: 0.2rem;
color: var(--text-primary); color: var(--text-primary);
text-decoration: none; text-decoration: none;
padding: 0.5rem; padding: 0.5rem;
border-radius: var(--radius-md); border-radius: var(--radius-md);
transition: all 0.3s; transition: all 0.3s;
svg {
display: block;
}
&:hover, &:hover,
&.novo-cart-active { &.novo-cart-active {
color: var(--primary-color); color: var(--primary-color);
background: var(--bg-secondary); background: var(--bg-secondary);
} }
}
.novo-cart-badge { .novo-cart-icon {
position: relative;
display: flex;
align-items: center;
justify-content: center;
line-height: 0;
svg {
display: block;
}
}
.novo-cart-badge {
position: absolute; position: absolute;
top: -4px; top: -4px;
right: -4px; right: -4px;
@@ -362,7 +378,13 @@
justify-content: center; justify-content: center;
padding: 0 5px; padding: 0 5px;
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
} }
.novo-cart-total {
font-size: 0.68rem;
font-weight: 700;
line-height: 1;
white-space: nowrap;
} }
.novo-menu-toggle { .novo-menu-toggle {
@@ -409,7 +431,8 @@
transition: max-height 0.3s; transition: max-height 0.3s;
&.novo-nav-open { &.novo-nav-open {
max-height: 300px; max-height: calc(100dvh - 65px);
overflow-y: auto;
} }
.novo-nav-links { .novo-nav-links {
@@ -435,6 +458,25 @@
} }
} }
.novo-right {
flex: 0 0 auto;
gap: 0.75rem;
}
.novo-right > app-region-selector,
.novo-right > app-language-selector {
display: none;
}
.novo-mobile-controls {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 1rem 2rem 1.25rem;
border-top: 1px solid var(--border-color);
}
.novo-menu-toggle { .novo-menu-toggle {
display: flex; display: flex;
} }
@@ -468,7 +510,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 32px; gap: 32px;
height: 48px; min-height: 48px;
} }
.dexar-logo { .dexar-logo {
@@ -591,15 +633,15 @@
} }
.dexar-cart-btn { .dexar-cart-btn {
position: relative;
width: 36px;
height: 28px;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-width: 36px;
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
gap: 2px;
svg { svg {
width: 36px; width: 36px;
@@ -615,6 +657,15 @@
} }
} }
.dexar-cart-icon {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 28px;
}
.dexar-cart-badge { .dexar-cart-badge {
position: absolute; position: absolute;
top: -8px; top: -8px;
@@ -634,6 +685,15 @@
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
} }
.dexar-cart-total {
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-weight: 700;
font-size: 10px;
line-height: 1;
color: #1e3c38;
white-space: nowrap;
}
.dexar-lang-selector { .dexar-lang-selector {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -714,10 +774,20 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-top: 16px; }
.dexar-mobile-controls {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
width: 100%;
margin-top: 4px;
} }
.dexar-menu-toggle { .dexar-menu-toggle {
position: relative;
z-index: 1001;
display: none; display: none;
flex-direction: column; flex-direction: column;
gap: 5px; gap: 5px;
@@ -841,7 +911,7 @@
// Hide desktop language selector // Hide desktop language selector
.dexar-lang-desktop { .dexar-lang-desktop {
display: none; display: none !important;
} }
.dexar-search-wrapper { .dexar-search-wrapper {
@@ -903,9 +973,10 @@
transition: max-height 0.35s ease, opacity 0.25s ease, padding 0.35s ease; transition: max-height 0.35s ease, opacity 0.25s ease, padding 0.35s ease;
&.dexar-mobile-menu-open { &.dexar-mobile-menu-open {
max-height: 700px; max-height: calc(100dvh - 84px);
padding: 28px 20px 32px; padding: 28px 20px 32px;
opacity: 1; opacity: 1;
overflow-y: auto;
} }
} }
@@ -914,8 +985,8 @@
max-width: 100%; max-width: 100%;
} }
.dexar-mobile-lang { .dexar-mobile-controls {
margin-top: 16px; margin-top: 4px;
} }
} }

View File

@@ -1,6 +1,7 @@
import { Component, ChangeDetectionStrategy, Renderer2, inject, DOCUMENT } from '@angular/core'; import { Component, ChangeDetectionStrategy, Renderer2, inject, DOCUMENT } from '@angular/core';
import { Router, RouterLink, RouterLinkActive } from '@angular/router'; import { Router, RouterLink, RouterLinkActive } from '@angular/router';
import { CartService, LanguageService } from '../../services'; import { CartService } from '../../services/cart.service';
import { LanguageService } from '../../services/language.service';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { LogoComponent } from '../logo/logo.component'; import { LogoComponent } from '../logo/logo.component';
import { LanguageSelectorComponent } from '../language-selector/language-selector.component'; import { LanguageSelectorComponent } from '../language-selector/language-selector.component';
@@ -17,6 +18,7 @@ import { TranslatePipe } from '../../i18n/translate.pipe';
}) })
export class HeaderComponent { export class HeaderComponent {
cartItemCount; cartItemCount;
cartTotal;
menuOpen = false; menuOpen = false;
brandName = environment.brandFullName; brandName = environment.brandFullName;
logo = environment.logo; logo = environment.logo;
@@ -28,6 +30,11 @@ export class HeaderComponent {
constructor(private cartService: CartService, private router: Router) { constructor(private cartService: CartService, private router: Router) {
this.cartItemCount = this.cartService.itemCount; this.cartItemCount = this.cartService.itemCount;
this.cartTotal = this.cartService.totalPrice;
}
get homeUrl(): string {
return `/${this.langService.currentLanguage()}`;
} }
toggleMenu(): void { toggleMenu(): void {
@@ -44,6 +51,23 @@ export class HeaderComponent {
this.renderer.removeClass(this.document.body, 'dexar-menu-open'); this.renderer.removeClass(this.document.body, 'dexar-menu-open');
} }
navigateHome(event?: Event): void {
event?.preventDefault();
this.closeMenu();
const homeUrl = this.homeUrl;
const currentUrl = this.router.url.split('?')[0].split('#')[0];
if (currentUrl === homeUrl || currentUrl === `${homeUrl}/`) {
this.document.defaultView?.scrollTo({ top: 0, behavior: 'smooth' });
return;
}
this.router.navigateByUrl(homeUrl).then(() => {
this.document.defaultView?.scrollTo({ top: 0, behavior: 'auto' });
});
}
navigateToSearch(): void { navigateToSearch(): void {
const lang = this.langService.currentLanguage(); const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}/search`]); this.router.navigate([`/${lang}/search`]);
@@ -58,4 +82,20 @@ export class HeaderComponent {
}, 100); }, 100);
}); });
} }
formatCartTotal(total: number): string {
const locale = this.langService.currentLanguage() === 'en'
? 'en-US'
: this.langService.currentLanguage() === 'hy'
? 'hy-AM'
: 'ru-RU';
const fractionDigits = Number.isInteger(total) ? 0 : 2;
const amount = new Intl.NumberFormat(locale, {
minimumFractionDigits: fractionDigits,
maximumFractionDigits: 2,
}).format(total);
const currencySymbol = this.langService.getCurrentCurrency()?.symbol ?? this.langService.currentCurrency();
return `${amount} ${currencySymbol}`;
}
} }

View File

@@ -262,13 +262,17 @@
// Inside mobile menu: use inline dropdown instead of absolute // Inside mobile menu: use inline dropdown instead of absolute
// to avoid being clipped by overflow: hidden on the menu panel // to avoid being clipped by overflow: hidden on the menu panel
:host-context(.dexar-mobile-menu), :host-context(.dexar-mobile-menu),
:host-context(.dexar-mobile-lang) { :host-context(.dexar-mobile-lang),
:host-context(.novo-mobile-controls) {
.language-selector { .language-selector {
width: auto; width: auto;
height: auto; height: auto;
flex-wrap: wrap;
justify-content: center;
} }
.language-dropdown { .language-dropdown,
.currency-dropdown {
position: static; position: static;
opacity: 1; opacity: 1;
visibility: hidden; visibility: hidden;
@@ -289,7 +293,8 @@
} }
} }
.language-option { .language-option,
.currency-option {
color: #1e3c38; color: #1e3c38;
&:hover:not(.disabled) { &:hover:not(.disabled) {

View File

@@ -3,12 +3,15 @@ import { environment } from '../../../environments/environment';
@Component({ @Component({
selector: 'app-logo', selector: 'app-logo',
template: `<img [src]="logoPath" [alt]="brandName + ' logo'" class="logo-img" fetchpriority="high" />`, template: `<img [src]="logoPath" [alt]="brandName + ' logo'" class="logo-img" fetchpriority="high" draggable="false" />`,
styles: [` styles: [`
.logo-img { .logo-img {
display: block;
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
pointer-events: none;
user-select: none;
} }
`], `],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -29,6 +29,12 @@
{{ 'auth.loginWithTelegram' | translate }} {{ 'auth.loginWithTelegram' | translate }}
</button> </button>
<!-- @if (loginUrl()) {
<a class="bot-link" [href]="loginUrl()" target="_blank" rel="noopener noreferrer">
{{ loginUrl() }}
</a>
} -->
<div class="qr-section"> <div class="qr-section">
<p class="qr-hint">{{ 'auth.orScanQr' | translate }}</p> <p class="qr-hint">{{ 'auth.orScanQr' | translate }}</p>
@@ -57,12 +63,12 @@
</div> </div>
} }
@case ('error') { @case ('error') {
<div class="qr-container"> <div class="qr-container qr-error" (click)="refreshQr()">
<img [src]="'https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=' + encodedQrUrl()" <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
alt="QR Code" <path d="M1 4v6h6M23 20v-6h-6"/>
width="180" <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"/>
height="180" </svg>
loading="lazy" /> <span>{{ 'auth.qrError' | translate }}</span>
</div> </div>
} }
} }

View File

@@ -102,6 +102,20 @@ h2 {
} }
} }
.bot-link {
display: block;
margin-top: 10px;
color: var(--accent-color, #497671);
font-size: 12px;
line-height: 1.35;
overflow-wrap: anywhere;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.qr-section { .qr-section {
margin-top: 20px; margin-top: 20px;
@@ -158,6 +172,26 @@ h2 {
font-size: 13px; font-size: 13px;
} }
} }
&.qr-error {
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,8 +1,6 @@
import { Component, ChangeDetectionStrategy, inject, signal, computed, effect, 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',
@@ -13,92 +11,104 @@ import { getDiscountedPrice } from '../../utils/item.utils';
}) })
export class TelegramLoginComponent implements 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(''); webSessionID = signal('');
qrStatus = signal<'loading' | 'ready' | 'expired' | 'error'>('loading'); qrStatus = signal<'loading' | 'ready' | 'expired' | 'error'>('loading');
encodedQrUrl = computed(() => encodeURIComponent(this.loginUrl())); encodedQrUrl = computed(() => encodeURIComponent(this.loginUrl()));
awaitingTelegramReturn = signal(false);
private readonly pollIntervalMs = 5000;
private pollTimer?: ReturnType<typeof setInterval>; private pollTimer?: ReturnType<typeof setInterval>;
private readonly handleVisibilityChange = () => {
if (typeof document !== 'undefined' && document.visibilityState === 'visible') {
this.checkLoginAfterReturn();
}
};
private readonly handleWindowFocus = () => {
this.checkLoginAfterReturn();
};
private readonly handlePageShow = () => {
this.checkLoginAfterReturn();
};
constructor() { constructor() {
effect(() => { effect(() => {
if (this.showDialog()) { if (this.showDialog()) {
this.initQrLogin(); this.initQrLogin();
} else { } else {
this.awaitingTelegramReturn.set(false);
this.stopPolling(); this.stopPolling();
} }
}); });
if (typeof window !== 'undefined') {
document.addEventListener('visibilitychange', this.handleVisibilityChange);
window.addEventListener('focus', this.handleWindowFocus);
window.addEventListener('pageshow', this.handlePageShow);
}
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.awaitingTelegramReturn.set(false);
this.stopPolling(); this.stopPolling();
if (typeof window !== 'undefined') {
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
window.removeEventListener('focus', this.handleWindowFocus);
window.removeEventListener('pageshow', this.handlePageShow);
}
} }
close(): void { close(): void {
this.awaitingTelegramReturn.set(false);
this.authService.hideLogin(); this.authService.hideLogin();
this.stopPolling(); this.stopPolling();
} }
openTelegramLogin(): void { openTelegramLogin(): void {
window.open(this.loginUrl(), '_blank'); const webSessionID = this.webSessionID();
if (!webSessionID || typeof window === 'undefined') return;
if (!this.pollTimer) { if (!this.pollTimer) {
if (this.qrToken()) { this.startPolling(webSessionID);
this.startPolling(this.qrToken());
} else {
this.startSessionPolling();
}
} }
this.awaitingTelegramReturn.set(true);
window.location.href = this.authService.getTelegramAppLoginUrl(webSessionID);
} }
refreshQr(): void { refreshQr(): void {
this.awaitingTelegramReturn.set(false);
this.stopPolling(); this.stopPolling();
this.initQrLogin(); this.initQrLogin();
} }
private initQrLogin(): void { private initQrLogin(): void {
this.awaitingTelegramReturn.set(false);
this.qrStatus.set('loading'); this.qrStatus.set('loading');
this.authService.createQrToken().subscribe({ this.loginUrl.set('');
this.webSessionID.set('');
this.authService.createWebSession().subscribe({
next: (res) => { next: (res) => {
this.loginUrl.set(res.url); this.loginUrl.set(res.url);
this.qrToken.set(res.token); this.webSessionID.set(res.webSessionID);
this.qrStatus.set('ready'); this.qrStatus.set('ready');
this.startPolling(res.token); this.startPolling(res.webSessionID);
}, },
error: () => { error: () => {
this.loginUrl.set(this.authService.getTelegramLoginUrl());
this.qrStatus.set('error'); this.qrStatus.set('error');
this.startSessionPolling();
} }
}); });
} }
private startSessionPolling(): void { private startPolling(webSessionID: string): void {
this.stopPolling(); this.stopPolling();
let checks = 0; if (!webSessionID) return;
this.pollTimer = setInterval(() => {
checks++;
if (checks > 100) {
this.stopPolling();
this.qrStatus.set('expired');
return;
}
this.authService.checkSessionOnce().subscribe(session => {
if (session && session.active) {
this.stopPolling();
this.syncCartAndComplete(session.sessionId);
}
});
}, 3000);
}
private startPolling(token: string): void {
this.stopPolling();
if (!token) return;
let checks = 0; let checks = 0;
this.pollTimer = setInterval(() => { this.pollTimer = setInterval(() => {
@@ -109,44 +119,19 @@ export class TelegramLoginComponent implements OnDestroy {
return; return;
} }
this.authService.pollQrToken(token).subscribe({ this.authService.checkSessionOnce(webSessionID).subscribe({
next: (res) => { next: (session) => {
switch (res.status) { if (session?.active) {
case 'confirmed': this.awaitingTelegramReturn.set(false);
this.stopPolling(); this.stopPolling();
if (res.session) {
this.syncCartAndComplete(res.session.sessionId);
} else {
this.authService.onTelegramLoginComplete(); this.authService.onTelegramLoginComplete();
} }
break;
case 'expired':
this.stopPolling();
this.qrStatus.set('expired');
break;
}
}, },
error: () => { error: () => {
// Network error — keep polling // Network error — keep polling
} }
}); });
}, 3000); }, this.pollIntervalMs);
}
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 {
@@ -155,4 +140,28 @@ export class TelegramLoginComponent implements OnDestroy {
this.pollTimer = undefined; this.pollTimer = undefined;
} }
} }
private checkLoginAfterReturn(): void {
if (!this.showDialog() || !this.awaitingTelegramReturn()) {
return;
}
const webSessionID = this.webSessionID();
if (!webSessionID) {
this.awaitingTelegramReturn.set(false);
return;
}
if (!this.pollTimer) {
this.startPolling(webSessionID);
}
this.authService.checkSessionOnce(webSessionID).subscribe(session => {
if (session?.active) {
this.awaitingTelegramReturn.set(false);
this.stopPolling();
this.authService.onTelegramLoginComplete();
}
});
}
} }

View File

@@ -2,7 +2,6 @@
export const PAYMENT_POLL_INTERVAL_MS = 5000; export const PAYMENT_POLL_INTERVAL_MS = 5000;
export const PAYMENT_MAX_CHECKS = 36; export const PAYMENT_MAX_CHECKS = 36;
export const PAYMENT_TIMEOUT_CLOSE_MS = 3000; export const PAYMENT_TIMEOUT_CLOSE_MS = 3000;
export const PAYMENT_ERROR_CLOSE_MS = 4000;
export const LINK_COPIED_DURATION_MS = 2000; export const LINK_COPIED_DURATION_MS = 2000;
// Infinite scroll // Infinite scroll
@@ -12,7 +11,6 @@ export const ITEMS_PER_PAGE = 50;
// Search // Search
export const SEARCH_DEBOUNCE_MS = 300; export const SEARCH_DEBOUNCE_MS = 300;
export const SEARCH_MIN_LENGTH = 3;
// Cache // Cache
export const CACHE_DURATION_MS = 5 * 60 * 1000; export const CACHE_DURATION_MS = 5 * 60 * 1000;

View File

@@ -64,6 +64,12 @@ export const en: Translations = {
total: 'Total', total: 'Total',
items: 'Products', items: 'Products',
deliveryLabel: 'Delivery', deliveryLabel: 'Delivery',
deliveryMethod: 'Delivery option',
selectDelivery: 'Select delivery option',
deliveryPlace: 'Place',
deliveryTime: 'Delivery time',
digitalDelivery: 'Digital delivery',
deliveryRequired: 'Select delivery for every shippable item before checkout.',
toPay: 'To pay', toPay: 'To pay',
agreeWith: 'I agree with the', agreeWith: 'I agree with the',
publicOffer: 'public offer', publicOffer: 'public offer',
@@ -85,6 +91,9 @@ export const en: Translations = {
paymentSuccessDesc: 'Enter your contact details and we will send your purchase within a few minutes', paymentSuccessDesc: 'Enter your contact details and we will send your purchase within a few minutes',
sending: 'Sending...', sending: 'Sending...',
send: 'Send', send: 'Send',
paymentError: 'Unable to create payment',
paymentErrorDesc: 'We could not prepare the payment QR code right now. Please try again in a moment.',
retryPayment: 'Try again',
paymentTimeout: 'Payment timed out', paymentTimeout: 'Payment timed out',
paymentTimeoutDesc: 'We did not receive payment confirmation within 3 minutes.', paymentTimeoutDesc: 'We did not receive payment confirmation within 3 minutes.',
autoClose: 'Window will close automatically...', autoClose: 'Window will close automatically...',
@@ -206,5 +215,6 @@ export const en: Translations = {
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', qrExpired: 'QR code expired. Click to refresh',
qrError: 'Could not create login session. Click to retry',
}, },
}; };

View File

@@ -64,6 +64,12 @@ export const hy: Translations = {
total: 'Ընդամենը', total: 'Ընդամենը',
items: 'Ապրանքներ', items: 'Ապրանքներ',
deliveryLabel: 'Առաքում', deliveryLabel: 'Առաքում',
deliveryMethod: 'Առաքման տարբերակ',
selectDelivery: 'Ընտրեք առաքման տարբերակը',
deliveryPlace: 'Վայր',
deliveryTime: 'Առաքման ժամկետ',
digitalDelivery: 'Թվային առաքում',
deliveryRequired: 'Մինչ պատվերը ձևակերպելը ընտրեք առաքումը բոլոր առաքվող ապրանքների համար։',
toPay: 'Վճարման ենթակա', toPay: 'Վճարման ենթակա',
agreeWith: 'Ես համաձայն եմ', agreeWith: 'Ես համաձայն եմ',
publicOffer: 'հանրային օֆերտայի', publicOffer: 'հանրային օֆերտայի',
@@ -85,6 +91,9 @@ export const hy: Translations = {
paymentSuccessDesc: 'Մուտքագրեք ձեր տվյալները, և մենք կուղարկենք գնումը մի քանի րոպեի ընթացքում', paymentSuccessDesc: 'Մուտքագրեք ձեր տվյալները, և մենք կուղարկենք գնումը մի քանի րոպեի ընթացքում',
sending: 'Ուղարկվում է...', sending: 'Ուղարկվում է...',
send: 'Ուղարկել', send: 'Ուղարկել',
paymentError: 'Չհաջողվեց ստեղծել վճարումը',
paymentErrorDesc: 'Այս պահին չհաջողվեց պատրաստել վճարման QR կոդը։ Խնդրում ենք մի փոքր հետո կրկին փորձել։',
retryPayment: 'Փորձել կրկին',
paymentTimeout: 'Ժամանակը սպառվեց', paymentTimeout: 'Ժամանակը սպառվեց',
paymentTimeoutDesc: 'Մենք չստացանք վճարման հաստատում 3 րոպեի ընթացքում։', paymentTimeoutDesc: 'Մենք չստացանք վճարման հաստատում 3 րոպեի ընթացքում։',
autoClose: 'Պատուհանը կփակվի ավտոմատ...', autoClose: 'Պատուհանը կփակվի ավտոմատ...',
@@ -206,5 +215,6 @@ export const hy: Translations = {
orScanQr: 'Կամ սքանավորեք QR կոդը', orScanQr: 'Կամ սքանավորեք QR կոդը',
loginNote: 'Մուտքից հետո դուք կվերաուղղվեք', loginNote: 'Մուտքից հետո դուք կվերաուղղվեք',
qrExpired: 'QR կոդը հնացել է։ Սեղմեք՝ թարմացնելու համար', qrExpired: 'QR կոդը հնացել է։ Սեղմեք՝ թարմացնելու համար',
qrError: 'Չհաջողվեց ստեղծել մուտքի սեսիա։ Սեղմեք՝ կրկնելու համար',
}, },
}; };

View File

@@ -64,6 +64,12 @@ export const ru: Translations = {
total: 'Итого', total: 'Итого',
items: 'Товары', items: 'Товары',
deliveryLabel: 'Доставка', deliveryLabel: 'Доставка',
deliveryMethod: 'Способ доставки',
selectDelivery: 'Выберите способ доставки',
deliveryPlace: 'Место',
deliveryTime: 'Срок доставки',
digitalDelivery: 'Цифровая доставка',
deliveryRequired: 'Выберите доставку для всех товаров с доставкой перед оформлением заказа.',
toPay: 'К оплате', toPay: 'К оплате',
agreeWith: 'Я согласен с', agreeWith: 'Я согласен с',
publicOffer: 'публичной офертой', publicOffer: 'публичной офертой',
@@ -85,6 +91,9 @@ export const ru: Translations = {
paymentSuccessDesc: 'Введите ваши контактные данные, и мы отправим вам покупку в течение нескольких минут', paymentSuccessDesc: 'Введите ваши контактные данные, и мы отправим вам покупку в течение нескольких минут',
sending: 'Отправка...', sending: 'Отправка...',
send: 'Отправить', send: 'Отправить',
paymentError: 'Не удалось создать платеж',
paymentErrorDesc: 'Сейчас не получилось подготовить QR-код для оплаты. Попробуйте еще раз через минуту.',
retryPayment: 'Попробовать снова',
paymentTimeout: 'Время ожидания истекло', paymentTimeout: 'Время ожидания истекло',
paymentTimeoutDesc: 'Мы не получили подтверждение оплаты в течение 3 минут.', paymentTimeoutDesc: 'Мы не получили подтверждение оплаты в течение 3 минут.',
autoClose: 'Окно закроется автоматически...', autoClose: 'Окно закроется автоматически...',
@@ -206,5 +215,6 @@ export const ru: Translations = {
orScanQr: 'Или отсканируйте QR-код', orScanQr: 'Или отсканируйте QR-код',
loginNote: 'После входа вы будете перенаправлены обратно', loginNote: 'После входа вы будете перенаправлены обратно',
qrExpired: 'QR-код устарел. Нажмите, чтобы обновить', qrExpired: 'QR-код устарел. Нажмите, чтобы обновить',
qrError: 'Не удалось создать сессию входа. Нажмите, чтобы повторить',
}, },
}; };

View File

@@ -62,6 +62,12 @@ export interface Translations {
total: string; total: string;
items: string; items: string;
deliveryLabel: string; deliveryLabel: string;
deliveryMethod: string;
selectDelivery: string;
deliveryPlace: string;
deliveryTime: string;
digitalDelivery: string;
deliveryRequired: string;
toPay: string; toPay: string;
agreeWith: string; agreeWith: string;
publicOffer: string; publicOffer: string;
@@ -83,6 +89,9 @@ export interface Translations {
paymentSuccessDesc: string; paymentSuccessDesc: string;
sending: string; sending: string;
send: string; send: string;
paymentError: string;
paymentErrorDesc: string;
retryPayment: string;
paymentTimeout: string; paymentTimeout: string;
paymentTimeoutDesc: string; paymentTimeoutDesc: string;
autoClose: string; autoClose: string;
@@ -204,5 +213,6 @@ export interface Translations {
orScanQr: string; orScanQr: string;
loginNote: string; loginNote: string;
qrExpired: string; qrExpired: string;
qrError: string;
}; };
} }

View File

@@ -50,11 +50,6 @@ export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
); );
}; };
/** Clear all cached responses */
export function clearCache(): void {
cache.clear();
}
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()) {

View File

@@ -0,0 +1,3 @@
import { HttpInterceptorFn } from '@angular/common/http';
export const mockDataInterceptor: HttpInterceptorFn = (req, next) => next(req);

View File

@@ -615,6 +615,47 @@ const MOCK_ITEMS: any[] = [
], ],
questions: [] questions: []
}, },
{
id: 'bolset-cream',
categoryID: 2010,
itemID: 496218563,
name: 'Женские кеды BOLSET (кремовый)',
description: 'Сезон: Весна - лето (SS)\nМатериал : телячья кожа\nПроизводство: Турция\n\nРазмерный ряд: 36 - 40 (EU)',
discount: 0,
rating: 0,
visible: true,
priority: 0,
tags: null,
badges: null,
basecurrency: '',
itemDetails: [
{ color: '0xFFFDD0', size: '37', price: 6899.94, currency: 'RUB', remaining: 100 },
{ color: '0xFFFDD0', size: '38', price: 6900, currency: 'RUB', remaining: 100 },
{ color: '0xFFFDD0', size: '39', price: 6900, currency: 'RUB', remaining: 100 },
{ color: '0xFFFDD0', size: '40', price: 6900, currency: 'RUB', remaining: 100 },
{ color: '0xFFFDD0', size: '41', price: 6900, currency: 'RUB', remaining: 100 },
{ color: '0xFFFDD0', size: '37', price: 94.09, currency: 'USD', remaining: 100 },
{ color: '0xFFFDD0', size: '37', price: 82.04, currency: 'EUR', remaining: 100 },
{ color: '0xFFFDD0', size: '37', price: 34900.47, currency: 'AMD', remaining: 100 }
],
names: [
{ language: 'ru', valuue: '' }
],
descriptions: [
{ language: 'ru', value: 'Сезон: Весна - лето (SS)\nМатериал : телячья кожа\nПроизводство: Турция\n\nРазмерный ряд: 36 - 40 (EU)' }
],
attributes: [],
photos: [
{ type: 'photo', url: './images/496218563.webp' }
],
questions: [],
visits: 0,
callbacks: [],
partnerID: '',
delivery: [
{ deliveryPrice: 0, deliveryPlace: 'по всему миру', deliveryTime: '30 минут' }
]
},
{ {
id: 'lamp-smart', id: 'lamp-smart',
itemID: 301, itemID: 301,
@@ -672,7 +713,7 @@ function respond<T>(body: T, delayMs = 150) {
} }
// ─── Mock Auth State ─── // ─── Mock Auth State ───
let mockQrPollCount = 0; const mockWebSessionChecks = new Map<string, number>();
// ─── The Interceptor ─── // ─── The Interceptor ───
@@ -688,38 +729,39 @@ export const mockDataInterceptor: HttpInterceptorFn = (req, next) => {
return respond({ message: 'pong (mock)' }); return respond({ message: 'pong (mock)' });
} }
// ── GET /auth/session // ── POST /users/sessions
if (url.includes('/auth/session') && req.method === 'GET') { if (url.endsWith('/users/sessions') && req.method === 'POST') {
return respond({ active: false }, 100); const body = req.body as { webSessionID?: string } | null;
const webSessionID = body?.webSessionID || req.headers.get('WebSessionID') || 'mock-web-session';
mockWebSessionChecks.set(webSessionID, 0);
return respond({ webSessionID, status: false }, 200);
} }
// ── POST /auth/qr/create // ── GET /users/sessions/:webSessionID
if (url.includes('/auth/qr/create') && req.method === 'POST') { const userSessionMatch = url.match(/\/users\/sessions\/([^/?]+)$/);
const token = 'mock-qr-token-' + Date.now(); if (userSessionMatch && req.method === 'GET') {
const botUsername = (environment as Record<string, unknown>)['telegramBot'] as string || 'DexarSupport_bot'; const webSessionID = decodeURIComponent(userSessionMatch[1]);
mockQrPollCount = 0; const checks = (mockWebSessionChecks.get(webSessionID) ?? 0) + 1;
return respond({ mockWebSessionChecks.set(webSessionID, checks);
token,
url: `https://t.me/${botUsername}?start=qr_${token}`
}, 200);
}
// ── GET /auth/qr/poll if (checks >= 3) {
if (url.includes('/auth/qr/poll') && req.method === 'GET') {
mockQrPollCount++;
// Simulate confirmed after 3 polls (~9 seconds)
if (mockQrPollCount >= 3) {
return respond({ return respond({
status: 'confirmed', webSessionID,
session: { status: true,
sessionId: 'mock-session-' + Date.now(), telegramUserID: 123456,
active: true, username: 'telegram_user',
displayName: 'Telegram User', displayName: 'Telegram User',
expiresAt: new Date(Date.now() + 3600000).toISOString() expiresAt: new Date(Date.now() + 3600000).toISOString()
}
}, 200); }, 200);
} }
return respond({ status: 'pending' }, 200);
return respond({ webSessionID, status: false }, 200);
}
// ── DELETE /users/sessions/:webSessionID
if (userSessionMatch && req.method === 'DELETE') {
mockWebSessionChecks.delete(decodeURIComponent(userSessionMatch[1]));
return respond({ status: true }, 100);
} }
// ── GET /category (all categories flat list) // ── GET /category (all categories flat list)
@@ -779,8 +821,8 @@ export const mockDataInterceptor: HttpInterceptorFn = (req, next) => {
return respond([]); return respond([]);
} }
// ── POST /websession/:id (add to cart) // ── POST /usersession/:id or /websession/:id (sync cart)
if (url.match(/\/websession\/[^/]+$/) && req.method === 'POST') { if (url.match(/\/(?:user|web)session\/[^/]+$/) && req.method === 'POST') {
return respond({ return respond({
sessionId: 'mock-session', sessionId: 'mock-session',
Status: true, Status: true,
@@ -794,11 +836,41 @@ export const mockDataInterceptor: HttpInterceptorFn = (req, next) => {
qrId: 'mock-qr-' + Date.now(), qrId: 'mock-qr-' + Date.now(),
qrStatus: 'NEW', qrStatus: 'NEW',
qrExpirationDate: new Date(Date.now() + 180000).toISOString(), qrExpirationDate: new Date(Date.now() + 180000).toISOString(),
Payload: 'https://example.com/pay/mock', payload: 'https://example.com/pay/mock',
qrUrl: 'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=mock-payment' qrUrl: 'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=mock-payment'
}, 300); }, 300);
} }
// ── POST /qr (create payment QR directly)
if (url.endsWith('/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 /cart (legacy create payment QR)
if (url.endsWith('/cart') && req.method === 'POST') {
const body = req.body;
const looksLikePaymentRequest = !!body
&& typeof body === 'object'
&& !Array.isArray(body)
&& 'amount' in body
&& 'items' in body;
if (looksLikePaymentRequest) {
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) // ── POST /items/:id/callback (review)
if (url.match(/\/items\/\d+\/callback$/) && req.method === 'POST') { if (url.match(/\/items\/\d+\/callback$/) && req.method === 'POST') {
return respond({ message: 'Review submitted (mock)' }, 200); return respond({ message: 'Review submitted (mock)' }, 200);
@@ -812,7 +884,7 @@ export const mockDataInterceptor: HttpInterceptorFn = (req, next) => {
// ── GET /websession/:id/:qrId (check QR payment status) // ── GET /websession/:id/:qrId (check QR payment status)
if (url.match(/\/websession\/[^/]+\/[^/]+$/) && !url.match(/\/websession\/[^/]+\/qr$/) && req.method === 'GET') { if (url.match(/\/websession\/[^/]+\/[^/]+$/) && !url.match(/\/websession\/[^/]+\/qr$/) && req.method === 'GET') {
return respond({ return respond({
paymentStatus: 'SUCCESS', paymentStatus: 'COMPLETED',
code: 'SUCCESS', code: 'SUCCESS',
amount: 0, amount: 0,
currency: 'RUB', currency: 'RUB',
@@ -827,6 +899,25 @@ export const mockDataInterceptor: HttpInterceptorFn = (req, next) => {
}, 500); }, 500);
} }
// ── GET /qr/payment/:qrId (payment status)
if (url.match(/\/qr\/payment\/[^/]+$/) && req.method === 'GET') {
return respond({
paymentStatus: 'COMPLETED',
code: 'SUCCESS',
amount: 0,
currency: 'RUB',
qrId: 'mock',
transactionId: 999,
transactionDate: new Date().toISOString(),
additionalInfo: '',
paymentPurpose: '',
createDate: new Date().toISOString(),
order: 'mock-order',
qrExpirationDate: new Date().toISOString(),
phoneNumber: '+70000000000'
}, 500);
}
// Fallback — pass through // Fallback — pass through
return next(req); return next(req);
}; };

View File

@@ -1,25 +1,15 @@
export interface AuthSession { export interface AuthSession {
sessionId: string; sessionId: string;
telegramUserId: number; userId: number | null;
username: string | null; username: string | null;
displayName: string; displayName: string;
active: boolean; active: boolean;
expiresAt: string; expires: string;
} }
export interface TelegramAuthData { export interface WebSessionStart {
id: number; webSessionID: string;
first_name: string; url: string;
last_name?: string;
username?: string;
photo_url?: string;
auth_date: number;
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

@@ -33,6 +33,6 @@ export interface Subcategory {
subcategories?: Subcategory[]; subcategories?: Subcategory[];
} }
export interface CategoryTranslation { interface CategoryTranslation {
name?: string; name?: string;
} }

View File

@@ -1,4 +1,4 @@
export interface Photo { interface Photo {
photo?: string; photo?: string;
video?: string; video?: string;
url: string; url: string;
@@ -10,7 +10,7 @@ export interface DescriptionField {
value: string; value: string;
} }
export interface Comment { interface Comment {
id?: string; id?: string;
text: string; text: string;
author?: string; author?: string;
@@ -18,13 +18,13 @@ export interface Comment {
createdAt?: string; createdAt?: string;
} }
export interface ItemTranslation { interface ItemTranslation {
name?: string; name?: string;
simpleDescription?: string; simpleDescription?: string;
description?: DescriptionField[]; description?: DescriptionField[];
} }
export interface Review { interface Review {
rating?: number; rating?: number;
content?: string; content?: string;
userID?: string; userID?: string;
@@ -32,10 +32,7 @@ export interface Review {
timestamp?: string; timestamp?: string;
} }
/** @deprecated Use {@link Review} instead */ interface Question {
export type Callback = Review;
export interface Question {
question: string; question: string;
answer: string; answer: string;
upvotes: number; upvotes: number;
@@ -51,23 +48,32 @@ export interface ItemName {
} }
/** Localized description entry from backend */ /** Localized description entry from backend */
export interface ItemDescription { interface ItemDescription {
language: string; language: string;
value: string; value: string;
} }
/** Key-value attribute pair */ /** Key-value attribute pair */
export interface ItemAttribute { interface ItemAttribute {
key: string; key: string;
value: string; value: string;
} }
export interface DeliveryOption {
deliveryPrice: number;
deliveryPlace: string;
deliveryTime: string;
}
type DeliveryMode = 'selectable' | 'digital';
/** Item variant detail (price, size, colour per variant) */ /** Item variant detail (price, size, colour per variant) */
export interface ItemDetail { export interface ItemDetail {
color?: string; color?: string;
colour?: string; colour?: string;
size?: string; size?: string;
price: number; price: number;
deliveryPrice?: number;
currency: string; currency: string;
remaining: number; remaining: number;
} }
@@ -80,6 +86,7 @@ export interface Item {
description: string; description: string;
currency: string; currency: string;
price: number; price: number;
deliveryPrice?: number;
discount: number; discount: number;
remainings?: string; remainings?: string;
rating: number; rating: number;
@@ -90,6 +97,9 @@ export interface Item {
// Backend API fields // Backend API fields
colour?: string; colour?: string;
size?: string; size?: string;
deliveryOptions?: DeliveryOption[];
deliveryMode?: DeliveryMode;
deliverySelectionRequired?: boolean;
language?: string; language?: string;
names?: ItemName[]; names?: ItemName[];
descriptions?: ItemDescription[]; descriptions?: ItemDescription[];
@@ -113,4 +123,5 @@ export interface Item {
export interface CartItem extends Item { export interface CartItem extends Item {
quantity: number; quantity: number;
selectedDelivery?: DeliveryOption | null;
} }

View File

@@ -94,6 +94,13 @@
</button> </button>
</div> </div>
</div> </div>
@if (item.deliveryMode === 'digital' || item.deliveryOptions?.length) {
<app-delivery-selector
[item]="item"
(selectedDeliveryChange)="selectDelivery(item.itemID, $event)"
/>
}
</div> </div>
</div> </div>
@@ -116,14 +123,20 @@
<span class="value">{{ totalPrice() | number:'1.2-2' }} {{ currentCurrency }}</span> <span class="value">{{ totalPrice() | number:'1.2-2' }} {{ currentCurrency }}</span>
</div> </div>
@if (hasDeliveryPrice()) {
<div class="summary-row delivery"> <div class="summary-row delivery">
<span>{{ 'cart.deliveryLabel' | translate }}</span> <span>{{ 'cart.deliveryLabel' | translate }}</span>
<span>0 {{ currentCurrency }}</span> <span class="value">{{ totalDeliveryPrice() | number:'1.2-2' }} {{ currentCurrency }}</span>
</div> </div>
}
@if (!allRequiredDeliveriesSelected()) {
<p class="delivery-warning">{{ 'cart.deliveryRequired' | translate }}</p>
}
<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' }} {{ currentCurrency }}</span> <span class="total-price">{{ totalWithDelivery() | number:'1.2-2' }} {{ currentCurrency }}</span>
</div> </div>
<div class="terms-agreement"> <div class="terms-agreement">
@@ -147,8 +160,8 @@
<button <button
class="checkout-btn" class="checkout-btn"
(click)="checkout()" (click)="checkout()"
[class.disabled]="!termsAccepted" [class.disabled]="isCheckoutDisabled"
[disabled]="!termsAccepted" [disabled]="isCheckoutDisabled"
> >
{{ 'cart.checkout' | translate }} {{ 'cart.checkout' | translate }}
</button> </button>
@@ -169,17 +182,6 @@
</svg> </svg>
{{ 'cart.loginWithTelegram' | translate }} {{ 'cart.loginWithTelegram' | translate }}
</button> </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>
@@ -218,7 +220,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' }} {{ currentCurrency }}</span> <span class="amount">{{ totalWithDelivery() | number:'1.2-2' }} {{ currentCurrency }}</span>
</div> </div>
<div class="waiting-indicator"> <div class="waiting-indicator">
@@ -238,13 +240,27 @@
</div> </div>
} }
@if (paymentStatus() === 'error') {
<div class="payment-status-screen error">
<div class="error-icon">!</div>
<h2>{{ 'cart.paymentError' | translate }}</h2>
<p>{{ 'cart.paymentErrorDesc' | translate }}</p>
<div class="payment-error-actions">
<button class="retry-payment-btn" (click)="retryPayment()">
{{ 'cart.retryPayment' | translate }}
</button>
</div>
</div>
}
@if (paymentStatus() === 'success') { @if (paymentStatus() === 'success') {
<div class="payment-status-screen success"> <div class="payment-status-screen success">
<div class="success-icon"></div> <div class="success-icon"></div>
<h2>{{ 'cart.paymentSuccess' | translate }}</h2> <h2>{{ 'cart.paymentSuccess' | translate }}</h2>
<p class="success-text">{{ 'cart.paymentSuccessDesc' | translate }}</p> <!-- <p class="success-text">{{ 'cart.paymentSuccessDesc' | translate }}</p> -->
<div class="email-form"> <!-- <div class="email-form">
<div class="input-group"> <div class="input-group">
<input <input
type="email" type="email"
@@ -293,7 +309,7 @@
{{ 'cart.send' | translate }} {{ 'cart.send' | translate }}
} }
</button> </button>
</div> </div> -->
</div> </div>
} }

View File

@@ -225,14 +225,14 @@
.cart-content { .cart-content {
display: grid; display: grid;
grid-template-columns: 1fr 350px; grid-template-columns: minmax(0, 1fr) 350px;
gap: 24px; gap: 24px;
align-items: start; align-items: start;
} }
// Novo wider summary // Novo wider summary
.cart-container.novo .cart-content { .cart-container.novo .cart-content {
grid-template-columns: 1fr 400px; grid-template-columns: minmax(0, 1fr) 400px;
gap: 32px; gap: 32px;
} }
@@ -240,6 +240,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
min-width: 0;
} }
// Novo larger gap // Novo larger gap
@@ -554,6 +555,12 @@
font-weight: 700; font-weight: 700;
color: #497671; color: #497671;
} }
.delivery-price {
font-size: 0.85rem;
font-weight: 500;
color: #697777;
}
} }
// Dexar quantity controls // Dexar quantity controls
@@ -802,29 +809,6 @@
box-shadow: 0 4px 12px rgba(42, 171, 238, 0.3); 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;
}
}
}
} }
} }
@@ -860,7 +844,7 @@
color: #6b7280; color: #6b7280;
&.delivery { &.delivery {
display: none; // Hide delivery in Novo display: flex;
} }
&.total { &.total {
@@ -967,29 +951,6 @@
box-shadow: 0 4px 12px rgba(42, 171, 238, 0.3); 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: #9ca3af;
}
.qr-wrapper {
display: inline-flex;
padding: 10px;
background: #fff;
border-radius: 12px;
border: 1px solid #e5e7eb;
img {
display: block;
border-radius: 4px;
}
}
}
} }
} }
@@ -1055,6 +1016,27 @@
} }
} }
.delivery-warning {
margin: -4px 0 0;
padding: 10px 12px;
border-radius: 10px;
font-size: 0.85rem;
line-height: 1.5;
}
.cart-container.dexar .cart-summary .delivery-warning {
color: #9a6700;
background: #fff8e8;
border: 1px solid #f1ddb2;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.cart-container.novo .cart-summary .delivery-warning {
color: #92400e;
background: #fffbeb;
border: 1px solid #fde68a;
}
// Dexar checkbox colors // Dexar checkbox colors
.cart-container.dexar .terms-agreement .checkbox-container { .cart-container.dexar .terms-agreement .checkbox-container {
input[type="checkbox"]:checked ~ .checkmark { input[type="checkbox"]:checked ~ .checkmark {
@@ -1242,6 +1224,45 @@
} }
} }
&.error {
.error-icon {
width: 80px;
height: 80px;
background: #f97316;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
font-weight: 700;
color: white;
margin: 0 auto 20px;
}
.payment-error-actions {
margin-top: 24px;
}
.retry-payment-btn {
padding: 14px 24px;
background: #497671;
color: white;
border: none;
border-radius: 13px;
font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
&:hover {
background: #3a5f5b;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(73, 118, 113, 0.3);
}
}
}
&.timeout { &.timeout {
.timeout-icon { .timeout-icon {
font-size: 4rem; font-size: 4rem;
@@ -1534,11 +1555,31 @@
// Mobile responsive // Mobile responsive
@media (max-width: 768px) { @media (max-width: 768px) {
.cart-content { .cart-content {
grid-template-columns: 1fr; grid-template-columns: minmax(0, 1fr);
gap: 20px;
}
.cart-container.novo .cart-content {
grid-template-columns: minmax(0, 1fr);
gap: 20px;
} }
.cart-summary { .cart-summary {
position: static; position: static;
width: 100%;
max-width: 100%;
min-width: 0;
}
.cart-container.novo,
.cart-container.dexar {
padding: 16px;
}
.cart-container.novo .cart-header,
.cart-container.dexar .cart-header {
flex-wrap: wrap;
gap: 12px;
} }
.remove-btn-desktop { .remove-btn-desktop {
@@ -1816,6 +1857,7 @@
margin: 0; margin: 0;
line-height: 1.5; line-height: 1.5;
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;

View File

@@ -1,11 +1,12 @@
import { Component, computed, ChangeDetectionStrategy, signal, OnDestroy, inject } from '@angular/core'; import { Component, ChangeDetectionStrategy, signal, OnDestroy, inject } from '@angular/core';
import { DecimalPipe } from '@angular/common'; import { DecimalPipe } from '@angular/common';
import { Router, RouterLink } from '@angular/router'; import { Router, RouterLink } from '@angular/router';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { CartService, ApiService, LanguageService, AuthService } from '../../services'; import { CartService, ApiService, LanguageService, AuthService } from '../../services';
import { Item, CartItem } from '../../models'; import { Item, CartItem, DeliveryOption } from '../../models';
import { interval, Subscription } from 'rxjs'; import { interval, of, Subscription } from 'rxjs';
import { switchMap, take } from 'rxjs/operators'; import { catchError, exhaustMap, take, timeout } from 'rxjs/operators';
import { DeliverySelectorComponent } from '../../components/delivery-selector/delivery-selector.component';
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 { TelegramLoginComponent } from '../../components/telegram-login/telegram-login.component';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
@@ -13,11 +14,11 @@ import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass, getTran
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'; import { PAYMENT_POLL_INTERVAL_MS, PAYMENT_MAX_CHECKS, PAYMENT_TIMEOUT_CLOSE_MS, LINK_COPIED_DURATION_MS } from '../../config/constants';
@Component({ @Component({
selector: 'app-cart', selector: 'app-cart',
imports: [DecimalPipe, RouterLink, FormsModule, EmptyCartIconComponent, TelegramLoginComponent, LangRoutePipe, TranslatePipe], imports: [DecimalPipe, RouterLink, FormsModule, EmptyCartIconComponent, DeliverySelectorComponent, 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
@@ -26,6 +27,10 @@ export class CartComponent implements OnDestroy {
items; items;
itemCount; itemCount;
totalPrice; totalPrice;
totalDeliveryPrice;
totalWithDelivery;
hasDeliveryPrice;
allRequiredDeliveriesSelected;
termsAccepted = false; termsAccepted = false;
isnovo = environment.theme === 'novo'; isnovo = environment.theme === 'novo';
@@ -33,14 +38,13 @@ export class CartComponent implements OnDestroy {
private authService = inject(AuthService); private authService = inject(AuthService);
isAuthenticated = this.authService.isAuthenticated; isAuthenticated = this.authService.isAuthenticated;
loginUrl = signal('');
// Swipe state // Swipe state
swipedItemId = signal<number | null>(null); swipedItemId = signal<number | null>(null);
// Payment popup states // Payment popup states
showPaymentPopup = signal<boolean>(false); showPaymentPopup = signal<boolean>(false);
paymentStatus = signal<'creating' | 'waiting' | 'success' | 'timeout'>('creating'); paymentStatus = signal<'creating' | 'waiting' | 'success' | 'timeout' | 'error' | null>('creating');
qrCodeUrl = signal<string>(''); qrCodeUrl = signal<string>('');
paymentUrl = signal<string>(''); paymentUrl = signal<string>('');
paymentId = signal<string>(''); paymentId = signal<string>('');
@@ -69,7 +73,10 @@ 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()); this.totalDeliveryPrice = this.cartService.totalDeliveryPrice;
this.totalWithDelivery = this.cartService.totalWithDelivery;
this.hasDeliveryPrice = this.cartService.hasDeliveryPrice;
this.allRequiredDeliveriesSelected = this.cartService.allRequiredDeliveriesSelected;
} }
requestLogin(): void { requestLogin(): void {
@@ -105,7 +112,6 @@ export class CartComponent implements OnDestroy {
} }
onSwipeStart(itemID: number, event: TouchEvent): void { onSwipeStart(itemID: number, event: TouchEvent): void {
const item = event.currentTarget as HTMLElement;
const startX = event.touches[0].clientX; const startX = event.touches[0].clientX;
const onMove = (e: TouchEvent) => { const onMove = (e: TouchEvent) => {
@@ -144,15 +150,20 @@ export class CartComponent implements OnDestroy {
itemName(item: Item): string { return getTranslatedField(item, 'name', this.langService.currentLanguage()); } itemName(item: Item): string { return getTranslatedField(item, 'name', this.langService.currentLanguage()); }
itemDesc(item: Item): string { return getTranslatedField(item, 'simpleDescription', this.langService.currentLanguage()); } itemDesc(item: Item): string { return getTranslatedField(item, 'simpleDescription', this.langService.currentLanguage()); }
get currentCurrency(): string { return this.langService.currentCurrency(); } get currentCurrency(): string { return this.langService.currentCurrency(); }
get isCheckoutDisabled(): boolean { return !this.termsAccepted || !this.isAuthenticated() || !this.allRequiredDeliveriesSelected(); }
selectDelivery(itemID: number, selectedDelivery: DeliveryOption | null): void {
this.cartService.setSelectedDelivery(itemID, selectedDelivery);
}
checkout(): void { checkout(): void {
if (!this.termsAccepted) { if (!this.allRequiredDeliveriesSelected()) {
alert(this.i18n.t('cart.acceptTerms')); alert(this.i18n.t('cart.deliveryRequired'));
return; return;
} }
// Auth gate: require Telegram login before payment
if (!this.authService.isAuthenticated()) { if (!this.termsAccepted) {
this.authService.requestLogin(); alert(this.i18n.t('cart.acceptTerms'));
return; return;
} }
this.openPaymentPopup(); this.openPaymentPopup();
@@ -161,6 +172,10 @@ export class CartComponent implements OnDestroy {
openPaymentPopup(): void { openPaymentPopup(): void {
this.showPaymentPopup.set(true); this.showPaymentPopup.set(true);
this.paymentStatus.set('creating'); this.paymentStatus.set('creating');
this.paymentId.set('');
this.qrCodeUrl.set('');
this.paymentUrl.set('');
this.linkCopied.set(false);
this.userEmail.set(''); this.userEmail.set('');
this.userPhone.set(''); this.userPhone.set('');
this.emailTouched.set(false); this.emailTouched.set(false);
@@ -181,73 +196,110 @@ export class CartComponent implements OnDestroy {
} }
} }
retryPayment(): void {
if (this.closeTimeout) {
clearTimeout(this.closeTimeout);
this.closeTimeout = undefined;
}
this.paymentStatus.set('creating');
this.paymentId.set('');
this.qrCodeUrl.set('');
this.paymentUrl.set('');
this.linkCopied.set(false);
this.createPayment();
}
createPayment(): void { createPayment(): void {
const sessionId = this.authService.session()?.sessionId || ''; const orderId = this.generateOrderId();
if (!sessionId) { const paymentPayload = {
this.paymentStatus.set('timeout'); amount: Number(this.totalWithDelivery()),
currency: 'RUB' as const,
siteuserID: this.getPaymentUserId(),
siteorderID: orderId,
redirectUrl: '',
telegramUsername: this.getTelegramUsername(),
items: this.buildPaymentItems(),
};
this.apiService.createCartPayment(paymentPayload)
.subscribe({
next: (response) => {
const qrId = this.apiService.resolvePaymentQrId(response);
const qrUrl = this.apiService.resolvePaymentQrUrl(response);
const paymentLink = this.apiService.resolvePaymentLink(response);
if (!qrId || !qrUrl) {
console.error('Payment response missing qr fields:', response);
this.setPaymentError();
return; return;
} }
// First sync cart items to server via websession, then create QR this.paymentId.set(qrId);
const cartItems = this.items().map((item: CartItem) => ({ this.qrCodeUrl.set(qrUrl);
itemID: item.itemID, this.paymentUrl.set(paymentLink);
quantity: item.quantity,
colour: item.colour || '',
size: item.size || '',
price: item.discount > 0
? item.price * (1 - item.discount / 100)
: item.price,
}));
this.apiService.addToCart(sessionId, cartItems).subscribe({
next: () => {
this.apiService.createPayment(sessionId).subscribe({
next: (response) => {
this.paymentId.set(response.qrId);
this.qrCodeUrl.set(response.qrUrl);
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.setPaymentError();
if (this.closeTimeout) clearTimeout(this.closeTimeout);
this.closeTimeout = setTimeout(() => {
this.closePaymentPopup();
}, 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.stopPolling(); this.stopPolling();
if (!this.paymentId()) {
this.setPaymentError();
return;
}
this.pollingSubscription = interval(PAYMENT_POLL_INTERVAL_MS) 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(() => { exhaustMap(() => {
const sessionId = this.authService.session()?.sessionId || ''; return this.apiService.checkCartPaymentStatus(this.paymentId()).pipe(
return this.apiService.checkPaymentStatus(sessionId, this.paymentId()); timeout(8000),
catchError((err) => {
console.error('Error checking payment status:', err);
return of(null);
})
);
}) })
) )
.subscribe({ .subscribe({
next: (response) => { next: (response) => {
if (!response) {
return;
}
const paymentStatus = response.status?.toUpperCase() || '';
const paymentCode = response.code?.toUpperCase() || '';
if (paymentStatus === 'FAILED' || paymentStatus === 'EXPIRED' || paymentStatus === 'CANCELLED' || paymentStatus === 'REJECTED') {
this.paymentStatus.set('timeout');
this.stopPolling();
if (this.closeTimeout) clearTimeout(this.closeTimeout);
this.closeTimeout = setTimeout(() => {
this.closePaymentPopup();
}, PAYMENT_TIMEOUT_CLOSE_MS);
return;
}
// Check if payment is successful // Check if payment is successful
if (response.paymentStatus === 'SUCCESS' && response.code === 'SUCCESS') { if (paymentStatus === 'COMPLETED' || paymentStatus === 'APPROVED' || paymentStatus === 'PAID' || paymentCode === 'SUCCESS') {
this.paymentStatus.set('success'); this.paymentStatus.set('success');
this.stopPolling(); this.stopPolling();
// Clear cart but don't close popup - wait for email submission // Auto-submit purchase after 5 seconds
if (this.closeTimeout) clearTimeout(this.closeTimeout);
this.closeTimeout = setTimeout(() => {
this.autoSubmitPurchase();
}, 5000);
this.cartService.clearCart(); this.cartService.clearCart();
} }
// Continue checking for 3 minutes regardless of other statuses // Continue checking for 3 minutes regardless of other statuses
}, },
@@ -262,14 +314,6 @@ export class CartComponent implements OnDestroy {
this.closePaymentPopup(); this.closePaymentPopup();
}, PAYMENT_TIMEOUT_CLOSE_MS); }, PAYMENT_TIMEOUT_CLOSE_MS);
} }
},
error: (err) => {
console.error('Error checking payment status:', err);
// Continue checking even on error until time runs out
if (this.closeTimeout) clearTimeout(this.closeTimeout);
this.closeTimeout = setTimeout(() => {
this.closePaymentPopup();
}, PAYMENT_TIMEOUT_CLOSE_MS);
} }
}); });
} }
@@ -277,9 +321,71 @@ export class CartComponent implements OnDestroy {
stopPolling(): void { stopPolling(): void {
if (this.pollingSubscription) { if (this.pollingSubscription) {
this.pollingSubscription.unsubscribe(); this.pollingSubscription.unsubscribe();
this.pollingSubscription = undefined;
} }
} }
private setPaymentError(): void {
this.paymentStatus.set('error');
this.stopPolling();
if (this.closeTimeout) {
clearTimeout(this.closeTimeout);
this.closeTimeout = undefined;
}
}
private autoSubmitPurchase(): void {
setTimeout(() => {
const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}`]);}, 0);
const telegramUserId = this.getTelegramUserId();
// Telegram ID is mandatory
if (!telegramUserId) {
console.error('Cannot submit purchase: Telegram ID is required');
this.emailSubmitting.set(false);
return;
}
this.emailSubmitting.set(true);
const emailData = {
email: '',
phone: '',
telegramUserId: telegramUserId,
items: this.paidItems.map((item: CartItem) => ({
itemID: item.itemID,
name: item.name,
price: item.discount > 0
? item.price * (1 - item.discount / 100)
: item.price,
currency: item.currency,
quantity: item.quantity,
...(item.selectedDelivery ? { delivery: item.selectedDelivery } : {})
}))
};
this.apiService.submitPurchaseEmail(emailData).subscribe({
next: () => {
this.emailSubmitting.set(false);
this.closePaymentPopup();
const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}`]);
},
error: (err) => {
console.error('Error submitting purchase:', err);
this.emailSubmitting.set(false);
// Still close popup and redirect even if submission fails
this.closePaymentPopup();
const lang = this.langService.currentLanguage();
this.router.navigate([`/${lang}`]);
}
});
this.paymentStatus.set(null);
}
copyPaymentLink(): void { copyPaymentLink(): void {
const url = this.paymentUrl(); const url = this.paymentUrl();
if (url) { if (url) {
@@ -323,7 +429,8 @@ export class CartComponent implements OnDestroy {
? item.price * (1 - item.discount / 100) ? item.price * (1 - item.discount / 100)
: item.price, : item.price,
currency: item.currency, currency: item.currency,
quantity: item.quantity quantity: item.quantity,
...(item.selectedDelivery ? { delivery: item.selectedDelivery } : {})
})) }))
}; };
@@ -348,12 +455,60 @@ export class CartComponent implements OnDestroy {
} }
private getTelegramUserId(): string | null { private getTelegramUserId(): string | null {
const sessionTelegramUserId = this.authService.session()?.userId;
if (sessionTelegramUserId !== null && sessionTelegramUserId !== undefined) {
return sessionTelegramUserId.toString();
}
if (typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.user) { if (typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.user) {
return window.Telegram.WebApp.initDataUnsafe.user.id.toString(); return window.Telegram.WebApp.initDataUnsafe.user.id.toString();
} }
return null; return null;
} }
private getTelegramUsername(): string {
const sessionUsername = this.authService.session()?.username;
if (sessionUsername) {
return sessionUsername;
}
if (typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.user) {
return window.Telegram.WebApp.initDataUnsafe.user.username || 'nontelegram';
}
return 'nontelegram';
}
private getPaymentUserId(): string {
return this.getTelegramUserId() ?? `web_${Date.now()}`;
}
private generateOrderId(): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2, 8);
return `order_${timestamp}_${random}`;
}
private buildPaymentItems(): Array<{ itemID: number; price: number; name: string; quantity: number; delivery?: DeliveryOption }> {
return this.items().map((item: CartItem) => {
const unitPrice = item.discount > 0
? item.price * (1 - item.discount / 100)
: item.price;
const details = [item.colour, item.size].filter(Boolean).join(', ');
const translatedName = this.itemName(item).trim() || `Item ${item.itemID}`;
const name = details ? `${item.quantity} x ${translatedName} (${details})` : `${item.quantity} x ${translatedName}`;
return {
itemID: item.itemID,
price: unitPrice * item.quantity,
name,
quantity: item.quantity,
...(item.selectedDelivery ? { delivery: item.selectedDelivery } : {}),
};
});
}
onPhoneInput(event: Event): void { onPhoneInput(event: Event): void {
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
let value = input.value.replace(/\D/g, ''); // Remove all non-digits let value = input.value.replace(/\D/g, ''); // Remove all non-digits

View File

@@ -63,10 +63,10 @@ export class SubcategoriesComponent implements OnInit, OnDestroy {
// Check for nested subcategories from API response (backOffice format) // Check for nested subcategories from API response (backOffice format)
const nested = parent?.subcategories || []; const nested = parent?.subcategories || [];
const visibleNested = nested.filter(s => s.visible !== false); const visibleNested = nested.filter(s => this.isDisplayableNestedSubcategory(s));
// Also check flat legacy subcategories // Also check flat legacy subcategories
const flatSubs = cats.filter(c => c.parentID === parentID); const flatSubs = cats.filter(c => c.parentID === parentID && this.isDisplayableFlatSubcategory(c));
if (visibleNested.length > 0) { if (visibleNested.length > 0) {
// Use nested subcategories from API // Use nested subcategories from API
@@ -110,6 +110,20 @@ export class SubcategoriesComponent implements OnInit, OnDestroy {
}); });
} }
private isDisplayableFlatSubcategory(category: Category): boolean {
return category.visible !== false
&& ((category.itemCount ?? 0) > 0 || (category.subcategories?.length ?? 0) > 0);
}
private isDisplayableNestedSubcategory(subcategory: Subcategory): boolean {
return subcategory.visible !== false
&& (
(subcategory.itemCount ?? 0) > 0
|| subcategory.hasItems === true
|| (subcategory.subcategories?.length ?? 0) > 0
);
}
hasSubcategories(): boolean { hasSubcategories(): boolean {
return this.subcategories().length > 0 || this.nestedSubcategories().length > 0; return this.subcategories().length > 0 || this.nestedSubcategories().length > 0;
} }
@@ -121,11 +135,11 @@ export class SubcategoriesComponent implements OnInit, OnDestroy {
} }
// 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 { trackBySubId(_index: number, sub: Subcategory): string {
return sub.id; return sub.id;
} }

View File

@@ -28,6 +28,7 @@ export class HomeComponent implements OnInit, OnDestroy {
topLevelCategories = computed(() => { topLevelCategories = computed(() => {
return this.categories() return this.categories()
.filter(cat => cat.parentID === 0) .filter(cat => cat.parentID === 0)
.filter(cat => this.isDisplayableTopLevelCategory(cat))
.sort((a, b) => (a.priority ?? Infinity) - (b.priority ?? Infinity)); .sort((a, b) => (a.priority ?? Infinity) - (b.priority ?? Infinity));
}); });
@@ -42,7 +43,7 @@ export class HomeComponent implements OnInit, OnDestroy {
private subcategoriesCache = computed(() => { private subcategoriesCache = computed(() => {
const cache = new Map<number, Category[]>(); const cache = new Map<number, Category[]>();
this.categories().forEach(cat => { this.categories().forEach(cat => {
if (cat.parentID !== 0) { if (cat.parentID !== 0 && this.isDisplayableFlatSubcategory(cat)) {
if (!cache.has(cat.parentID)) { if (!cache.has(cat.parentID)) {
cache.set(cat.parentID, []); cache.set(cat.parentID, []);
} }
@@ -90,6 +91,21 @@ export class HomeComponent implements OnInit, OnDestroy {
return this.subcategoriesCache().get(parentID) || []; return this.subcategoriesCache().get(parentID) || [];
} }
private isDisplayableFlatSubcategory(category: Category): boolean {
return category.visible !== false
&& ((category.itemCount ?? 0) > 0 || (category.subcategories?.length ?? 0) > 0);
}
private isDisplayableTopLevelCategory(category: Category): boolean {
return category.visible !== false
&& (
(category.itemCount ?? 0) > 0
|| (category.categoriesCount ?? 0) > 0
|| (category.subcategories?.length ?? 0) > 0
|| this.getSubCategories(category.categoryID).length > 0
);
}
isWideCategory(categoryID: number): boolean { isWideCategory(categoryID: number): boolean {
return this.wideCategories().has(categoryID); return this.wideCategories().has(categoryID);
} }

View File

@@ -144,6 +144,32 @@
</div> </div>
} }
@if (hasDeliveryInfo()) {
<div class="novo-delivery">
<h3 class="novo-delivery-title">{{ 'cart.deliveryLabel' | translate }}</h3>
@if (isDigitalDelivery()) {
<div class="novo-delivery-chip">{{ 'cart.digitalDelivery' | translate }}</div>
} @else {
<div class="novo-delivery-list">
@for (option of deliveryOptions(); track trackByDeliveryOption($index, option)) {
<div class="novo-delivery-option">
<div class="novo-delivery-option-main">
@if (option.deliveryPlace) {
<span>{{ 'cart.deliveryPlace' | translate }}: {{ option.deliveryPlace }}</span>
}
@if (option.deliveryTime) {
<span>{{ 'cart.deliveryTime' | translate }}: {{ option.deliveryTime }}</span>
}
</div>
<span class="novo-delivery-price">{{ option.deliveryPrice | number:'1.2-2' }} {{ getDeliveryCurrency() }}</span>
</div>
}
</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>
@@ -423,6 +449,32 @@
</div> </div>
} }
@if (hasDeliveryInfo()) {
<div class="dx-delivery">
<h3 class="dx-delivery-title">{{ 'cart.deliveryLabel' | translate }}</h3>
@if (isDigitalDelivery()) {
<div class="dx-delivery-chip">{{ 'cart.digitalDelivery' | translate }}</div>
} @else {
<div class="dx-delivery-list">
@for (option of deliveryOptions(); track trackByDeliveryOption($index, option)) {
<div class="dx-delivery-option">
<div class="dx-delivery-option-main">
@if (option.deliveryPlace) {
<span>{{ 'cart.deliveryPlace' | translate }}: {{ option.deliveryPlace }}</span>
}
@if (option.deliveryTime) {
<span>{{ 'cart.deliveryTime' | translate }}: {{ option.deliveryTime }}</span>
}
</div>
<span class="dx-delivery-price">{{ option.deliveryPrice | number:'1.2-2' }} {{ getDeliveryCurrency() }}</span>
</div>
}
</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>

View File

@@ -383,6 +383,65 @@ $dx-card-bg: #f5f3f9;
} }
} }
.dx-delivery, .novo-delivery {
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px 16px;
border: 1px solid $dx-border;
border-radius: 12px;
background: #f8fafa;
}
.dx-delivery-title, .novo-delivery-title {
margin: 0;
font-size: 1rem;
font-weight: 700;
color: $dx-dark;
}
.dx-delivery-list, .novo-delivery-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.dx-delivery-option, .novo-delivery-option {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
padding: 10px 12px;
border-radius: 10px;
background: white;
}
.dx-delivery-option-main, .novo-delivery-option-main {
display: flex;
flex-direction: column;
gap: 4px;
color: $dx-muted;
font-size: 0.9rem;
}
.dx-delivery-price, .novo-delivery-price {
flex-shrink: 0;
font-weight: 700;
color: $dx-primary;
font-size: 0.95rem;
}
.dx-delivery-chip, .novo-delivery-chip {
display: inline-flex;
align-self: flex-start;
padding: 6px 12px;
border-radius: 999px;
background: rgba(73, 118, 113, 0.12);
color: $dx-primary;
font-size: 0.9rem;
font-weight: 700;
}
.dx-description { .dx-description {
padding-top: 8px; padding-top: 8px;
border-top: 1px solid $dx-border; border-top: 1px solid $dx-border;

View File

@@ -4,12 +4,12 @@ import { FormsModule } from '@angular/forms';
import { ActivatedRoute, RouterLink } from '@angular/router'; import { ActivatedRoute, RouterLink } from '@angular/router';
import { ApiService, CartService, TelegramService, LanguageService, SeoService } from '../../services'; import { ApiService, CartService, TelegramService, LanguageService, SeoService } from '../../services';
import { AuthService } from '../../services/auth.service'; import { AuthService } from '../../services/auth.service';
import { Item, ItemDetail, DescriptionField } from '../../models'; import { Item, ItemDetail, DescriptionField, DeliveryOption } 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, getAllImages, getStockStatus, getBadgeClass, getTranslatedField } from '../../utils/item.utils'; import { 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';
@@ -77,6 +77,13 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
return detail?.remaining ?? this.item()?.quantity ?? null; return detail?.remaining ?? this.item()?.quantity ?? null;
}); });
deliveryOptions = computed(() => this.item()?.deliveryOptions ?? []);
hasDeliveryInfo = computed(() => {
const currentItem = this.item();
return currentItem?.deliveryMode === 'digital' || this.deliveryOptions().length > 0;
});
newReview = { newReview = {
rating: 0, rating: 0,
comment: '', comment: '',
@@ -187,6 +194,18 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
return discount > 0 ? price * (1 - discount / 100) : price; return discount > 0 ? price * (1 - discount / 100) : price;
} }
isDigitalDelivery(): boolean {
return this.item()?.deliveryMode === 'digital';
}
getDeliveryCurrency(): string {
return this.effectiveCurrency() || this.item()?.currency || 'RUB';
}
trackByDeliveryOption(index: number, option: DeliveryOption): string {
return `${index}-${option.deliveryPlace}-${option.deliveryTime}-${option.deliveryPrice}`;
}
// BackOffice integration helpers // BackOffice integration helpers
getItemName(): string { getItemName(): string {
@@ -297,7 +316,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
}; };
this.apiService.submitReview(reviewData).subscribe({ this.apiService.submitReview(reviewData).subscribe({
next: (response) => { next: () => {
this.reviewSubmitStatus.set('success'); this.reviewSubmitStatus.set('success');
this.newReview = { rating: 0, comment: '', anonymous: false }; this.newReview = { rating: 0, comment: '', anonymous: false };

View File

@@ -70,6 +70,14 @@
<p>2.2. The agreement regulates the procedure for using the site and the functions provided by the Owner.</p> <p>2.2. The agreement regulates the procedure for using the site and the functions provided by the Owner.</p>
<p>2.3. The document applies to all types of goods, services and products available on the site.</p> <p>2.3. The document applies to all types of goods, services and products available on the site.</p>
<p>2.4. The Marketplace is an agent acting under agency agreements with Sellers (Third Parties). The Marketplace does not acquire ownership rights to the Goods/Services sold through the Platform and only provides the technical ability to conclude a transaction between the Seller and the Buyer.</p>
<p>2.5. The Seller is an independent business entity and bears full responsibility for the quality, safety, conformity of the Goods/Services to the stated characteristics, and fulfillment of obligations to the Buyer (warranties, returns, claims).</p>
<p>2.6. The Marketplace is not a party to the sale and purchase agreement between the Seller and the Buyer. All claims regarding the Goods/Services are submitted by the Buyer directly to the Seller.</p>
<p>2.7. The Marketplace's agency remuneration is withheld from the amount paid by the Buyer and does not increase the price for the Buyer beyond the price set by the Seller.</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
@@ -509,7 +517,7 @@
<p>Free services provided under the agreement do not imply the application of consumer protection legislation to the relationship between the User and the Seller.</p> <p>Free services provided under the agreement do not imply the application of consumer protection legislation to the relationship between the User and the Seller.</p>
<p><strong>16.6. Absent Relationships</strong></p> <p><strong>16.6. Absent Relationships</strong></p>
<p>No provisions of the agreement may be construed as establishing agency relationships, partnerships, joint ventures, employment relationships or other types of relationships not expressly provided for by the agreement.</p> <p>No provisions of the agreement, except for the agency model with Sellers expressly provided in clause 2.4, may be construed as establishing partnerships, joint ventures, employment relationships or other types of relationships not expressly provided for by the agreement.</p>
<p><strong>16.7. Invalidity of Provisions</strong></p> <p><strong>16.7. Invalidity of Provisions</strong></p>
<p>The recognition of one or more provisions of the agreement as invalid does not affect the legal force and applicability of the remaining provisions.</p> <p>The recognition of one or more provisions of the agreement as invalid does not affect the legal force and applicability of the remaining provisions.</p>

View File

@@ -70,6 +70,14 @@
<p>2.2. Համաձայնագիրը կարգավորում է կայքի և Սեփականատիրոջ կողմից տրամադրվող գործառույթների օգտագործման կարգը:</p> <p>2.2. Համաձայնագիրը կարգավորում է կայքի և Սեփականատիրոջ կողմից տրամադրվող գործառույթների օգտագործման կարգը:</p>
<p>2.3. Փաստաթղթի գործողությունը տարածվում է կայքում առկա ապրանքների, ծառայությունների և պրոդուկտների բոլոր տեսակների վրա:</p> <p>2.3. Փաստաթղթի գործողությունը տարածվում է կայքում առկա ապրանքների, ծառայությունների և պրոդուկտների բոլոր տեսակների վրա:</p>
<p>2.4. Մարկետփլեյսը հանդիսանում է գործակալ, որը գործում է Վաճառողների (Երրորդ անձանց) հետ կնքված գործակալության պայմանագրի հիման վրա։ Մարկետփլեյսը չի ձեռք բերում Հարթակի միջոցով իրացվող Ապրանքների/Ծառայությունների նկատմամբ սեփականության իրավունք, այլ միայն ապահովում է Վաճառողի և Գնորդի միջև գործարք կնքելու տեխնիկական հնարավորությունը։</p>
<p>2.5. Վաճառողը հանդիսանում է ինքնուրույն տնտեսվարող սուբյեկտ և կրում է ամբողջական պատասխանատվություն Ապրանքի/Ծառայության որակի, անվտանգության, հայտարարված բնութագրերին համապատասխանության, ինչպես նաև Գնորդի նկատմամբ պարտավորությունների կատարման համար (երաշխիքներ, վերադարձ, պահանջներ)։</p>
<p>2.6. Մարկետփլեյսը չի հանդիսանում Վաճառողի և Գնորդի միջև առուվաճառքի պայմանագրի կողմ։ Ապրանքի/Ծառայության վերաբերյալ բոլոր պահանջները Գնորդը ներկայացնում է անմիջապես Վաճառողին։</p>
<p>2.7. Մարկետփլեյսի գործակալական վարձատրությունը պահվում է Գնորդի վճարած գումարից և չի ավելացնում Գնորդի համար գինը Վաճառողի սահմանած գնից ավել։</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
@@ -509,7 +517,7 @@
<p>Համաձայնագրի շրջանակում տրամադրվող անվճար ծառայությունները չեն ենթադրում սպառողների իրավունքների պաշտպանության մասին օրենսդրության նորմերի կիրառում Օգտատիրոջ և Վաճառողի հարաբերությունների նկատմամբ:</p> <p>Համաձայնագրի շրջանակում տրամադրվող անվճար ծառայությունները չեն ենթադրում սպառողների իրավունքների պաշտպանության մասին օրենսդրության նորմերի կիրառում Օգտատիրոջ և Վաճառողի հարաբերությունների նկատմամբ:</p>
<p><strong>16.6. Բացակայող հարաբերություններ</strong></p> <p><strong>16.6. Բացակայող հարաբերություններ</strong></p>
<p>Համաձայնագրի ոչ մի դրույթ չի կարող մեկնաբանվել որպես գործակալական հարաբերությունների, գործընկերության, համատեղ ձեռնարկության, աշխատանքային հարաբերությունների կամ այլ հարաբերությունների հաստատում, որոնք ուղղակիորեն նախատեսված չեն համաձայնագրով:</p> <p>Համաձայնագրի ոչ մի դրույթ, բացի 2.4 կետով Վաճառողների հետ ուղղակիորեն նախատեսված գործակալական մոդելից, չի կարող մեկնաբանվել որպես գործընկերության, համատեղ ձեռնարկության, աշխատանքային հարաբերությունների կամ համաձայնագրով ուղղակիորեն չնախատեսված այլ հարաբերությունների հաստատում:</p>
<p><strong>16.7. Կետերի անվավեր ճանաչում</strong></p> <p><strong>16.7. Կետերի անվավեր ճանաչում</strong></p>
<p>Համաձայնագրի մեկ կամ մի քանի դրույթների անվավեր ճանաչումը չի ազդում մնացած կետերի իրավաբանական ուժի և կիրառելիության վրա:</p> <p>Համաձայնագրի մեկ կամ մի քանի դրույթների անվավեր ճանաչումը չի ազդում մնացած կետերի իրավաբանական ուժի և կիրառելիության վրա:</p>

View File

@@ -70,6 +70,14 @@
<p>2.2. Соглашение регулирует порядок использования сайта и функций, предоставляемых Владельцем.</p> <p>2.2. Соглашение регулирует порядок использования сайта и функций, предоставляемых Владельцем.</p>
<p>2.3. Действие документа распространяется на все типы товаров, услуг и продуктов, имеющихся на сайте.</p> <p>2.3. Действие документа распространяется на все типы товаров, услуг и продуктов, имеющихся на сайте.</p>
<p>2.4. Маркетплейс является агентом, действующим на основании договора агентирования с Продавцами (Третьими лицами). Маркетплейс не приобретает право собственности на Товары/Услуги, реализуемые через Платформу, а лишь обеспечивает техническую возможность заключения сделки между Продавцом и Покупателем.</p>
<p>2.5. Продавец является самостоятельным хозяйствующим субъектом и несёт полную ответственность за качество, безопасность, соответствие Товара/Услуги заявленным характеристикам, а также за исполнение обязательств перед Покупателем (гарантии, возврат, претензии).</p>
<p>2.6. Маркетплейс не выступает стороной договора купли-продажи между Продавцом и Покупателем. Все претензии по Товару/Услуге Покупатель предъявляет непосредственно Продавцу.</p>
<p>2.7. Агентское вознаграждение Маркетплейса удерживается из суммы, уплаченной Покупателем, и не увеличивает цену для Покупателя сверх установленной Продавцом.</p>
</section> </section>
<section class="legal-section"> <section class="legal-section">
@@ -509,7 +517,7 @@
<p>Бесплатные услуги, предоставляемые в рамках соглашения, не предполагают применение норм законодательства о защите прав потребителей к взаимоотношениям между Пользователем и Продавцом.</p> <p>Бесплатные услуги, предоставляемые в рамках соглашения, не предполагают применение норм законодательства о защите прав потребителей к взаимоотношениям между Пользователем и Продавцом.</p>
<p><strong>16.6. Отсутствующие отношения</strong></p> <p><strong>16.6. Отсутствующие отношения</strong></p>
<p>Никакие положения соглашения не могут трактоваться как установление агентских связей, партнёрства, совместного предприятия, трудовых отношений или иных видов взаимоотношений, прямо не предусмотренных соглашением.</p> <p>Никакие положения соглашения, кроме прямо предусмотренной пунктом 2.4 агентской модели взаимодействия с Продавцами, не могут трактоваться как установление партнёрства, совместного предприятия, трудовых отношений или иных видов взаимоотношений, прямо не предусмотренных соглашением.</p>
<p><strong>16.7. Признание недействительности пунктов</strong></p> <p><strong>16.7. Признание недействительности пунктов</strong></p>
<p>Признание одного или нескольких положений соглашения недействительным не влияет на законную силу и применимость оставшихся пунктов.</p> <p>Признание одного или нескольких положений соглашения недействительным не влияет на законную силу и применимость оставшихся пунктов.</p>

View File

@@ -1,19 +1,76 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable, timer } from 'rxjs'; import { Observable, timer } from 'rxjs';
import { map, retry } from 'rxjs/operators'; import { map, retry } from 'rxjs/operators';
import { Category, Item, Subcategory } from '../models'; import { Category, DeliveryOption, Item, Subcategory } from '../models';
import { normalizeDeliveryOption, normalizeOptionalNumber } from '../utils/normalization.utils';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
export interface QrCreateRequest {
qrtype: 'QRDynamic';
amount: number;
currency: 'RUB';
partnerqrID?: string;
qrDescription?: string;
Userid?: string;
Reference?: string;
RedirectUrl?: string;
}
export interface QrCreateResponse {
qrId?: string;
qrID?: string;
nspkID?: string;
nspkId?: string;
nspkurl?: string;
status?: string;
qrStatus?: string;
qrExpirationDate?: string;
payload?: string;
Payload?: string;
qrUrl?: string;
partnerqrID?: string | number;
partnerID?: string | number;
partnerId?: string | number;
PartnerID?: string | number;
}
export interface CartPaymentRequest {
amount: number;
currency: 'RUB';
siteuserID: string;
siteorderID: string;
redirectUrl: string;
telegramUsername: string;
items: Array<{ itemID: number; price: number; name: string; quantity?: number; delivery?: DeliveryOption }>;
}
export interface QrDynamicStatusResponse {
additionalInfo: string;
paymentPurpose: string;
amount: number;
code: string;
createDate: string;
currency: string;
order: string;
status: string;
qrId: string;
transactionDate: string;
transactionId: number;
qrExpirationDate: string;
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ApiService { export class ApiService {
private readonly baseUrl = environment.apiUrl; private readonly baseUrl = environment.apiUrl;
private readonly qrBaseUrl = (environment as any).qrApiUrl as string;
private readonly cartPaymentPartnerId = 'web-97ec-9c57-4dde-9037-3a68f7f83750';
private readonly retryConfig = { private readonly retryConfig = {
count: 2, count: 2,
delay: (error: unknown, retryCount: number) => timer(Math.pow(2, retryCount) * 500) delay: (_error: unknown, retryCount: number) => timer(Math.pow(2, retryCount) * 500)
}; };
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
@@ -30,6 +87,83 @@ export class ApiService {
return c.startsWith('0x') ? '#' + c.slice(2) : c; return c.startsWith('0x') ? '#' + c.slice(2) : c;
} }
private normalizeDeliveryData(
raw: any,
legacyDeliveryPrice?: number
): { options: DeliveryOption[]; isDigital: boolean; requiresSelection: boolean } {
const rawDelivery = raw.delivery ?? raw.deliveries;
if (typeof rawDelivery === 'string' && rawDelivery.trim().toLowerCase() === 'digital') {
return { options: [], isDigital: true, requiresSelection: false };
}
const deliveryCandidates = Array.isArray(rawDelivery)
? rawDelivery
: rawDelivery != null
? [rawDelivery]
: [];
const options = deliveryCandidates
.map(candidate => normalizeDeliveryOption(candidate))
.filter((option): option is DeliveryOption => option !== null);
if (options.length > 0) {
return { options, isDigital: false, requiresSelection: rawDelivery != null };
}
if (legacyDeliveryPrice !== undefined) {
return {
options: [{ deliveryPrice: legacyDeliveryPrice, deliveryPlace: '', deliveryTime: '' }],
isDigital: false,
requiresSelection: false,
};
}
if (rawDelivery != null) {
return { options: [], isDigital: true, requiresSelection: false };
}
return { options: [], isDigital: false, requiresSelection: false };
}
private normalizeSubcategory(raw: any): Subcategory {
const subcategory: Subcategory = {
id: String(raw.id ?? raw.categoryId ?? raw.categoryID ?? ''),
name: typeof raw.name === 'string' ? raw.name : '',
visible: raw.visible ?? true,
priority: raw.priority ?? 0,
img: raw.img ? this.resolveImageUrl(raw.img) : undefined,
categoryId: String(raw.categoryId ?? raw.categoryID ?? raw.id ?? ''),
parentId: String(raw.parentId ?? raw.parentID ?? ''),
itemCount: raw.itemCount ?? raw.ItemsCount ?? 0,
hasItems: raw.hasItems,
subcategories: Array.isArray(raw.subcategories)
? raw.subcategories
.map((sub: any) => this.normalizeSubcategory(sub))
.filter((sub: Subcategory) => this.isDisplayableSubcategory(sub))
: [],
};
return subcategory;
}
private isDisplayableSubcategory(subcategory: Subcategory): boolean {
if (subcategory.visible === false) {
return false;
}
return (subcategory.itemCount ?? 0) > 0
|| subcategory.hasItems === true
|| (subcategory.subcategories?.length ?? 0) > 0;
}
private isDisplayableCategory(category: Category): boolean {
return category.visible !== false;
}
private isDisplayableItem(item: Item): boolean {
return item.visible !== false;
}
/** Resolve relative image URLs (e.g. ./images/x.webp) against site origin */ /** Resolve relative image URLs (e.g. ./images/x.webp) against site origin */
private resolveImageUrl(url: string): string { private resolveImageUrl(url: string): string {
if (!url) return ''; if (!url) return '';
@@ -46,6 +180,9 @@ export class ApiService {
private normalizeItem(raw: any): Item { private normalizeItem(raw: any): Item {
const { partnerID, ...rest } = raw; const { partnerID, ...rest } = raw;
const item: Item = { ...rest }; const item: Item = { ...rest };
let legacyDeliveryPrice = normalizeOptionalNumber(
raw.deliveryPrice ?? raw.delivery_price ?? raw.deliveryprice
);
// Extract price/currency/remaining/colour/size from itemDetails[] // Extract price/currency/remaining/colour/size from itemDetails[]
// Note: Go struct tag is "itemdetails" but actual API may send "itemDetails" // Note: Go struct tag is "itemdetails" but actual API may send "itemDetails"
@@ -56,16 +193,33 @@ export class ApiService {
...d, ...d,
colour: this.normalizeColor(d.colour || d.color || ''), colour: this.normalizeColor(d.colour || d.color || ''),
color: undefined, color: undefined,
deliveryPrice: normalizeOptionalNumber(
d.deliveryPrice ?? d.delivery_price ?? d.deliveryprice
),
})); }));
if (item.price == null || item.price === 0) item.price = detail.price; if (item.price == null || item.price === 0) item.price = detail.price;
if (!item.currency) item.currency = detail.currency; if (!item.currency) item.currency = detail.currency;
if (!item.colour) item.colour = this.normalizeColor(detail.colour || detail.color || ''); if (!item.colour) item.colour = this.normalizeColor(detail.colour || detail.color || '');
if (!item.size) item.size = detail.size || ''; if (!item.size) item.size = detail.size || '';
if (legacyDeliveryPrice === undefined) {
legacyDeliveryPrice = normalizeOptionalNumber(
detail.deliveryPrice ?? detail.delivery_price ?? detail.deliveryprice
);
}
// Use remaining from detail for stock level // Use remaining from detail for stock level
if (raw.remaining == null && detail.remaining != null) { if (raw.remaining == null && detail.remaining != null) {
(raw as any).remaining = detail.remaining; (raw as any).remaining = detail.remaining;
} }
} }
const deliveryData = this.normalizeDeliveryData(raw, legacyDeliveryPrice);
if (deliveryData.options.length > 0) {
item.deliveryOptions = deliveryData.options;
item.deliveryMode = 'selectable';
item.deliverySelectionRequired = deliveryData.requiresSelection;
} else if (deliveryData.isDigital) {
item.deliveryMode = 'digital';
item.deliverySelectionRequired = false;
}
// Map backOffice string id → legacy numeric itemID // Map backOffice string id → legacy numeric itemID
if (raw.id != null && raw.itemID == null) { if (raw.id != null && raw.itemID == null) {
@@ -186,7 +340,9 @@ export class ApiService {
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))
.filter(item => this.isDisplayableItem(item));
} }
/** /**
@@ -244,7 +400,9 @@ export class ApiService {
cat.name = cat.name || ''; cat.name = cat.name || '';
if (raw.subcategories && Array.isArray(raw.subcategories)) { if (raw.subcategories && Array.isArray(raw.subcategories)) {
cat.subcategories = raw.subcategories; cat.subcategories = raw.subcategories
.map((sub: any) => this.normalizeSubcategory(sub))
.filter((sub: Subcategory) => this.isDisplayableSubcategory(sub));
} }
return cat; return cat;
@@ -252,7 +410,9 @@ export class ApiService {
private normalizeCategories(cats: any[] | null | undefined): Category[] { private normalizeCategories(cats: any[] | null | undefined): Category[] {
if (!cats || !Array.isArray(cats)) return []; if (!cats || !Array.isArray(cats)) return [];
return cats.map(c => this.normalizeCategory(c)); return cats
.map(c => this.normalizeCategory(c))
.filter(category => this.isDisplayableCategory(category));
} }
// ─── Core Marketplace Endpoints ─────────────────────────── // ─── Core Marketplace Endpoints ───────────────────────────
@@ -348,57 +508,55 @@ export class ApiService {
return this.http.post<{ message: string }>(`${this.baseUrl}/items/${itemID}/questiion`, body); return this.http.post<{ message: string }>(`${this.baseUrl}/items/${itemID}/questiion`, body);
} }
// Payment - SBP Integration via websession QR createPayment(payload: QrCreateRequest, headers?: { authorizationKey?: string; userIdValue?: string }): Observable<QrCreateResponse> {
createPayment(sessionId: string): Observable<{ let httpHeaders = new HttpHeaders();
qrId: string; if (headers?.authorizationKey) {
qrStatus: string; httpHeaders = httpHeaders.set('authorization-key', headers.authorizationKey);
qrExpirationDate: string; }
Payload: string; if (headers?.userIdValue) {
qrUrl: string; httpHeaders = httpHeaders.set('userid-value', headers.userIdValue);
}> { }
return this.http.post<{ return this.http.post<QrCreateResponse>(`${this.qrBaseUrl}/qr`, payload, { headers: httpHeaders });
qrId: string;
qrStatus: string;
qrExpirationDate: string;
Payload: string;
qrUrl: string;
}>(`${this.baseUrl}/websession/${sessionId}/qr`, {});
} }
checkPaymentStatus(sessionId: string, qrId: string): Observable<{ createCartPayment(payload: CartPaymentRequest): Observable<QrCreateResponse> {
additionalInfo: string; return this.http.post<QrCreateResponse>(`${this.baseUrl}/cart`, payload);
paymentPurpose: string; }
amount: number;
code: string; checkCartPaymentStatus(qrId: string): Observable<QrDynamicStatusResponse> {
createDate: string; return this.http.get<QrDynamicStatusResponse>(
currency: string; `${this.qrBaseUrl}/qr/dynamic/${this.cartPaymentPartnerId}/${encodeURIComponent(qrId)}`
order: string; );
paymentStatus: string; }
qrId: string;
transactionDate: string; checkPaymentStatus(partnerQrId: string, qrId: string): Observable<QrDynamicStatusResponse> {
transactionId: number; return this.http.get<QrDynamicStatusResponse>(
qrExpirationDate: string; `${this.qrBaseUrl}/qr/dynamic/${encodeURIComponent(partnerQrId)}/${encodeURIComponent(qrId)}`
}> { );
return this.http.get<{ }
additionalInfo: string;
paymentPurpose: string; resolvePaymentQrId(response: QrCreateResponse): string {
amount: number; return response.qrId ?? response.qrID ?? response.nspkID ?? response.nspkId ?? '';
code: string; }
createDate: string;
currency: string; resolvePaymentLink(response: QrCreateResponse): string {
order: string; return response.nspkurl ?? response.Payload ?? response.payload ?? response.qrUrl ?? '';
paymentStatus: string; }
qrId: string;
transactionDate: string; resolvePaymentQrUrl(response: QrCreateResponse): string {
transactionId: number; const paymentLink = this.resolvePaymentLink(response);
qrExpirationDate: string; if (paymentLink) {
}>(`${this.baseUrl}/websession/${sessionId}/${qrId}`); return `https://api.qrserver.com/v1/create-qr-code/?size=256x256&margin=8&data=${encodeURIComponent(paymentLink)}`;
}
return response.qrUrl ?? '';
} }
submitPurchaseEmail(emailData: { submitPurchaseEmail(emailData: {
email: string; email: string;
phone?: string;
telegramUserId: string | null; telegramUserId: string | null;
items: Array<{ itemID: number; name: string; price: number; currency: string }>; items: Array<{ itemID: number; name: string; price: number; currency: string; quantity?: number; delivery?: DeliveryOption }>;
}): Observable<{ message: string }> { }): Observable<{ message: string }> {
return this.http.post<{ message: string }>(`${this.baseUrl}/purchase-email`, emailData); return this.http.post<{ message: string }>(`${this.baseUrl}/purchase-email`, emailData);
} }

View File

@@ -1,9 +1,12 @@
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, QrPollResponse } from '../models/auth.model'; import { AuthSession, AuthStatus, WebSessionStart } from '../models/auth.model';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
const WEB_SESSION_COOKIE = 'webSessionID';
const WEB_SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60;
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
@@ -23,53 +26,45 @@ export class AuthService {
/** Display name of authenticated user */ /** Display name of authenticated user */
readonly displayName = computed(() => this.sessionSignal()?.displayName ?? null); readonly displayName = computed(() => this.sessionSignal()?.displayName ?? null);
private readonly apiUrl = environment.apiUrl; private readonly authApiUrl = environment.authApiUrl;
private sessionCheckTimer?: ReturnType<typeof setInterval>; private sessionCheckTimer?: ReturnType<typeof setTimeout>;
constructor(private http: HttpClient) { constructor(private http: HttpClient) {
// On init, check existing session via cookie // On init, check existing session via cookie
this.checkSession(); this.checkSession();
} }
/** /** Check the current webSessionID cookie against the auth backend. */
* Check current session status with backend.
* The backend reads the session cookie and returns the session info.
*/
checkSession(): void { checkSession(): void {
const webSessionID = this.getStoredWebSessionID();
if (!webSessionID) {
this.clearAuthState('unauthenticated');
return;
}
this.statusSignal.set('checking'); this.statusSignal.set('checking');
this.http.get<AuthSession>(`${this.apiUrl}/auth/session`, { this.checkSessionOnce(webSessionID).subscribe(session => {
withCredentials: true if (!session?.active) {
}).pipe( this.clearAuthState('unauthenticated');
catchError(() => {
this.statusSignal.set('unauthenticated');
this.sessionSignal.set(null);
return of(null);
})
).subscribe(session => {
if (session && session.active) {
this.sessionSignal.set(session);
this.statusSignal.set('authenticated');
this.scheduleSessionRefresh(session.expiresAt);
} else if (session && !session.active) {
this.sessionSignal.set(null);
this.statusSignal.set('expired');
} else {
this.statusSignal.set('unauthenticated');
} }
}); });
} }
/** Check session without updating internal state (for polling) */ /** Check session without updating internal state (for polling) */
checkSessionOnce(): Observable<AuthSession | null> { checkSessionOnce(webSessionID = this.getStoredWebSessionID()): Observable<AuthSession | null> {
return this.http.get<AuthSession>(`${this.apiUrl}/auth/session`, { if (!webSessionID) {
withCredentials: true return of(null);
}).pipe( }
return this.http.get<Record<string, unknown>>(
`${this.authApiUrl}/users/sessions/${encodeURIComponent(webSessionID)}`
).pipe(
map(response => this.normalizeWebSession(response, webSessionID)),
tap(session => { tap(session => {
if (session && session.active) { if (session?.active) {
this.sessionSignal.set(session); this.activateSession(session);
this.statusSignal.set('authenticated');
this.scheduleSessionRefresh(session.expiresAt);
} }
}), }),
catchError(() => of(null)) catchError(() => of(null))
@@ -78,19 +73,25 @@ export class AuthService {
/** /**
* Called after user completes Telegram login. * Called after user completes Telegram login.
* The callback URL from Telegram will hit our backend which sets the cookie.
* Then we re-check the session.
*/ */
onTelegramLoginComplete(): void { onTelegramLoginComplete(): void {
this.checkSession();
this.hideLogin(); this.hideLogin();
if (!this.isAuthenticated()) {
this.checkSession();
}
} }
/** Generate the Telegram login URL for bot-based auth */ /** Generate the Telegram login URL for bot-based auth */
getTelegramLoginUrl(): string { getTelegramLoginUrl(webSessionID = this.generateGuid()): string {
const botUsername = (environment as Record<string, unknown>)['telegramBot'] as string || 'DexarSupport_bot'; const botUsername = this.getTelegramBotUsername();
const callbackUrl = encodeURIComponent(`${this.apiUrl}/auth/telegram/callback`); return `https://t.me/${botUsername}?start=${encodeURIComponent(webSessionID)}`;
return `https://t.me/${botUsername}?start=auth_${callbackUrl}`; }
/** Generate a Telegram app deep link for mobile login without opening a browser tab. */
getTelegramAppLoginUrl(webSessionID: string): string {
const botUsername = this.getTelegramBotUsername();
return `tg://resolve?domain=${encodeURIComponent(botUsername)}&start=${encodeURIComponent(webSessionID)}`;
} }
/** Get QR code data URL for Telegram login */ /** Get QR code data URL for Telegram login */
@@ -98,34 +99,25 @@ export class AuthService {
return this.getTelegramLoginUrl(); return this.getTelegramLoginUrl();
} }
/** Create a one-time QR login token via backend */ /** Create a backend web session and return the Telegram start link for it. */
createQrToken(): Observable<{ token: string; url: string }> { createWebSession(): Observable<WebSessionStart> {
return this.http.post<{ token: string; url: string }>( const webSessionID = this.generateGuid();
`${this.apiUrl}/auth/qr/create`,
{},
{ withCredentials: true }
);
}
/** Poll the QR token status (pending → confirmed / expired) */ return this.http.post<Record<string, unknown>>(
pollQrToken(token: string): Observable<QrPollResponse> { `${this.authApiUrl}/users/sessions`,
return this.http.get<QrPollResponse>( { webSessionID },
`${this.apiUrl}/auth/qr/poll`, { headers: { WebSessionID: webSessionID } }
{ ).pipe(
params: { token }, map(response => {
withCredentials: true, const responseWebSessionID = this.extractSessionId(response, webSessionID);
} return {
webSessionID: responseWebSessionID,
url: this.getTelegramLoginUrl(responseWebSessionID),
};
})
); );
} }
/** 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);
@@ -138,17 +130,36 @@ export class AuthService {
/** Logout — clears session on backend and locally */ /** Logout — clears session on backend and locally */
logout(): void { logout(): void {
this.http.post(`${this.apiUrl}/auth/logout`, {}, { const webSessionID = this.sessionSignal()?.sessionId || this.getStoredWebSessionID();
withCredentials: true
if (!webSessionID) {
this.clearAuthState('unauthenticated');
return;
}
this.http.delete(`${this.authApiUrl}/users/sessions/${encodeURIComponent(webSessionID)}`, {
headers: { WebSessionID: webSessionID }
}).pipe( }).pipe(
catchError(() => of(null)) catchError(() => of(null))
).subscribe(() => { ).subscribe(() => {
this.sessionSignal.set(null); this.clearAuthState('unauthenticated');
this.statusSignal.set('unauthenticated');
this.clearSessionRefresh();
}); });
} }
private activateSession(session: AuthSession): void {
this.sessionSignal.set(session);
this.statusSignal.set('authenticated');
this.setStoredWebSessionID(session.sessionId);
this.scheduleSessionRefresh(session.expires);
}
private clearAuthState(status: AuthStatus): void {
this.sessionSignal.set(null);
this.statusSignal.set(status);
this.clearStoredWebSessionID();
this.clearSessionRefresh();
}
/** Schedule a session re-check before it expires */ /** Schedule a session re-check before it expires */
private scheduleSessionRefresh(expiresAt: string): void { private scheduleSessionRefresh(expiresAt: string): void {
this.clearSessionRefresh(); this.clearSessionRefresh();
@@ -156,7 +167,9 @@ export class AuthService {
const expiresMs = new Date(expiresAt).getTime(); const expiresMs = new Date(expiresAt).getTime();
const nowMs = Date.now(); const nowMs = Date.now();
// Re-check 60 seconds before expiry, minimum 30s from now // Re-check 60 seconds before expiry, minimum 30s from now
const refreshIn = Math.max(expiresMs - nowMs - 60_000, 30_000); const refreshIn = Number.isFinite(expiresMs)
? Math.max(expiresMs - nowMs - 60_000, 30_000)
: WEB_SESSION_COOKIE_MAX_AGE_SECONDS * 1000;
this.sessionCheckTimer = setTimeout(() => { this.sessionCheckTimer = setTimeout(() => {
this.checkSession(); this.checkSession();
@@ -169,4 +182,180 @@ export class AuthService {
this.sessionCheckTimer = undefined; this.sessionCheckTimer = undefined;
} }
} }
private normalizeWebSession(response: Record<string, unknown> | null, fallbackSessionId: string): AuthSession | null {
if (!response) {
return null;
}
const user = this.asRecord(this.readFirst(response, ['user', 'User', 'telegramUser', 'TelegramUser'])) ?? response;
const status = this.readFirst(response, [
'status',
'Status',
'active',
'Active',
'loggedIn',
'LoggedIn',
'isLoggedIn',
'IsLoggedIn',
'authenticated',
'Authenticated'
]);
const active = this.isActiveStatus(status);
const sessionId = this.extractSessionId(response, fallbackSessionId);
const username = this.readString(this.readFirst(user, ['username', 'Username']))
?? this.readString(this.readFirst(response, ['username', 'Username']));
const firstName = this.readString(this.readFirst(user, ['firstName', 'first_name', 'FirstName', 'First_name']));
const lastName = this.readString(this.readFirst(user, ['lastName', 'last_name', 'LastName', 'Last_name']));
const fullName = [firstName, lastName].filter(Boolean).join(' ');
const explicitDisplayName = this.readString(this.readFirst(response, ['displayName', 'DisplayName', 'name', 'Name']))
?? this.readString(this.readFirst(user, ['displayName', 'DisplayName', 'name', 'Name']))
const displayName = explicitDisplayName ?? username ?? (fullName || 'Telegram User');
const telegramUserId = this.readNumber(this.readFirst(user, ['userId','telegramUserId', 'telegramUserID', 'TelegramUserID', 'id', 'ID']))
?? this.readNumber(this.readFirst(response, ['userId', 'telegramUserId', 'telegramUserID', 'TelegramUserID', 'userID', 'UserID', 'UserId']))
?? null;
const expiresAt = this.readString(this.readFirst(response, ['expiresAt', 'ExpiresAt', 'expires', 'Expires']))
?? new Date(Date.now() + WEB_SESSION_COOKIE_MAX_AGE_SECONDS * 1000).toISOString();
return {
sessionId,
userId: telegramUserId,
username,
displayName,
active,
expires: expiresAt,
};
}
private extractSessionId(response: Record<string, unknown> | null, fallbackSessionId: string): string {
if (!response) {
return fallbackSessionId;
}
return this.readString(this.readFirst(response, [
'webSessionID',
'WebSessionID',
'webSessionId',
'sessionID',
'SessionID',
'sessionId',
'id',
'ID'
])) ?? fallbackSessionId;
}
private readFirst(source: Record<string, unknown>, keys: string[]): unknown {
for (const key of keys) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
return source[key];
}
}
return undefined;
}
private readString(value: unknown): string | null {
if (typeof value === 'string' && value.trim()) {
return value;
}
if (typeof value === 'number' || typeof value === 'bigint') {
return value.toString();
}
return null;
}
private readNumber(value: unknown): number | null {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string') {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
private asRecord(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, unknown>
: null;
}
private isActiveStatus(status: unknown): boolean {
if (status === true || status === 1) {
return true;
}
if (typeof status !== 'string') {
return false;
}
return ['true', '1', 'active', 'authenticated', 'confirmed', 'success', 'logged_in'].includes(status.toLowerCase());
}
private generateGuid(): string {
if (globalThis.crypto?.randomUUID) {
return globalThis.crypto.randomUUID();
}
const bytes = new Uint8Array(16);
if (globalThis.crypto?.getRandomValues) {
globalThis.crypto.getRandomValues(bytes);
} else {
for (let index = 0; index < bytes.length; index++) {
bytes[index] = Math.floor(Math.random() * 256);
}
}
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0'));
return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`;
}
private getStoredWebSessionID(): string | null {
if (typeof document === 'undefined') {
return null;
}
const cookie = document.cookie
.split('; ')
.find(row => row.startsWith(`${WEB_SESSION_COOKIE}=`));
if (!cookie) {
return null;
}
try {
return decodeURIComponent(cookie.substring(WEB_SESSION_COOKIE.length + 1));
} catch {
return null;
}
}
private setStoredWebSessionID(webSessionID: string): void {
if (typeof document === 'undefined') {
return;
}
const secure = typeof window !== 'undefined' && window.location.protocol === 'https:' ? '; Secure' : '';
document.cookie = `${WEB_SESSION_COOKIE}=${encodeURIComponent(webSessionID)}; Max-Age=${WEB_SESSION_COOKIE_MAX_AGE_SECONDS}; Path=/; SameSite=Lax${secure}`;
}
private clearStoredWebSessionID(): void {
if (typeof document === 'undefined') {
return;
}
document.cookie = `${WEB_SESSION_COOKIE}=; Max-Age=0; Path=/; SameSite=Lax`;
}
private getTelegramBotUsername(): string {
return (environment as Record<string, unknown>)['telegramBot'] as string || 'DexarSupport_bot';
}
} }

View File

@@ -1,7 +1,7 @@
import { Injectable, signal, computed, effect } from '@angular/core'; import { Injectable, signal, computed, effect, Injector } from '@angular/core';
import { ApiService } from './api.service'; import { DeliveryOption, CartItem } from '../models';
import { Item, CartItem } from '../models';
import { getDiscountedPrice } from '../utils/item.utils'; import { getDiscountedPrice } from '../utils/item.utils';
import { normalizeDeliveryOption, normalizeOptionalNumber } from '../utils/normalization.utils';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
import type { } from '../types/telegram.types'; import type { } from '../types/telegram.types';
@@ -28,8 +28,27 @@ export class CartService {
return total + (getDiscountedPrice(item) * item.quantity); return total + (getDiscountedPrice(item) * item.quantity);
}, 0); }, 0);
}); });
totalDeliveryPrice = computed(() => {
const items = this.cartItems();
if (!Array.isArray(items)) return 0;
return items.reduce((total, item) => {
return total + ((item.selectedDelivery?.deliveryPrice ?? 0) * item.quantity);
}, 0);
});
totalWithDelivery = computed(() => this.totalPrice() + this.totalDeliveryPrice());
allRequiredDeliveriesSelected = computed(() => {
const items = this.cartItems();
if (!Array.isArray(items)) return true;
return items.every(item => !this.itemRequiresDeliverySelection(item) || item.selectedDelivery != null);
});
hasDeliveryPrice = computed(() => {
const items = this.cartItems();
if (!Array.isArray(items)) return false;
return this.allRequiredDeliveriesSelected()
&& items.some(item => (item.deliveryOptions?.length ?? 0) > 0 || item.selectedDelivery != null);
});
constructor(private apiService: ApiService) { constructor(private injector: Injector) {
this.loadCart(); this.loadCart();
// Auto-save whenever cart changes (skip the initial empty state) // Auto-save whenever cart changes (skip the initial empty state)
@@ -41,6 +60,74 @@ export class CartService {
}); });
} }
private sameDeliveryOption(left: DeliveryOption, right: DeliveryOption): boolean {
return left.deliveryPrice === right.deliveryPrice
&& left.deliveryPlace === right.deliveryPlace
&& left.deliveryTime === right.deliveryTime;
}
private itemRequiresDeliverySelection(item: CartItem): boolean {
return item.deliveryMode !== 'digital'
&& (item.deliveryOptions?.length ?? 0) > 0
&& item.deliverySelectionRequired !== false;
}
private normalizeDeliveryState(item: CartItem): {
deliveryMode?: CartItem['deliveryMode'];
deliveryOptions: DeliveryOption[];
deliverySelectionRequired: boolean;
selectedDelivery: DeliveryOption | null;
} {
const normalizedOptions = Array.isArray(item.deliveryOptions)
? item.deliveryOptions
.map(option => normalizeDeliveryOption(option))
.filter((option): option is DeliveryOption => option !== null)
: [];
const legacyDeliveryPrice = normalizeOptionalNumber(item.deliveryPrice);
const deliveryOptions = normalizedOptions.length > 0
? normalizedOptions
: legacyDeliveryPrice !== undefined
? [{ deliveryPrice: legacyDeliveryPrice, deliveryPlace: '', deliveryTime: '' }]
: [];
const deliveryMode = item.deliveryMode === 'digital'
? 'digital'
: deliveryOptions.length > 0
? 'selectable'
: undefined;
const deliverySelectionRequired = deliveryOptions.length > 0
? item.deliverySelectionRequired !== false && normalizedOptions.length > 0
: false;
const selectedDelivery = normalizeDeliveryOption(item.selectedDelivery)
?? (deliveryOptions.length === 1 && !deliverySelectionRequired ? deliveryOptions[0] : null);
const matchedSelection = selectedDelivery && deliveryOptions.length > 0
? deliveryOptions.find(option => this.sameDeliveryOption(option, selectedDelivery)) ?? selectedDelivery
: selectedDelivery;
return {
deliveryMode,
deliveryOptions,
deliverySelectionRequired,
selectedDelivery: matchedSelection,
};
}
private normalizeCartItem(item: CartItem): CartItem {
const { deliveryPrice, ...rest } = item;
const deliveryState = this.normalizeDeliveryState(item);
const hasDeliveryState = !!deliveryState.deliveryMode
|| deliveryState.deliveryOptions.length > 0
|| deliveryState.selectedDelivery != null;
return {
...rest,
quantity: item.quantity || 1,
...(hasDeliveryState ? { deliverySelectionRequired: deliveryState.deliverySelectionRequired } : {}),
...(deliveryState.deliveryMode ? { deliveryMode: deliveryState.deliveryMode } : {}),
...(deliveryState.deliveryOptions.length > 0 ? { deliveryOptions: deliveryState.deliveryOptions } : {}),
...(deliveryState.selectedDelivery ? { selectedDelivery: deliveryState.selectedDelivery } : {}),
};
}
private saveToStorage(items: CartItem[]): void { private saveToStorage(items: CartItem[]): void {
const data = JSON.stringify(items); const data = JSON.stringify(items);
@@ -90,10 +177,7 @@ export class CartService {
try { try {
const items = JSON.parse(json); const items = JSON.parse(json);
if (Array.isArray(items)) { if (Array.isArray(items)) {
this.cartItems.set(items.map(item => ({ this.cartItems.set(items.map(item => this.normalizeCartItem(item)));
...item,
quantity: item.quantity || 1
})));
return true; return true;
} }
} catch (err) { } catch (err) {
@@ -116,16 +200,17 @@ export class CartService {
} else { } else {
// Get item details from API and add to cart // Get item details from API and add to cart
this.addingItems.add(itemID); this.addingItems.add(itemID);
this.apiService.getItem(itemID).subscribe({ import('./api.service').then(({ ApiService }) => {
this.injector.get(ApiService).getItem(itemID).subscribe({
next: (item) => { next: (item) => {
const cartItem: CartItem = { const cartItem = this.normalizeCartItem({
...item, ...item,
quantity, quantity,
...(variant?.colour != null && { colour: variant.colour }), ...(variant?.colour != null && { colour: variant.colour }),
...(variant?.size != null && { size: variant.size }), ...(variant?.size != null && { size: variant.size }),
...(variant?.price != null && { price: variant.price }), ...(variant?.price != null && { price: variant.price }),
...(variant?.currency != null && { currency: variant.currency }), ...(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);
}, },
@@ -134,6 +219,10 @@ export class CartService {
this.addingItems.delete(itemID); this.addingItems.delete(itemID);
} }
}); });
}).catch((err) => {
console.error('Error loading API service:', err);
this.addingItems.delete(itemID);
});
} }
} }
@@ -150,6 +239,22 @@ export class CartService {
this.cartItems.set(updatedItems); this.cartItems.set(updatedItems);
} }
setSelectedDelivery(itemID: number, selectedDelivery: DeliveryOption | null): void {
const normalizedSelection = normalizeDeliveryOption(selectedDelivery);
const updatedItems = this.cartItems().map(item => {
if (item.itemID !== itemID) {
return item;
}
return this.normalizeCartItem({
...item,
selectedDelivery: normalizedSelection,
});
});
this.cartItems.set(updatedItems);
}
removeItems(itemIDs: number[]): void { removeItems(itemIDs: number[]): void {
const currentItems = this.cartItems(); const currentItems = this.cartItems();
const updatedItems = currentItems.filter(item => !itemIDs.includes(item.itemID)); const updatedItems = currentItems.filter(item => !itemIDs.includes(item.itemID));

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