Compare commits
16
Commits
v0.5.0
...
fa88b2ff84
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa88b2ff84 | ||
|
|
82b4bf7b37 | ||
|
|
e56f0f54d3 | ||
|
|
10b05ffd45 | ||
|
|
5dd97a03a4 | ||
|
|
8f8eeb958c | ||
|
|
b3fd3219d7 | ||
|
|
fadaed985e | ||
|
|
4a812f60b3 | ||
|
|
75f95ee2f8 | ||
|
|
67b2fe7688 | ||
|
|
76ba1948e7 | ||
|
|
96ac249acf | ||
|
|
f0165aaa2e | ||
|
|
05096522e0 | ||
|
|
a52fa0f28e |
@@ -4,6 +4,15 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="icon" href="/images/logo.png">
|
<link rel="icon" href="/images/logo.png">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="Безопасный, быстрый и анонимный VPN-сервис для вашего ПК и смартфона. Защитите свои данные от посторонних глаз уже сегодня.">
|
||||||
|
<meta name="keywords" content="vpn, сервис, анонимность, безопасность, шифрование, купить vpn">
|
||||||
|
<meta property="og:title" content="uVPN">
|
||||||
|
<meta property="og:description" content="Безопасный, быстрый и анонимный VPN-сервис для вашего ПК и смартфона. Защитите свои данные от посторонних глаз уже сегодня.">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="https://uvpn.shop/">
|
||||||
|
<meta property="og:site_name" content="uVPN">
|
||||||
|
<meta property="telegram:channel" content="@uvpn_shop">
|
||||||
|
<meta property="tg:site_verification" content="g7j8/rPFXfhyrq5q0QQV7EsYWv4=">
|
||||||
<title>uVPN</title>
|
<title>uVPN</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Generated
+153
-2
@@ -1,17 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "uvpn.shop",
|
"name": "uvpn.shop",
|
||||||
"version": "0.3.1",
|
"version": "0.5.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "uvpn.shop",
|
"name": "uvpn.shop",
|
||||||
"version": "0.3.1",
|
"version": "0.5.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lucide/vue": "^1.16.0",
|
"@lucide/vue": "^1.16.0",
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"@tresjs/cientos": "^5.7.1",
|
"@tresjs/cientos": "^5.7.1",
|
||||||
"@tresjs/core": "^5.8.1",
|
"@tresjs/core": "^5.8.1",
|
||||||
|
"@tw-labs/countries": "^0.0.2",
|
||||||
|
"@vueuse/head": "^0.9.8",
|
||||||
"marked": "^18.0.5",
|
"marked": "^18.0.5",
|
||||||
"swiper": "^12.1.4",
|
"swiper": "^12.1.4",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
@@ -24,6 +26,7 @@
|
|||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@vitejs/plugin-vue": "^6.0.6",
|
"@vitejs/plugin-vue": "^6.0.6",
|
||||||
"vite": "^8.0.8",
|
"vite": "^8.0.8",
|
||||||
|
"vite-plugin-sitemap": "^0.8.2",
|
||||||
"vite-plugin-vue-devtools": "^8.1.1"
|
"vite-plugin-vue-devtools": "^8.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1200,6 +1203,20 @@
|
|||||||
"vue": ">=3.4"
|
"vue": ">=3.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tw-labs/countries": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tw-labs/countries/-/countries-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-mnCpRLhpMPTCMTnV18nKjLrdZ+4bUZSxdOgFWqdsZSM62RwPk1ddQCTN2XtRAR2zcLzlX91iTg2kSJ006qmXlA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tweenjs/tween.js": {
|
"node_modules/@tweenjs/tween.js": {
|
||||||
"version": "23.1.3",
|
"version": "23.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||||
@@ -1520,6 +1537,58 @@
|
|||||||
"vue": "^3.5.0"
|
"vue": "^3.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vueuse/head": {
|
||||||
|
"version": "0.9.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/head/-/head-0.9.8.tgz",
|
||||||
|
"integrity": "sha512-zt8+JksoVFKxRvmABlaUHA62w+8nOcD8cJnaJ0+SHcr6xaIP3GXgh7/n2TzUoWw4l3d9UxRNs+tapgHdsQ7RbA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vueuse/shared": "^9.3.0",
|
||||||
|
"@zhead/schema": "^0.8.5",
|
||||||
|
"@zhead/schema-vue": "^0.8.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": ">=2.7 || >=3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/head/node_modules/@vueuse/shared": {
|
||||||
|
"version": "9.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
|
||||||
|
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vue-demi": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/head/node_modules/@vueuse/shared/node_modules/vue-demi": {
|
||||||
|
"version": "0.14.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||||
|
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vueuse/metadata": {
|
"node_modules/@vueuse/metadata": {
|
||||||
"version": "13.9.0",
|
"version": "13.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.9.0.tgz",
|
||||||
@@ -1541,6 +1610,82 @@
|
|||||||
"vue": "^3.5.0"
|
"vue": "^3.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@zhead/schema": {
|
||||||
|
"version": "0.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zhead/schema/-/schema-0.8.5.tgz",
|
||||||
|
"integrity": "sha512-1S3Otr2zpl1zwP72dNseVXQNG9tnTQ6hHUEUYwINvBjRj6bHcUwdE+Itc9OLxnGAJT/7p8P7GHGo5sshXJNJsA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/harlan-zw"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@zhead/schema-raw": {
|
||||||
|
"version": "0.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zhead/schema-raw/-/schema-raw-0.8.5.tgz",
|
||||||
|
"integrity": "sha512-Aq+9mksf5zbtj7HYluT6PVyfpQ6z7mja9MzjFxg76Vt+Q9i0oL1XN6ZYaCXImWRafwbyAxjFQ5aUCVyFn79OpA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@zhead/schema": "0.8.5"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/harlan-zw"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@zhead/schema-vue": {
|
||||||
|
"version": "0.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zhead/schema-vue/-/schema-vue-0.8.5.tgz",
|
||||||
|
"integrity": "sha512-6aXjYy3fZVeYBLrHcJQqzqwzC/2tafRO5UxZEgBHnryRnzeLNZV6nTptDvIPWiJObMoJTK21vbg3gkfLNQg84g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vueuse/shared": "^9.2.0",
|
||||||
|
"@zhead/schema": "0.8.5",
|
||||||
|
"@zhead/schema-raw": "0.8.5"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/harlan-zw"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": ">=2.7 || >=3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@zhead/schema-vue/node_modules/@vueuse/shared": {
|
||||||
|
"version": "9.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
|
||||||
|
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vue-demi": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@zhead/schema-vue/node_modules/@vueuse/shared/node_modules/vue-demi": {
|
||||||
|
"version": "0.14.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||||
|
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansis": {
|
"node_modules/ansis": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.0.tgz",
|
||||||
@@ -2951,6 +3096,12 @@
|
|||||||
"vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0"
|
"vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite-plugin-sitemap": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-plugin-sitemap/-/vite-plugin-sitemap-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-bqIw6NVOXg6je81lzX8Lm0vjf8/QSAp8di8fYQzZ3ZdVicOm8+6idBGALJiy1R1FiXNIK8rgORO6HBqXyHW+iQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/vite-plugin-vue-devtools": {
|
"node_modules/vite-plugin-vue-devtools": {
|
||||||
"version": "8.1.2",
|
"version": "8.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.1.2.tgz",
|
||||||
|
|||||||
+6
-2
@@ -1,18 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "uvpn.shop",
|
"name": "uvpn.shop",
|
||||||
"version": "0.4.1",
|
"version": "0.6.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"postbuild": "scp -r dist root@uvpn.shop:/opt/uvpn.shop"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lucide/vue": "^1.16.0",
|
"@lucide/vue": "^1.16.0",
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"@tresjs/cientos": "^5.7.1",
|
"@tresjs/cientos": "^5.7.1",
|
||||||
"@tresjs/core": "^5.8.1",
|
"@tresjs/core": "^5.8.1",
|
||||||
|
"@tw-labs/countries": "^0.0.2",
|
||||||
|
"@vueuse/head": "^0.9.8",
|
||||||
"marked": "^18.0.5",
|
"marked": "^18.0.5",
|
||||||
"swiper": "^12.1.4",
|
"swiper": "^12.1.4",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
@@ -25,6 +28,7 @@
|
|||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@vitejs/plugin-vue": "^6.0.6",
|
"@vitejs/plugin-vue": "^6.0.6",
|
||||||
"vite": "^8.0.8",
|
"vite": "^8.0.8",
|
||||||
|
"vite-plugin-sitemap": "^0.8.2",
|
||||||
"vite-plugin-vue-devtools": "^8.1.1"
|
"vite-plugin-vue-devtools": "^8.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -105,3 +105,5 @@
|
|||||||
10.1. По всем вопросам Пользователь может обратиться в службу поддержки через форму в самом боте.
|
10.1. По всем вопросам Пользователь может обратиться в службу поддержки через форму в самом боте.
|
||||||
|
|
||||||
**Используя Сервис (в том числе запуская бота и/или вводя команду /start), Пользователь подтверждает, что ознакомлен с настоящим Соглашением и принимает его условия в полном объёме.**
|
**Используя Сервис (в том числе запуская бота и/или вводя команду /start), Пользователь подтверждает, что ознакомлен с настоящим Соглашением и принимает его условия в полном объёме.**
|
||||||
|
|
||||||
|
ИНН: 973300277093
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
* идентификаторы аккаунта (логин, ID, никнейм и т.п.);
|
* идентификаторы аккаунта (логин, ID, никнейм и т.п.);
|
||||||
* техническую информацию (IP-адрес, данные о браузере, устройстве и операционной системе);
|
* техническую информацию (IP-адрес, данные о браузере, устройстве и операционной системе);
|
||||||
* историю взаимодействий с Сервисом.
|
* историю взаимодействий с Сервисом.
|
||||||
* 2.2. Сервис не требует от Пользователя предоставления паспортных данных, документов, фотографий или другой личной информации, кроме минимально необходимой для работы.
|
|
||||||
|
2.2. Сервис не требует от Пользователя предоставления паспортных данных, документов, фотографий или другой личной информации, кроме минимально необходимой для работы.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,11 @@
|
|||||||
Попробовать бесплатно
|
Попробовать бесплатно
|
||||||
</a>
|
</a>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<IpSecurity />
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// Скрипты больше не нужны, всю магию сделает роутер!
|
import IpSecurity from './IpSecurity.vue';
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="ipData" class="bg-gray-50/80 dark:bg-gray-800/30 border-b border-gray-200 dark:border-gray-700 text-sm backdrop-blur-sm">
|
||||||
|
<div class="container mx-auto px-6 py-2 md:px-12">
|
||||||
|
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||||
|
|
||||||
|
<!-- IP и статус защиты -->
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400 text-xs">🌐 Ваш IP:</span>
|
||||||
|
<code class="font-mono text-xs md:text-sm font-medium bg-white dark:bg-gray-900 px-2 py-0.5 rounded border border-gray-200 dark:border-gray-700">
|
||||||
|
{{ ipData.ip }}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-2 text-xs px-2 py-0.5 rounded-full font-medium"
|
||||||
|
:class="isProtected
|
||||||
|
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
||||||
|
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400'"
|
||||||
|
>
|
||||||
|
<span class="relative flex h-1.5 w-1.5">
|
||||||
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full opacity-75"
|
||||||
|
:class="isProtected ? 'bg-green-500' : 'bg-amber-500'"></span>
|
||||||
|
<span class="relative inline-flex rounded-full h-1.5 w-1.5"
|
||||||
|
:class="isProtected ? 'bg-green-600' : 'bg-amber-600'"></span>
|
||||||
|
</span>
|
||||||
|
{{ isProtected ? 'Защищено' : 'Не защищено' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Локация с флагом -->
|
||||||
|
<div class="flex items-center justify-center gap-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<div class="flex h-4 items-center">
|
||||||
|
<img
|
||||||
|
v-if="flagUrl"
|
||||||
|
:src="flagUrl"
|
||||||
|
:alt="countryCode"
|
||||||
|
class="w-4 h-3 object-cover"
|
||||||
|
@error="handleFlagError"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="ipData.location?.city">
|
||||||
|
<span class="hidden sm:inline">{{ ipData.location.city }}</span>
|
||||||
|
<span class="text-gray-400 hidden sm:inline">•</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<span v-if="ipData.location?.country" class="text-gray-600 dark:text-gray-300 font-medium hidden sm:inline">
|
||||||
|
{{ ipData.location.country }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<template v-if="ipData.company?.name">
|
||||||
|
<span class="text-gray-400 hidden md:inline">•</span>
|
||||||
|
<span class="hidden md:inline text-gray-400">
|
||||||
|
{{ truncateText(ipData.company.name, 30) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Лоадер -->
|
||||||
|
<div v-else-if="loading" class="bg-gray-50/80 dark:bg-gray-800/30 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="container mx-auto px-6 py-2 md:px-12">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="h-5 w-28 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||||
|
<div class="h-5 w-16 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ошибка -->
|
||||||
|
<div v-else-if="error" class="bg-amber-50 dark:bg-amber-900/10 border-b border-amber-200 dark:border-amber-800/50">
|
||||||
|
<div class="container mx-auto px-6 py-1.5 md:px-12">
|
||||||
|
<div class="flex items-center gap-2 text-xs text-amber-700 dark:text-amber-400">
|
||||||
|
<span>⚠️</span>
|
||||||
|
<span>Не удалось определить IP</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { getAllCountries, withFlags } from '@tw-labs/countries'
|
||||||
|
|
||||||
|
const ipData = ref(null)
|
||||||
|
const loading = ref(true)
|
||||||
|
const error = ref(false)
|
||||||
|
const flagError = ref(false)
|
||||||
|
|
||||||
|
// Загружаем список стран один раз
|
||||||
|
const countriesList = getAllCountries()
|
||||||
|
|
||||||
|
// Функция поиска страны по названию
|
||||||
|
function findCountryByName(countryName) {
|
||||||
|
if (!countryName) return null
|
||||||
|
|
||||||
|
// Точное совпадение
|
||||||
|
const found = countriesList.find(
|
||||||
|
c => c.label.toLowerCase() === countryName.toLowerCase()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (found) return found
|
||||||
|
|
||||||
|
// Частичное совпадение (для "Russian Federation" vs "Russia" и т.п.)
|
||||||
|
const partial = countriesList.find(
|
||||||
|
c => countryName.toLowerCase().includes(c.label.toLowerCase()) ||
|
||||||
|
c.label.toLowerCase().includes(countryName.toLowerCase())
|
||||||
|
)
|
||||||
|
|
||||||
|
return partial || null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchIp() {
|
||||||
|
loading.value = true
|
||||||
|
error.value = false
|
||||||
|
flagError.value = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/v1/ip')
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
// Находим код страны по названию
|
||||||
|
const countryName = data.location?.country
|
||||||
|
const foundCountry = findCountryByName(countryName)
|
||||||
|
const countryCode = foundCountry?.code || null
|
||||||
|
|
||||||
|
// Добавляем флаговые данные через withFlags
|
||||||
|
let countryWithFlags = null
|
||||||
|
if (foundCountry) {
|
||||||
|
countryWithFlags = withFlags(foundCountry)
|
||||||
|
}
|
||||||
|
|
||||||
|
ipData.value = {
|
||||||
|
...data,
|
||||||
|
location: {
|
||||||
|
...data.location,
|
||||||
|
computed_country_code: countryCode
|
||||||
|
},
|
||||||
|
_flags: countryWithFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('IP fetch error:', err)
|
||||||
|
error.value = true
|
||||||
|
ipData.value = null
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isProtected = computed(() => {
|
||||||
|
if (!ipData.value) return false
|
||||||
|
return ipData.value.security?.vpn || ipData.value.security?.proxy || ipData.value.security?.tor || ipData.value.security?.hosting
|
||||||
|
})
|
||||||
|
|
||||||
|
// Код страны (вычисленный)
|
||||||
|
const countryCode = computed(() => {
|
||||||
|
return ipData.value?.location?.computed_country_code || null
|
||||||
|
})
|
||||||
|
|
||||||
|
// URL флага
|
||||||
|
const flagUrl = computed(() => {
|
||||||
|
// Сначала пробуем взять SVG из withFlags
|
||||||
|
if (ipData.value?._flags?.flagSvg && !flagError.value) {
|
||||||
|
return ipData.value._flags.flagSvg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если нет — формируем сами через flagcdn
|
||||||
|
const code = countryCode.value
|
||||||
|
if (!code || flagError.value) return null
|
||||||
|
|
||||||
|
return `https://flagcdn.com/${code.toLowerCase()}.svg`
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleFlagError() {
|
||||||
|
flagError.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateText(text, maxLength) {
|
||||||
|
if (!text) return ''
|
||||||
|
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchIp()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import { createHead } from '@vueuse/head'
|
||||||
import './assets/styles.css'
|
import './assets/styles.css'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
const head = createHead()
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
app.use(head)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
+21
-2
@@ -26,8 +26,27 @@ const router = createRouter({
|
|||||||
},
|
},
|
||||||
|
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', name: 'home', component: HomeView },
|
{
|
||||||
{ path: '/docs/:docName', name: 'documents', component: DocViewer, props: true }
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: HomeView,
|
||||||
|
meta: {
|
||||||
|
title: 'uVPN',
|
||||||
|
description: 'Безопасный, быстрый и анонимный VPN-сервис для вашего ПК и смартфона. Защитите свои данные от посторонних глаз уже сегодня.',
|
||||||
|
keywords: 'vpn, сервис, анонимность, безопасность, шифрование, купить vpn'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/docs/:docName',
|
||||||
|
name: 'documents',
|
||||||
|
component: DocViewer,
|
||||||
|
props: true,
|
||||||
|
meta: {
|
||||||
|
title: 'Юридические сведения',
|
||||||
|
description: 'Пользовательское соглашение и политика конфиденциальности сервиса uVPN.',
|
||||||
|
keywords: 'vpn, пользовательское соглашение, политика конфиденциальности, документы'
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -18,4 +18,52 @@ import PricingSection from '../components/PricingSection.vue'
|
|||||||
import StepsSection from '../components/StepsSection.vue'
|
import StepsSection from '../components/StepsSection.vue'
|
||||||
import TestimonialsSection from '../components/TestimonialsSection.vue'
|
import TestimonialsSection from '../components/TestimonialsSection.vue'
|
||||||
import FaqSection from '../components/FaqSection.vue'
|
import FaqSection from '../components/FaqSection.vue'
|
||||||
|
|
||||||
|
// import { useHead } from '@vueuse/head';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
// useHead({
|
||||||
|
// title: route.meta.title,
|
||||||
|
// meta: [
|
||||||
|
// {
|
||||||
|
// name: 'description',
|
||||||
|
// content: route.meta.description
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'keywords',
|
||||||
|
// content: route.meta.keywords
|
||||||
|
// },
|
||||||
|
// // Open Graph теги для соцсетей
|
||||||
|
// {
|
||||||
|
// property: 'og:title',
|
||||||
|
// content: route.meta.title
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// property: 'og:description',
|
||||||
|
// content: route.meta.description
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// property: 'og:type',
|
||||||
|
// content: 'website'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// property: 'og:url',
|
||||||
|
// content: window.location.href // Здесь можно также задать каноничный URL
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// property: 'og:site_name',
|
||||||
|
// content: route.meta.title
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// property: 'telegram:channel',
|
||||||
|
// content: '@uvpn_shop'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// property: 'tg:site_verification',
|
||||||
|
// content: 'g7j8/rPFXfhyrq5q0QQV7EsYWv4='
|
||||||
|
// },
|
||||||
|
// ]
|
||||||
|
// });
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { fileURLToPath, URL } from 'node:url'
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import { default as Sitemap } from 'vite-plugin-sitemap';
|
||||||
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
@@ -10,6 +11,17 @@ export default defineConfig({
|
|||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
|
Sitemap({
|
||||||
|
hostname: 'https://uvpn.shop',
|
||||||
|
// Плагин сам прочитает маршруты из вашего router, но если нужно указать вручную:
|
||||||
|
// routes: ['/', '/docs'], // можно опустить — он определит автоматически
|
||||||
|
// Если у вас динамические маршруты (например, /docs/:docName), их нужно добавить отдельно:
|
||||||
|
dynamicRoutes: ['/docs/policy', '/docs/agreement'], // сюда перечислите все возможные динамические URL
|
||||||
|
// Настройки changefreq, priority и т.д.:
|
||||||
|
outDir: 'dist', // папка сборки
|
||||||
|
changefreq: 'weekly',
|
||||||
|
priority: 0.8
|
||||||
|
})
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
Reference in New Issue
Block a user