chore: add ESLint + Prettier, fix all lint errors
- Install eslint, typescript-eslint, eslint-plugin-astro, prettier, prettier-plugin-astro, globals - Add flat eslint.config.mjs with recommended rules for JS, TS, Astro - Add .prettierrc with astro plugin - Add lint, lint:fix, format, format:check scripts - Fix prefer-const, no-unused-vars, no-useless-assignment across codebase - Fix AdBanner.astro JSX fragment for Prettier compatibility - Add eslint-disable around Yandex/Top.Mail.Ru inline scripts - Auto-format entire codebase with Prettier
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
.astro/
|
||||||
|
package-lock.json
|
||||||
|
CLAUDE.md
|
||||||
|
.prettierignore
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"plugins": ["prettier-plugin-astro"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.astro",
|
||||||
|
"options": {
|
||||||
|
"parser": "astro"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
import astro from "eslint-plugin-astro";
|
||||||
|
import globals from "globals";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{ ignores: ["dist/**", ".astro/**", "node_modules/**", "src/env.d.ts"] },
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
...astro.configs.recommended,
|
||||||
|
{
|
||||||
|
files: ["public/sw.js"],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.serviceworker,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
Generated
+1391
File diff suppressed because it is too large
Load Diff
+14
-1
@@ -8,7 +8,11 @@
|
|||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"test:coverage": "vitest run --coverage"
|
"test:coverage": "vitest run --coverage",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/sitemap": "^3.2.1",
|
"@astrojs/sitemap": "^3.2.1",
|
||||||
@@ -17,7 +21,16 @@
|
|||||||
"tailwindcss": "^4.0.0"
|
"tailwindcss": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.59.2",
|
||||||
|
"@typescript-eslint/parser": "^8.59.2",
|
||||||
"@vitest/coverage-v8": "^4.1.5",
|
"@vitest/coverage-v8": "^4.1.5",
|
||||||
|
"eslint": "^10.3.0",
|
||||||
|
"eslint-plugin-astro": "^1.7.0",
|
||||||
|
"globals": "^17.6.0",
|
||||||
|
"prettier": "^3.8.3",
|
||||||
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
|
"typescript-eslint": "^8.59.2",
|
||||||
"vitest": "^4.1.5"
|
"vitest": "^4.1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-8
@@ -1,16 +1,16 @@
|
|||||||
const CACHE_NAME = 'randify-v1';
|
const CACHE_NAME = "randify-v1";
|
||||||
|
|
||||||
self.addEventListener('install', (event) => {
|
self.addEventListener("install", (event) => {
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('activate', (event) => {
|
self.addEventListener("activate", (event) => {
|
||||||
event.waitUntil(self.clients.claim());
|
event.waitUntil(self.clients.claim());
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('fetch', (event) => {
|
self.addEventListener("fetch", (event) => {
|
||||||
const { request } = event;
|
const { request } = event;
|
||||||
const isNavigate = request.mode === 'navigate';
|
const isNavigate = request.mode === "navigate";
|
||||||
const isSameOrigin = new URL(request.url).origin === self.location.origin;
|
const isSameOrigin = new URL(request.url).origin === self.location.origin;
|
||||||
|
|
||||||
if (!isSameOrigin) {
|
if (!isSameOrigin) {
|
||||||
@@ -29,7 +29,7 @@ self.addEventListener('fetch', (event) => {
|
|||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
})
|
})
|
||||||
.catch(() => caches.match(request))
|
.catch(() => caches.match(request)),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Cache-first for static assets (JS, CSS, SVG, fonts, etc.)
|
// Cache-first for static assets (JS, CSS, SVG, fonts, etc.)
|
||||||
@@ -39,14 +39,18 @@ self.addEventListener('fetch', (event) => {
|
|||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
return fetch(request).then((response) => {
|
return fetch(request).then((response) => {
|
||||||
if (!response || response.status !== 200 || response.type !== 'basic') {
|
if (
|
||||||
|
!response ||
|
||||||
|
response.status !== 200 ||
|
||||||
|
response.type !== "basic"
|
||||||
|
) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
const clone = response.clone();
|
const clone = response.clone();
|
||||||
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
|
caches.open(CACHE_NAME).then((cache) => cache.put(request, clone));
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
</head>
|
</head>
|
||||||
<body>Verification: dd9dad039a139021</body>
|
<body>
|
||||||
|
Verification: dd9dad039a139021
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
---
|
---
|
||||||
interface Props {
|
interface Props {
|
||||||
size: 'leaderboard' | 'rectangle' | 'tile';
|
size: "leaderboard" | "rectangle" | "tile";
|
||||||
}
|
}
|
||||||
|
|
||||||
const { size } = Astro.props;
|
const { size } = Astro.props;
|
||||||
|
|
||||||
const ads = [
|
const ads = [
|
||||||
{
|
{
|
||||||
href: 'https://redirect.appmetrica.yandex.com/serve/29835370459904648?clid=15006577&appmetrica_js_redirect=0',
|
href: "https://redirect.appmetrica.yandex.com/serve/29835370459904648?clid=15006577&appmetrica_js_redirect=0",
|
||||||
label: 'Яндекс с Алисой — скачать бесплатно',
|
label: "Яндекс с Алисой — скачать бесплатно",
|
||||||
sublabel: 'Голосовой ИИ-помощник для Android и iOS',
|
sublabel: "Голосовой ИИ-помощник для Android и iOS",
|
||||||
cta: 'Установить',
|
cta: "Установить",
|
||||||
ctaMobile: 'Установить бесплатно',
|
ctaMobile: "Установить бесплатно",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: 'https://yandex.ru/project/browser/bonus/multioffer/affiliate_4prod?source=pWRP8eS1VsC2X59560&partner_string=P89XvN11U6RuE47077&cliddbro=15006579&clidmbro=15006578&cliddefault=15006570&clidpp=15006567',
|
href: "https://yandex.ru/project/browser/bonus/multioffer/affiliate_4prod?source=pWRP8eS1VsC2X59560&partner_string=P89XvN11U6RuE47077&cliddbro=15006579&clidmbro=15006578&cliddefault=15006570&clidpp=15006567",
|
||||||
label: 'Яндекс Браузер — быстрый и безопасный',
|
label: "Яндекс Браузер — быстрый и безопасный",
|
||||||
sublabel: 'Браузер с Алисой и защитой от угроз',
|
sublabel: "Браузер с Алисой и защитой от угроз",
|
||||||
cta: 'Скачать',
|
cta: "Скачать",
|
||||||
ctaMobile: 'Скачать бесплатно',
|
ctaMobile: "Скачать бесплатно",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const ad = ads[Math.floor(Math.random() * ads.length)];
|
const ad = ads[Math.floor(Math.random() * ads.length)];
|
||||||
|
|
||||||
const specs = {
|
const specs = {
|
||||||
leaderboard: { class: 'w-full h-[90px]', horizontal: true },
|
leaderboard: { class: "w-full h-[90px]", horizontal: true },
|
||||||
rectangle: { class: 'w-full max-w-[300px] h-[250px]', horizontal: false },
|
rectangle: { class: "w-full max-w-[300px] h-[250px]", horizontal: false },
|
||||||
tile: { class: 'w-full h-[250px]', horizontal: false },
|
tile: { class: "w-full h-[250px]", horizontal: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
const { class: sizeClass, horizontal } = specs[size];
|
const { class: sizeClass, horizontal } = specs[size];
|
||||||
@@ -38,16 +38,30 @@ const { class: sizeClass, horizontal } = specs[size];
|
|||||||
href={ad.href}
|
href={ad.href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener sponsored"
|
rel="noopener sponsored"
|
||||||
class={`${sizeClass} flex ${horizontal ? 'flex-row items-center gap-4 px-6' : 'flex-col items-center justify-center gap-3 px-4'} rounded-xl border border-zinc-800 bg-zinc-900/50 hover:border-[#534AB7]/50 hover:bg-zinc-900 transition-colors duration-200 group`}
|
class={`${sizeClass} flex ${horizontal ? "flex-row items-center gap-4 px-6" : "flex-col items-center justify-center gap-3 px-4"} rounded-xl border border-zinc-800 bg-zinc-900/50 hover:border-[#534AB7]/50 hover:bg-zinc-900 transition-colors duration-200 group`}
|
||||||
>
|
>
|
||||||
<svg class="shrink-0 text-[#FC3F1D]" width={horizontal ? '32' : '40'} height={horizontal ? '32' : '40'} viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
<svg
|
||||||
<circle cx="20" cy="20" r="20" fill="currentColor"/>
|
class="shrink-0 text-[#FC3F1D]"
|
||||||
<path d="M22.44 10H19.1c-3.78 0-6.1 2.02-6.1 5.32 0 2.66 1.26 4.24 3.64 5.8L13.5 30h3.38l3.4-8.38-.96-.58c-1.84-1.12-2.72-2.08-2.72-3.92 0-1.94 1.3-3.08 3.44-3.08h1.4V30h3V10z" fill="white"/>
|
width={horizontal ? "32" : "40"}
|
||||||
|
height={horizontal ? "32" : "40"}
|
||||||
|
viewBox="0 0 40 40"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<circle cx="20" cy="20" r="20" fill="currentColor"></circle>
|
||||||
|
<path
|
||||||
|
d="M22.44 10H19.1c-3.78 0-6.1 2.02-6.1 5.32 0 2.66 1.26 4.24 3.64 5.8L13.5 30h3.38l3.4-8.38-.96-.58c-1.84-1.12-2.72-2.08-2.72-3.92 0-1.94 1.3-3.08 3.44-3.08h1.4V30h3V10z"
|
||||||
|
fill="white"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
{horizontal ? (
|
{
|
||||||
|
horizontal ? (
|
||||||
|
<>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="text-xs font-medium text-zinc-500 uppercase tracking-widest mb-0.5">Реклама</div>
|
<div class="text-xs font-medium text-zinc-500 uppercase tracking-widest mb-0.5">
|
||||||
|
Реклама
|
||||||
|
</div>
|
||||||
<div class="text-sm font-semibold text-zinc-100 group-hover:text-[#534AB7] transition-colors leading-tight">
|
<div class="text-sm font-semibold text-zinc-100 group-hover:text-[#534AB7] transition-colors leading-tight">
|
||||||
{ad.label}
|
{ad.label}
|
||||||
</div>
|
</div>
|
||||||
@@ -56,10 +70,13 @@ const { class: sizeClass, horizontal } = specs[size];
|
|||||||
<span class="shrink-0 text-xs font-semibold text-white bg-[#FC3F1D] px-3 py-1.5 rounded-lg group-hover:bg-[#e03518] transition-colors whitespace-nowrap">
|
<span class="shrink-0 text-xs font-semibold text-white bg-[#FC3F1D] px-3 py-1.5 rounded-lg group-hover:bg-[#e03518] transition-colors whitespace-nowrap">
|
||||||
{ad.cta}
|
{ad.cta}
|
||||||
</span>
|
</span>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="text-xs font-medium text-zinc-500 uppercase tracking-widest mb-2">Реклама</div>
|
<div class="text-xs font-medium text-zinc-500 uppercase tracking-widest mb-2">
|
||||||
|
Реклама
|
||||||
|
</div>
|
||||||
<div class="text-base font-semibold text-zinc-100 group-hover:text-[#534AB7] transition-colors leading-snug">
|
<div class="text-base font-semibold text-zinc-100 group-hover:text-[#534AB7] transition-colors leading-snug">
|
||||||
{ad.label}
|
{ad.label}
|
||||||
</div>
|
</div>
|
||||||
@@ -69,6 +86,7 @@ const { class: sizeClass, horizontal } = specs[size];
|
|||||||
{ad.ctaMobile}
|
{ad.ctaMobile}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
</a>
|
</a>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const mailRuId = analytics.topMailRuId;
|
|||||||
|
|
||||||
<!-- Yandex.Metrika counter -->
|
<!-- Yandex.Metrika counter -->
|
||||||
<script type="text/javascript" define:vars={{ yandexId }}>
|
<script type="text/javascript" define:vars={{ yandexId }}>
|
||||||
|
/* eslint-disable */
|
||||||
(function (m, e, t, r, i, k, a) {
|
(function (m, e, t, r, i, k, a) {
|
||||||
m[i] =
|
m[i] =
|
||||||
m[i] ||
|
m[i] ||
|
||||||
@@ -14,7 +15,7 @@ const mailRuId = analytics.topMailRuId;
|
|||||||
(m[i].a = m[i].a || []).push(arguments);
|
(m[i].a = m[i].a || []).push(arguments);
|
||||||
};
|
};
|
||||||
m[i].l = 1 * new Date();
|
m[i].l = 1 * new Date();
|
||||||
for (var j = 0; j < document.scripts.length; j++) {
|
for (let j = 0; j < document.scripts.length; j++) {
|
||||||
if (document.scripts[j].src === r) {
|
if (document.scripts[j].src === r) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -41,6 +42,7 @@ const mailRuId = analytics.topMailRuId;
|
|||||||
accurateTrackBounce: true,
|
accurateTrackBounce: true,
|
||||||
trackLinks: true,
|
trackLinks: true,
|
||||||
});
|
});
|
||||||
|
/* eslint-enable */
|
||||||
</script>
|
</script>
|
||||||
<noscript
|
<noscript
|
||||||
><div>
|
><div>
|
||||||
@@ -55,7 +57,7 @@ const mailRuId = analytics.topMailRuId;
|
|||||||
|
|
||||||
<!-- Top.Mail.Ru counter -->
|
<!-- Top.Mail.Ru counter -->
|
||||||
<script type="text/javascript" define:vars={{ mailRuId }}>
|
<script type="text/javascript" define:vars={{ mailRuId }}>
|
||||||
var _tmr = window._tmr || (window._tmr = []);
|
const _tmr = window._tmr || (window._tmr = []);
|
||||||
_tmr.push({
|
_tmr.push({
|
||||||
id: String(mailRuId),
|
id: String(mailRuId),
|
||||||
type: "pageView",
|
type: "pageView",
|
||||||
@@ -63,13 +65,13 @@ const mailRuId = analytics.topMailRuId;
|
|||||||
});
|
});
|
||||||
(function (d, w, id) {
|
(function (d, w, id) {
|
||||||
if (d.getElementById(id)) return;
|
if (d.getElementById(id)) return;
|
||||||
var ts = d.createElement("script");
|
const ts = d.createElement("script");
|
||||||
ts.type = "text/javascript";
|
ts.type = "text/javascript";
|
||||||
ts.async = true;
|
ts.async = true;
|
||||||
ts.id = id;
|
ts.id = id;
|
||||||
ts.src = "https://top-fwz1.mail.ru/js/code.js";
|
ts.src = "https://top-fwz1.mail.ru/js/code.js";
|
||||||
var f = function () {
|
const f = function () {
|
||||||
var s = d.getElementsByTagName("script")[0];
|
const s = d.getElementsByTagName("script")[0];
|
||||||
s.parentNode.insertBefore(ts, s);
|
s.parentNode.insertBefore(ts, s);
|
||||||
};
|
};
|
||||||
if (w.opera == "[object Opera]") {
|
if (w.opera == "[object Opera]") {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
import type { Generator } from '../data/generators';
|
import type { Generator } from "../data/generators";
|
||||||
import { useT } from '../i18n/translations';
|
import { useT } from "../i18n/translations";
|
||||||
import type { Lang } from '../i18n/translations';
|
import type { Lang } from "../i18n/translations";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
generator: Generator;
|
generator: Generator;
|
||||||
@@ -9,11 +9,11 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { generator, lang: langProp } = Astro.props;
|
const { generator, lang: langProp } = Astro.props;
|
||||||
const lang = (langProp || (Astro.currentLocale as Lang) || 'en');
|
const lang = langProp || (Astro.currentLocale as Lang) || "en";
|
||||||
const T = useT(lang);
|
const T = useT(lang);
|
||||||
const { slug, icon, status } = generator;
|
const { slug, icon, status } = generator;
|
||||||
const isLive = status === 'live';
|
const isLive = status === "live";
|
||||||
const isRu = lang === 'ru';
|
const isRu = lang === "ru";
|
||||||
const title = isRu ? generator.ruTitle : generator.title;
|
const title = isRu ? generator.ruTitle : generator.title;
|
||||||
const description = isRu ? generator.ruDescription : generator.description;
|
const description = isRu ? generator.ruDescription : generator.description;
|
||||||
const href = isRu ? `/ru/generators/${slug}/` : `/generators/${slug}/`;
|
const href = isRu ? `/ru/generators/${slug}/` : `/generators/${slug}/`;
|
||||||
@@ -23,22 +23,27 @@ const icons: Record<string, string> = {
|
|||||||
palette: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/></svg>`,
|
palette: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/></svg>`,
|
||||||
lock: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`,
|
lock: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`,
|
||||||
ticket: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"/><path d="M13 5v2"/><path d="M13 17v2"/><path d="M13 11v2"/></svg>`,
|
ticket: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"/><path d="M13 5v2"/><path d="M13 17v2"/><path d="M13 11v2"/></svg>`,
|
||||||
'dice-6': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="M16 8h.01"/><path d="M16 12h.01"/><path d="M16 16h.01"/><path d="M8 8h.01"/><path d="M8 12h.01"/><path d="M8 16h.01"/></svg>`,
|
"dice-6": `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="M16 8h.01"/><path d="M16 12h.01"/><path d="M16 16h.01"/><path d="M8 8h.01"/><path d="M8 12h.01"/><path d="M8 16h.01"/></svg>`,
|
||||||
'square-stack': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 10c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2"/><path d="M10 16c-1.1 0-2-.9-2-2v-4c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2"/><rect width="8" height="8" x="14" y="14" rx="2"/></svg>`,
|
"square-stack": `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 10c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2"/><path d="M10 16c-1.1 0-2-.9-2-2v-4c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2"/><rect width="8" height="8" x="14" y="14" rx="2"/></svg>`,
|
||||||
'circle-dollar-sign': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>`,
|
"circle-dollar-sign": `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg>`,
|
||||||
'list': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>`,
|
list: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/></svg>`,
|
||||||
'fingerprint': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4"/><path d="M14 13.12c0 2.38 0 6.38-1 8.88"/><path d="M17.29 21.02c.12-.6.43-2.3.5-3.02"/><path d="M2 12a10 10 0 0 1 18-6"/><path d="M2 17c1 .5 2.25 1 4 1 1.5 0 3-.5 4-1"/><path d="M20 12c0 2-.5 4.5-2 6"/><path d="M7 13.02c0 2.38 0 5.5 1 7.48"/><path d="M8.5 8.5A5 5 0 0 1 17 12"/></svg>`,
|
fingerprint: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4"/><path d="M14 13.12c0 2.38 0 6.38-1 8.88"/><path d="M17.29 21.02c.12-.6.43-2.3.5-3.02"/><path d="M2 12a10 10 0 0 1 18-6"/><path d="M2 17c1 .5 2.25 1 4 1 1.5 0 3-.5 4-1"/><path d="M20 12c0 2-.5 4.5-2 6"/><path d="M7 13.02c0 2.38 0 5.5 1 7.48"/><path d="M8.5 8.5A5 5 0 0 1 17 12"/></svg>`,
|
||||||
'pie-chart': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.21 15.89A10 10 0 1 1 8 2.83"/><path d="M22 12A10 10 0 0 0 12 2v10z"/></svg>`,
|
"pie-chart": `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.21 15.89A10 10 0 1 1 8 2.83"/><path d="M22 12A10 10 0 0 0 12 2v10z"/></svg>`,
|
||||||
};
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
{isLive ? (
|
{
|
||||||
|
isLive ? (
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
class="group block border border-zinc-800 rounded-xl p-5 hover:border-[#534AB7] transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7]"
|
class="group block border border-zinc-800 rounded-xl p-5 hover:border-[#534AB7] transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7]"
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between gap-3">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<div class="text-[#534AB7]" aria-hidden="true" set:html={icons[icon] ?? icons['hash']} />
|
<div
|
||||||
|
class="text-[#534AB7]"
|
||||||
|
aria-hidden="true"
|
||||||
|
set:html={icons[icon] ?? icons["hash"]}
|
||||||
|
/>
|
||||||
<span class="text-xs font-medium text-[#534AB7] bg-[#534AB7]/10 px-2 py-0.5 rounded-full">
|
<span class="text-xs font-medium text-[#534AB7] bg-[#534AB7]/10 px-2 py-0.5 rounded-full">
|
||||||
{T.live}
|
{T.live}
|
||||||
</span>
|
</span>
|
||||||
@@ -48,13 +53,17 @@ const icons: Record<string, string> = {
|
|||||||
</h2>
|
</h2>
|
||||||
<p class="mt-1 text-sm text-zinc-400 leading-relaxed">{description}</p>
|
<p class="mt-1 text-sm text-zinc-400 leading-relaxed">{description}</p>
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
class="block border border-zinc-800/50 rounded-xl p-5 opacity-40 cursor-default"
|
class="block border border-zinc-800/50 rounded-xl p-5 opacity-40 cursor-default"
|
||||||
aria-label={`${title} — ${T.comingSoon}`}
|
aria-label={`${title} — ${T.comingSoon}`}
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between gap-3">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<div class="text-zinc-500" aria-hidden="true" set:html={icons[icon] ?? icons['hash']} />
|
<div
|
||||||
|
class="text-zinc-500"
|
||||||
|
aria-hidden="true"
|
||||||
|
set:html={icons[icon] ?? icons["hash"]}
|
||||||
|
/>
|
||||||
<span class="text-xs font-medium text-zinc-500 bg-zinc-800 px-2 py-0.5 rounded-full">
|
<span class="text-xs font-medium text-zinc-500 bg-zinc-800 px-2 py-0.5 rounded-full">
|
||||||
{T.comingSoon}
|
{T.comingSoon}
|
||||||
</span>
|
</span>
|
||||||
@@ -62,4 +71,5 @@ const icons: Record<string, string> = {
|
|||||||
<h2 class="mt-3 text-base font-semibold text-zinc-400">{title}</h2>
|
<h2 class="mt-3 text-base font-semibold text-zinc-400">{title}</h2>
|
||||||
<p class="mt-1 text-sm text-zinc-500 leading-relaxed">{description}</p>
|
<p class="mt-1 text-sm text-zinc-500 leading-relaxed">{description}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,38 +1,42 @@
|
|||||||
---
|
---
|
||||||
const currentPath = Astro.url.pathname;
|
const currentPath = Astro.url.pathname;
|
||||||
const isRu = currentPath.startsWith('/ru');
|
const isRu = currentPath.startsWith("/ru");
|
||||||
const alternatePath = isRu
|
const alternatePath = isRu
|
||||||
? (currentPath.replace(/^\/ru/, '') || '/')
|
? currentPath.replace(/^\/ru/, "") || "/"
|
||||||
: ('/ru' + currentPath);
|
: "/ru" + currentPath;
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="fixed top-3 right-3 z-50 flex items-center gap-1 bg-zinc-900/80 border border-zinc-800 rounded-lg px-1 py-1 backdrop-blur-sm text-xs font-semibold">
|
<div
|
||||||
|
class="fixed top-3 right-3 z-50 flex items-center gap-1 bg-zinc-900/80 border border-zinc-800 rounded-lg px-1 py-1 backdrop-blur-sm text-xs font-semibold"
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href={isRu ? alternatePath : '#'}
|
href={isRu ? alternatePath : "#"}
|
||||||
id="lang-en"
|
id="lang-en"
|
||||||
class={`px-2 py-1 rounded-md transition-colors ${isRu ? 'text-zinc-400 hover:text-zinc-200' : 'bg-[#534AB7] text-white'}`}
|
class={`px-2 py-1 rounded-md transition-colors ${isRu ? "text-zinc-400 hover:text-zinc-200" : "bg-[#534AB7] text-white"}`}
|
||||||
aria-current={isRu ? undefined : 'true'}
|
aria-current={isRu ? undefined : "true"}
|
||||||
data-target-lang="en"
|
data-target-lang="en"
|
||||||
data-alternate={isRu ? alternatePath : ''}
|
data-alternate={isRu ? alternatePath : ""}>EN</a
|
||||||
>EN</a>
|
>
|
||||||
<a
|
<a
|
||||||
href={isRu ? '#' : alternatePath}
|
href={isRu ? "#" : alternatePath}
|
||||||
id="lang-ru"
|
id="lang-ru"
|
||||||
class={`px-2 py-1 rounded-md transition-colors ${isRu ? 'bg-[#534AB7] text-white' : 'text-zinc-400 hover:text-zinc-200'}`}
|
class={`px-2 py-1 rounded-md transition-colors ${isRu ? "bg-[#534AB7] text-white" : "text-zinc-400 hover:text-zinc-200"}`}
|
||||||
aria-current={isRu ? 'true' : undefined}
|
aria-current={isRu ? "true" : undefined}
|
||||||
data-target-lang="ru"
|
data-target-lang="ru"
|
||||||
data-alternate={isRu ? '' : alternatePath}
|
data-alternate={isRu ? "" : alternatePath}>RU</a
|
||||||
>RU</a>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.querySelectorAll<HTMLAnchorElement>('#lang-en, #lang-ru').forEach((el) => {
|
document
|
||||||
el.addEventListener('click', (e) => {
|
.querySelectorAll<HTMLAnchorElement>("#lang-en, #lang-ru")
|
||||||
|
.forEach((el) => {
|
||||||
|
el.addEventListener("click", (e) => {
|
||||||
const targetLang = el.dataset.targetLang!;
|
const targetLang = el.dataset.targetLang!;
|
||||||
const alternate = el.dataset.alternate!;
|
const alternate = el.dataset.alternate!;
|
||||||
if (!alternate || alternate === '#') return;
|
if (!alternate || alternate === "#") return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
localStorage.setItem('lang-pref', targetLang);
|
localStorage.setItem("lang-pref", targetLang);
|
||||||
location.href = alternate;
|
location.href = alternate;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
import { useT } from '../i18n/translations';
|
import { useT } from "../i18n/translations";
|
||||||
import type { Lang } from '../i18n/translations';
|
import type { Lang } from "../i18n/translations";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
howTo: string[];
|
howTo: string[];
|
||||||
@@ -8,19 +8,29 @@ interface Props {
|
|||||||
lang?: Lang;
|
lang?: Lang;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { howTo, whenTo, lang = 'en' } = Astro.props;
|
const { howTo, whenTo, lang = "en" } = Astro.props;
|
||||||
const T = useT(lang);
|
const T = useT(lang);
|
||||||
---
|
---
|
||||||
|
|
||||||
<section class="mt-12 border-t border-zinc-800/60 pt-8 text-sm text-zinc-500 space-y-4">
|
<section
|
||||||
|
class="mt-12 border-t border-zinc-800/60 pt-8 text-sm text-zinc-500 space-y-4"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xs font-semibold uppercase tracking-widest text-zinc-600 mb-2">{T.howToUse}</h2>
|
<h2
|
||||||
|
class="text-xs font-semibold uppercase tracking-widest text-zinc-600 mb-2"
|
||||||
|
>
|
||||||
|
{T.howToUse}
|
||||||
|
</h2>
|
||||||
<ol class="space-y-1 list-decimal list-inside">
|
<ol class="space-y-1 list-decimal list-inside">
|
||||||
{howTo.map((step) => <li>{step}</li>)}
|
{howTo.map((step) => <li>{step}</li>)}
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-xs font-semibold uppercase tracking-widest text-zinc-600 mb-2">{T.whenToUse}</h2>
|
<h2
|
||||||
|
class="text-xs font-semibold uppercase tracking-widest text-zinc-600 mb-2"
|
||||||
|
>
|
||||||
|
{T.whenToUse}
|
||||||
|
</h2>
|
||||||
<ul class="space-y-1 list-disc list-inside">
|
<ul class="space-y-1 list-disc list-inside">
|
||||||
{whenTo.map((item) => <li>{item}</li>)}
|
{whenTo.map((item) => <li>{item}</li>)}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -73,10 +73,8 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"
|
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect>
|
||||||
></rect>
|
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
||||||
<path
|
|
||||||
d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
@@ -131,23 +129,15 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
: "Maximum 52 cards at once.";
|
: "Maximum 52 cards at once.";
|
||||||
|
|
||||||
const countInput = document.getElementById("crd-count") as HTMLInputElement;
|
const countInput = document.getElementById("crd-count") as HTMLInputElement;
|
||||||
const replaceCb = document.getElementById(
|
const replaceCb = document.getElementById("crd-replace") as HTMLInputElement;
|
||||||
"crd-replace",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const btn = document.getElementById("crd-btn") as HTMLButtonElement;
|
const btn = document.getElementById("crd-btn") as HTMLButtonElement;
|
||||||
const copyBtn = document.getElementById(
|
const copyBtn = document.getElementById("crd-copy-btn") as HTMLButtonElement;
|
||||||
"crd-copy-btn",
|
|
||||||
) as HTMLButtonElement;
|
|
||||||
const cardsEl = document.getElementById("crd-cards") as HTMLDivElement;
|
const cardsEl = document.getElementById("crd-cards") as HTMLDivElement;
|
||||||
const errorEl = document.getElementById(
|
const errorEl = document.getElementById("crd-error") as HTMLParagraphElement;
|
||||||
"crd-error",
|
|
||||||
) as HTMLParagraphElement;
|
|
||||||
const copyLabel = document.getElementById(
|
const copyLabel = document.getElementById(
|
||||||
"crd-copy-label",
|
"crd-copy-label",
|
||||||
) as HTMLSpanElement;
|
) as HTMLSpanElement;
|
||||||
const copyIcon = document.getElementById(
|
const copyIcon = document.getElementById("crd-copy-icon") as HTMLSpanElement;
|
||||||
"crd-copy-icon",
|
|
||||||
) as HTMLSpanElement;
|
|
||||||
const checkIcon = document.getElementById(
|
const checkIcon = document.getElementById(
|
||||||
"crd-check-icon",
|
"crd-check-icon",
|
||||||
) as HTMLSpanElement;
|
) as HTMLSpanElement;
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
class="w-full accent-[#534AB7] cursor-pointer"
|
class="w-full accent-[#534AB7] cursor-pointer"
|
||||||
aria-label={T.numberOfCoins}
|
aria-label={T.numberOfCoins}
|
||||||
/>
|
/>
|
||||||
<div
|
<div class="flex justify-between text-xs text-zinc-600 mt-1 select-none">
|
||||||
class="flex justify-between text-xs text-zinc-600 mt-1 select-none"
|
|
||||||
>
|
|
||||||
<span>1</span><span>20</span>
|
<span>1</span><span>20</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,10 +38,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div id="coin-summary" class="hidden text-sm text-zinc-500 tabular-nums">
|
||||||
id="coin-summary"
|
|
||||||
class="hidden text-sm text-zinc-500 tabular-nums"
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -65,10 +60,8 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"
|
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect>
|
||||||
></rect>
|
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
||||||
<path
|
|
||||||
d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
@@ -118,24 +111,16 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
const HEADS_SUMMARY = isRu ? "орёл" : "heads";
|
const HEADS_SUMMARY = isRu ? "орёл" : "heads";
|
||||||
const TAILS_SUMMARY = isRu ? "решка" : "tails";
|
const TAILS_SUMMARY = isRu ? "решка" : "tails";
|
||||||
|
|
||||||
const countInput = document.getElementById(
|
const countInput = document.getElementById("coin-count") as HTMLInputElement;
|
||||||
"coin-count",
|
const countVal = document.getElementById("coin-count-val") as HTMLSpanElement;
|
||||||
) as HTMLInputElement;
|
|
||||||
const countVal = document.getElementById(
|
|
||||||
"coin-count-val",
|
|
||||||
) as HTMLSpanElement;
|
|
||||||
const btn = document.getElementById("coin-btn") as HTMLButtonElement;
|
const btn = document.getElementById("coin-btn") as HTMLButtonElement;
|
||||||
const copyBtn = document.getElementById(
|
const copyBtn = document.getElementById("coin-copy-btn") as HTMLButtonElement;
|
||||||
"coin-copy-btn",
|
|
||||||
) as HTMLButtonElement;
|
|
||||||
const coinsEl = document.getElementById("coin-coins") as HTMLDivElement;
|
const coinsEl = document.getElementById("coin-coins") as HTMLDivElement;
|
||||||
const summaryEl = document.getElementById("coin-summary") as HTMLDivElement;
|
const summaryEl = document.getElementById("coin-summary") as HTMLDivElement;
|
||||||
const copyLabel = document.getElementById(
|
const copyLabel = document.getElementById(
|
||||||
"coin-copy-label",
|
"coin-copy-label",
|
||||||
) as HTMLSpanElement;
|
) as HTMLSpanElement;
|
||||||
const copyIcon = document.getElementById(
|
const copyIcon = document.getElementById("coin-copy-icon") as HTMLSpanElement;
|
||||||
"coin-copy-icon",
|
|
||||||
) as HTMLSpanElement;
|
|
||||||
const checkIcon = document.getElementById(
|
const checkIcon = document.getElementById(
|
||||||
"coin-check-icon",
|
"coin-check-icon",
|
||||||
) as HTMLSpanElement;
|
) as HTMLSpanElement;
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
---
|
---
|
||||||
import { useT } from '../../i18n/translations';
|
import { useT } from "../../i18n/translations";
|
||||||
const isRu = Astro.url.pathname.startsWith('/ru');
|
const isRu = Astro.url.pathname.startsWith("/ru");
|
||||||
const T = useT(isRu ? 'ru' : 'en');
|
const T = useT(isRu ? "ru" : "en");
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="color-generator" class="mt-8">
|
<div id="color-generator" class="mt-8">
|
||||||
|
|
||||||
<!-- Swatch -->
|
<!-- Swatch -->
|
||||||
<div class="flex justify-center mb-8">
|
<div class="flex justify-center mb-8">
|
||||||
<div
|
<div
|
||||||
@@ -13,7 +12,8 @@ const T = useT(isRu ? 'ru' : 'en');
|
|||||||
class="w-44 h-44 rounded-2xl border border-zinc-800"
|
class="w-44 h-44 rounded-2xl border border-zinc-800"
|
||||||
style="background-color: #1f1f22; transition: background-color 0.35s cubic-bezier(0.4,0,0.2,1);"
|
style="background-color: #1f1f22; transition: background-color 0.35s cubic-bezier(0.4,0,0.2,1);"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></div>
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Format rows -->
|
<!-- Format rows -->
|
||||||
@@ -23,10 +23,11 @@ const T = useT(isRu ? 'ru' : 'en');
|
|||||||
style="transition: opacity 0.25s ease;"
|
style="transition: opacity 0.25s ease;"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
>
|
>
|
||||||
{[
|
{
|
||||||
{ id: 'cg-hex', label: 'HEX' },
|
[
|
||||||
{ id: 'cg-rgb', label: 'RGB' },
|
{ id: "cg-hex", label: "HEX" },
|
||||||
{ id: 'cg-hsl', label: 'HSL' },
|
{ id: "cg-rgb", label: "RGB" },
|
||||||
|
{ id: "cg-hsl", label: "HSL" },
|
||||||
].map(({ id, label }) => (
|
].map(({ id, label }) => (
|
||||||
<div class="flex items-center gap-3 px-4 py-3 bg-zinc-900/40">
|
<div class="flex items-center gap-3 px-4 py-3 bg-zinc-900/40">
|
||||||
<span class="w-10 text-xs font-semibold uppercase tracking-widest text-zinc-500 shrink-0">
|
<span class="w-10 text-xs font-semibold uppercase tracking-widest text-zinc-500 shrink-0">
|
||||||
@@ -35,23 +36,47 @@ const T = useT(isRu ? 'ru' : 'en');
|
|||||||
<span
|
<span
|
||||||
id={id}
|
id={id}
|
||||||
class="flex-1 font-mono text-sm text-zinc-100 tabular-nums select-all"
|
class="flex-1 font-mono text-sm text-zinc-100 tabular-nums select-all"
|
||||||
></span>
|
/>
|
||||||
<button
|
<button
|
||||||
id={`${id}-copy`}
|
id={`${id}-copy`}
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`Copy ${label} value`}
|
aria-label={`Copy ${label} value`}
|
||||||
class="group shrink-0 p-1 rounded text-zinc-600 hover:text-zinc-300 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#534AB7] transition-colors cursor-pointer"
|
class="group shrink-0 p-1 rounded text-zinc-600 hover:text-zinc-300 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#534AB7] transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<svg id={`${id}-copy-icon`} xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg
|
||||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
|
id={`${id}-copy-icon`}
|
||||||
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
||||||
|
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg id={`${id}-check-icon`} class="hidden text-[#534AB7]" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
<svg
|
||||||
<path d="M20 6 9 17l-5-5"/>
|
id={`${id}-check-icon`}
|
||||||
|
class="hidden text-[#534AB7]"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M20 6 9 17l-5-5" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Generate button -->
|
<!-- Generate button -->
|
||||||
@@ -67,21 +92,24 @@ const T = useT(isRu ? 'ru' : 'en');
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const swatch = document.getElementById('cg-swatch') as HTMLDivElement;
|
const swatch = document.getElementById("cg-swatch") as HTMLDivElement;
|
||||||
const valuesEl = document.getElementById('cg-values') as HTMLDivElement;
|
const valuesEl = document.getElementById("cg-values") as HTMLDivElement;
|
||||||
const hexEl = document.getElementById('cg-hex') as HTMLSpanElement;
|
const hexEl = document.getElementById("cg-hex") as HTMLSpanElement;
|
||||||
const rgbEl = document.getElementById('cg-rgb') as HTMLSpanElement;
|
const rgbEl = document.getElementById("cg-rgb") as HTMLSpanElement;
|
||||||
const hslEl = document.getElementById('cg-hsl') as HTMLSpanElement;
|
const hslEl = document.getElementById("cg-hsl") as HTMLSpanElement;
|
||||||
const btn = document.getElementById('cg-btn') as HTMLButtonElement;
|
const btn = document.getElementById("cg-btn") as HTMLButtonElement;
|
||||||
|
|
||||||
function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
|
function rgbToHsl(r: number, g: number, b: number): [number, number, number] {
|
||||||
const rn = r / 255, gn = g / 255, bn = b / 255;
|
const rn = r / 255,
|
||||||
const max = Math.max(rn, gn, bn), min = Math.min(rn, gn, bn);
|
gn = g / 255,
|
||||||
|
bn = b / 255;
|
||||||
|
const max = Math.max(rn, gn, bn),
|
||||||
|
min = Math.min(rn, gn, bn);
|
||||||
const l = (max + min) / 2;
|
const l = (max + min) / 2;
|
||||||
if (max === min) return [0, 0, Math.round(l * 100)];
|
if (max === min) return [0, 0, Math.round(l * 100)];
|
||||||
const d = max - min;
|
const d = max - min;
|
||||||
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||||
let h = 0;
|
let h;
|
||||||
if (max === rn) h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6;
|
if (max === rn) h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6;
|
||||||
else if (max === gn) h = ((bn - rn) / d + 2) / 6;
|
else if (max === gn) h = ((bn - rn) / d + 2) / 6;
|
||||||
else h = ((rn - gn) / d + 4) / 6;
|
else h = ((rn - gn) / d + 4) / 6;
|
||||||
@@ -89,10 +117,14 @@ const T = useT(isRu ? 'ru' : 'en');
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toHex(n: number) {
|
function toHex(n: number) {
|
||||||
return n.toString(16).padStart(2, '0');
|
return n.toString(16).padStart(2, "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeCopyHandler(el: HTMLSpanElement, copyIconId: string, checkIconId: string) {
|
function makeCopyHandler(
|
||||||
|
el: HTMLSpanElement,
|
||||||
|
copyIconId: string,
|
||||||
|
checkIconId: string,
|
||||||
|
) {
|
||||||
const copyIcon = document.getElementById(copyIconId)!;
|
const copyIcon = document.getElementById(copyIconId)!;
|
||||||
const checkIcon = document.getElementById(checkIconId)!;
|
const checkIcon = document.getElementById(checkIconId)!;
|
||||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||||
@@ -101,22 +133,34 @@ const T = useT(isRu ? 'ru' : 'en');
|
|||||||
const value = el.textContent?.trim();
|
const value = el.textContent?.trim();
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
await navigator.clipboard.writeText(value);
|
await navigator.clipboard.writeText(value);
|
||||||
copyIcon.classList.add('hidden');
|
copyIcon.classList.add("hidden");
|
||||||
checkIcon.classList.remove('hidden');
|
checkIcon.classList.remove("hidden");
|
||||||
if (timer) clearTimeout(timer);
|
if (timer) clearTimeout(timer);
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
checkIcon.classList.add('hidden');
|
checkIcon.classList.add("hidden");
|
||||||
copyIcon.classList.remove('hidden');
|
copyIcon.classList.remove("hidden");
|
||||||
}, 1500);
|
}, 1500);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('cg-hex-copy')!
|
document
|
||||||
.addEventListener('click', makeCopyHandler(hexEl, 'cg-hex-copy-icon', 'cg-hex-check-icon'));
|
.getElementById("cg-hex-copy")!
|
||||||
document.getElementById('cg-rgb-copy')!
|
.addEventListener(
|
||||||
.addEventListener('click', makeCopyHandler(rgbEl, 'cg-rgb-copy-icon', 'cg-rgb-check-icon'));
|
"click",
|
||||||
document.getElementById('cg-hsl-copy')!
|
makeCopyHandler(hexEl, "cg-hex-copy-icon", "cg-hex-check-icon"),
|
||||||
.addEventListener('click', makeCopyHandler(hslEl, 'cg-hsl-copy-icon', 'cg-hsl-check-icon'));
|
);
|
||||||
|
document
|
||||||
|
.getElementById("cg-rgb-copy")!
|
||||||
|
.addEventListener(
|
||||||
|
"click",
|
||||||
|
makeCopyHandler(rgbEl, "cg-rgb-copy-icon", "cg-rgb-check-icon"),
|
||||||
|
);
|
||||||
|
document
|
||||||
|
.getElementById("cg-hsl-copy")!
|
||||||
|
.addEventListener(
|
||||||
|
"click",
|
||||||
|
makeCopyHandler(hslEl, "cg-hsl-copy-icon", "cg-hsl-check-icon"),
|
||||||
|
);
|
||||||
|
|
||||||
function generate() {
|
function generate() {
|
||||||
const r = Math.floor(Math.random() * 256);
|
const r = Math.floor(Math.random() * 256);
|
||||||
@@ -130,8 +174,8 @@ const T = useT(isRu ? 'ru' : 'en');
|
|||||||
hexEl.textContent = hex;
|
hexEl.textContent = hex;
|
||||||
rgbEl.textContent = `rgb(${r}, ${g}, ${b})`;
|
rgbEl.textContent = `rgb(${r}, ${g}, ${b})`;
|
||||||
hslEl.textContent = `hsl(${h}, ${s}%, ${l}%)`;
|
hslEl.textContent = `hsl(${h}, ${s}%, ${l}%)`;
|
||||||
valuesEl.style.opacity = '1';
|
valuesEl.style.opacity = "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
btn.addEventListener('click', generate);
|
btn.addEventListener("click", generate);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -29,8 +29,7 @@ David"
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label
|
<label
|
||||||
for="lst-pick"
|
for="lst-pick"
|
||||||
class="block text-sm font-medium text-zinc-400 mb-1"
|
class="block text-sm font-medium text-zinc-400 mb-1">{T.pick}</label
|
||||||
>{T.pick}</label
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
id="lst-pick"
|
id="lst-pick"
|
||||||
@@ -93,10 +92,8 @@ David"
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"
|
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect>
|
||||||
></rect>
|
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
||||||
<path
|
|
||||||
d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
@@ -151,23 +148,15 @@ David"
|
|||||||
"lst-items",
|
"lst-items",
|
||||||
) as HTMLTextAreaElement;
|
) as HTMLTextAreaElement;
|
||||||
const pickInput = document.getElementById("lst-pick") as HTMLInputElement;
|
const pickInput = document.getElementById("lst-pick") as HTMLInputElement;
|
||||||
const replaceCb = document.getElementById(
|
const replaceCb = document.getElementById("lst-replace") as HTMLInputElement;
|
||||||
"lst-replace",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const btn = document.getElementById("lst-btn") as HTMLButtonElement;
|
const btn = document.getElementById("lst-btn") as HTMLButtonElement;
|
||||||
const copyBtn = document.getElementById(
|
const copyBtn = document.getElementById("lst-copy-btn") as HTMLButtonElement;
|
||||||
"lst-copy-btn",
|
|
||||||
) as HTMLButtonElement;
|
|
||||||
const resultEl = document.getElementById("lst-result") as HTMLDivElement;
|
const resultEl = document.getElementById("lst-result") as HTMLDivElement;
|
||||||
const errorEl = document.getElementById(
|
const errorEl = document.getElementById("lst-error") as HTMLParagraphElement;
|
||||||
"lst-error",
|
|
||||||
) as HTMLParagraphElement;
|
|
||||||
const copyLabel = document.getElementById(
|
const copyLabel = document.getElementById(
|
||||||
"lst-copy-label",
|
"lst-copy-label",
|
||||||
) as HTMLSpanElement;
|
) as HTMLSpanElement;
|
||||||
const copyIcon = document.getElementById(
|
const copyIcon = document.getElementById("lst-copy-icon") as HTMLSpanElement;
|
||||||
"lst-copy-icon",
|
|
||||||
) as HTMLSpanElement;
|
|
||||||
const checkIcon = document.getElementById(
|
const checkIcon = document.getElementById(
|
||||||
"lst-check-icon",
|
"lst-check-icon",
|
||||||
) as HTMLSpanElement;
|
) as HTMLSpanElement;
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
<div id="lottery-generator" class="mt-8">
|
<div id="lottery-generator" class="mt-8">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label for="lg-from" class="block text-sm font-medium text-zinc-400 mb-1"
|
||||||
for="lg-from"
|
|
||||||
class="block text-sm font-medium text-zinc-400 mb-1"
|
|
||||||
>{T.from}</label
|
>{T.from}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -20,9 +18,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label for="lg-to" class="block text-sm font-medium text-zinc-400 mb-1"
|
||||||
for="lg-to"
|
|
||||||
class="block text-sm font-medium text-zinc-400 mb-1"
|
|
||||||
>{T.to}</label
|
>{T.to}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -33,9 +29,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label for="lg-pick" class="block text-sm font-medium text-zinc-400 mb-1"
|
||||||
for="lg-pick"
|
|
||||||
class="block text-sm font-medium text-zinc-400 mb-1"
|
|
||||||
>{T.pick}</label
|
>{T.pick}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -80,16 +74,12 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
><rect width="14" height="14" x="8" y="8" rx="2" ry="2"
|
><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path
|
||||||
></rect><path
|
|
||||||
d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
||||||
></path></svg
|
></path></svg
|
||||||
></span
|
></span
|
||||||
>
|
>
|
||||||
<span
|
<span id="lg-check-icon" class="hidden text-[#534AB7]" aria-hidden="true"
|
||||||
id="lg-check-icon"
|
|
||||||
class="hidden text-[#534AB7]"
|
|
||||||
aria-hidden="true"
|
|
||||||
><svg
|
><svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="14"
|
width="14"
|
||||||
@@ -99,8 +89,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="2.5"
|
stroke-width="2.5"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"><path d="M20 6 9 17l-5-5"></path></svg
|
||||||
><path d="M20 6 9 17l-5-5"></path></svg
|
|
||||||
></span
|
></span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
@@ -141,13 +130,9 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
const copyBtn = document.getElementById("lg-copy-btn") as HTMLButtonElement;
|
const copyBtn = document.getElementById("lg-copy-btn") as HTMLButtonElement;
|
||||||
const resultEl = document.getElementById("lg-result") as HTMLDivElement;
|
const resultEl = document.getElementById("lg-result") as HTMLDivElement;
|
||||||
const errorEl = document.getElementById("lg-error") as HTMLParagraphElement;
|
const errorEl = document.getElementById("lg-error") as HTMLParagraphElement;
|
||||||
const copyLabel = document.getElementById(
|
const copyLabel = document.getElementById("lg-copy-label") as HTMLSpanElement;
|
||||||
"lg-copy-label",
|
|
||||||
) as HTMLSpanElement;
|
|
||||||
const copyIcon = document.getElementById("lg-copy-icon") as HTMLSpanElement;
|
const copyIcon = document.getElementById("lg-copy-icon") as HTMLSpanElement;
|
||||||
const checkIcon = document.getElementById(
|
const checkIcon = document.getElementById("lg-check-icon") as HTMLSpanElement;
|
||||||
"lg-check-icon",
|
|
||||||
) as HTMLSpanElement;
|
|
||||||
|
|
||||||
const errors = createErrorDisplay(errorEl);
|
const errors = createErrorDisplay(errorEl);
|
||||||
const clipboard = new CopyFeedback(copyIcon, checkIcon);
|
const clipboard = new CopyFeedback(copyIcon, checkIcon);
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
<div id="number-generator" class="mt-8" data-ru={isRu ? "1" : "0"}>
|
<div id="number-generator" class="mt-8" data-ru={isRu ? "1" : "0"}>
|
||||||
<div class="flex flex-col sm:flex-row gap-4">
|
<div class="flex flex-col sm:flex-row gap-4">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label
|
<label for="ng-from" class="block text-sm font-medium text-zinc-400 mb-1"
|
||||||
for="ng-from"
|
|
||||||
class="block text-sm font-medium text-zinc-400 mb-1"
|
|
||||||
>{T.from}</label
|
>{T.from}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -20,9 +18,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label
|
<label for="ng-to" class="block text-sm font-medium text-zinc-400 mb-1"
|
||||||
for="ng-to"
|
|
||||||
class="block text-sm font-medium text-zinc-400 mb-1"
|
|
||||||
>{T.to}</label
|
>{T.to}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -69,8 +65,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
><rect width="14" height="14" x="8" y="8" rx="2" ry="2"
|
><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path
|
||||||
></rect><path
|
|
||||||
d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
||||||
></path></svg
|
></path></svg
|
||||||
>
|
>
|
||||||
@@ -89,8 +84,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="2.5"
|
stroke-width="2.5"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"><path d="M20 6 9 17l-5-5"></path></svg
|
||||||
><path d="M20 6 9 17l-5-5"></path></svg
|
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -133,9 +127,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
const resultEl = document.getElementById("ng-result") as HTMLSpanElement;
|
const resultEl = document.getElementById("ng-result") as HTMLSpanElement;
|
||||||
const errorEl = document.getElementById("ng-error") as HTMLParagraphElement;
|
const errorEl = document.getElementById("ng-error") as HTMLParagraphElement;
|
||||||
const copyIcon = document.getElementById("ng-copy-icon") as HTMLSpanElement;
|
const copyIcon = document.getElementById("ng-copy-icon") as HTMLSpanElement;
|
||||||
const checkIcon = document.getElementById(
|
const checkIcon = document.getElementById("ng-check-icon") as HTMLSpanElement;
|
||||||
"ng-check-icon",
|
|
||||||
) as HTMLSpanElement;
|
|
||||||
const copiedLabel = document.getElementById(
|
const copiedLabel = document.getElementById(
|
||||||
"ng-copied-label",
|
"ng-copied-label",
|
||||||
) as HTMLSpanElement;
|
) as HTMLSpanElement;
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
id="pg-length-val"
|
id="pg-length-val"
|
||||||
class="text-sm font-semibold tabular-nums text-zinc-100"
|
class="text-sm font-semibold tabular-nums text-zinc-100">16</span
|
||||||
>16</span
|
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@@ -25,9 +24,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
value="16"
|
value="16"
|
||||||
class="w-full accent-[#534AB7] cursor-pointer"
|
class="w-full accent-[#534AB7] cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<div
|
<div class="flex justify-between text-xs text-zinc-600 mt-1 select-none">
|
||||||
class="flex justify-between text-xs text-zinc-600 mt-1 select-none"
|
|
||||||
>
|
|
||||||
<span>4</span><span>64</span>
|
<span>4</span><span>64</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,8 +69,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
<span
|
<span
|
||||||
id="pg-result"
|
id="pg-result"
|
||||||
class="font-mono text-2xl sm:text-3xl font-bold tracking-wide text-zinc-100 break-all select-none group-hover:text-zinc-300"
|
class="font-mono text-2xl sm:text-3xl font-bold tracking-wide text-zinc-100 break-all select-none group-hover:text-zinc-300"
|
||||||
style="transition: opacity 0.08s ease, color 0.15s ease;"
|
style="transition: opacity 0.08s ease, color 0.15s ease;"></span>
|
||||||
></span>
|
|
||||||
<span
|
<span
|
||||||
id="pg-copy-icon"
|
id="pg-copy-icon"
|
||||||
class="absolute -right-7 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-zinc-500"
|
class="absolute -right-7 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-zinc-500"
|
||||||
@@ -89,8 +85,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
><rect width="14" height="14" x="8" y="8" rx="2" ry="2"
|
><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path
|
||||||
></rect><path
|
|
||||||
d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
||||||
></path></svg
|
></path></svg
|
||||||
>
|
>
|
||||||
@@ -109,8 +104,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="2.5"
|
stroke-width="2.5"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"><path d="M20 6 9 17l-5-5"></path></svg
|
||||||
><path d="M20 6 9 17l-5-5"></path></svg
|
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -142,12 +136,8 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
? "Выберите хотя бы один тип символов."
|
? "Выберите хотя бы один тип символов."
|
||||||
: "Select at least one character type.";
|
: "Select at least one character type.";
|
||||||
|
|
||||||
const lengthInput = document.getElementById(
|
const lengthInput = document.getElementById("pg-length") as HTMLInputElement;
|
||||||
"pg-length",
|
const lengthVal = document.getElementById("pg-length-val") as HTMLSpanElement;
|
||||||
) as HTMLInputElement;
|
|
||||||
const lengthVal = document.getElementById(
|
|
||||||
"pg-length-val",
|
|
||||||
) as HTMLSpanElement;
|
|
||||||
const upperCb = document.getElementById("pg-upper") as HTMLInputElement;
|
const upperCb = document.getElementById("pg-upper") as HTMLInputElement;
|
||||||
const lowerCb = document.getElementById("pg-lower") as HTMLInputElement;
|
const lowerCb = document.getElementById("pg-lower") as HTMLInputElement;
|
||||||
const digitsCb = document.getElementById("pg-digits") as HTMLInputElement;
|
const digitsCb = document.getElementById("pg-digits") as HTMLInputElement;
|
||||||
@@ -157,9 +147,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
const resultEl = document.getElementById("pg-result") as HTMLSpanElement;
|
const resultEl = document.getElementById("pg-result") as HTMLSpanElement;
|
||||||
const errorEl = document.getElementById("pg-error") as HTMLParagraphElement;
|
const errorEl = document.getElementById("pg-error") as HTMLParagraphElement;
|
||||||
const copyIcon = document.getElementById("pg-copy-icon") as HTMLSpanElement;
|
const copyIcon = document.getElementById("pg-copy-icon") as HTMLSpanElement;
|
||||||
const checkIcon = document.getElementById(
|
const checkIcon = document.getElementById("pg-check-icon") as HTMLSpanElement;
|
||||||
"pg-check-icon",
|
|
||||||
) as HTMLSpanElement;
|
|
||||||
const copiedLabel = document.getElementById(
|
const copiedLabel = document.getElementById(
|
||||||
"pg-copied-label",
|
"pg-copied-label",
|
||||||
) as HTMLSpanElement;
|
) as HTMLSpanElement;
|
||||||
@@ -188,9 +176,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
errors.clear();
|
errors.clear();
|
||||||
const required = pools.map(
|
const required = pools.map((p) => p[Math.floor(Math.random() * p.length)]);
|
||||||
(p) => p[Math.floor(Math.random() * p.length)],
|
|
||||||
);
|
|
||||||
const combined = pools.join("");
|
const combined = pools.join("");
|
||||||
const rest = Array.from(
|
const rest = Array.from(
|
||||||
{ length: length - required.length },
|
{ length: length - required.length },
|
||||||
|
|||||||
@@ -10,11 +10,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
<label class="block text-sm font-medium text-zinc-400 mb-2"
|
<label class="block text-sm font-medium text-zinc-400 mb-2"
|
||||||
>{T.type}</label
|
>{T.type}</label
|
||||||
>
|
>
|
||||||
<div
|
<div class="flex flex-wrap gap-2" role="radiogroup" aria-label={T.type}>
|
||||||
class="flex flex-wrap gap-2"
|
|
||||||
role="radiogroup"
|
|
||||||
aria-label={T.type}
|
|
||||||
>
|
|
||||||
{
|
{
|
||||||
[
|
[
|
||||||
{ value: "uuid", label: "UUID v4" },
|
{ value: "uuid", label: "UUID v4" },
|
||||||
@@ -58,8 +54,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label
|
<label
|
||||||
for="ug-count"
|
for="ug-count"
|
||||||
class="block text-sm font-medium text-zinc-400 mb-1"
|
class="block text-sm font-medium text-zinc-400 mb-1">{T.count}</label
|
||||||
>{T.count}</label
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
id="ug-count"
|
id="ug-count"
|
||||||
@@ -100,18 +95,12 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"
|
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect>
|
||||||
></rect>
|
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
||||||
<path
|
|
||||||
d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span id="ug-check-icon" class="hidden text-[#534AB7]" aria-hidden="true">
|
||||||
id="ug-check-icon"
|
|
||||||
class="hidden text-[#534AB7]"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="14"
|
width="14"
|
||||||
@@ -150,20 +139,14 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
const lengthWrap = document.getElementById(
|
const lengthWrap = document.getElementById(
|
||||||
"ug-length-wrap",
|
"ug-length-wrap",
|
||||||
) as HTMLDivElement;
|
) as HTMLDivElement;
|
||||||
const lengthInput = document.getElementById(
|
const lengthInput = document.getElementById("ug-length") as HTMLInputElement;
|
||||||
"ug-length",
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const countInput = document.getElementById("ug-count") as HTMLInputElement;
|
const countInput = document.getElementById("ug-count") as HTMLInputElement;
|
||||||
const btn = document.getElementById("ug-btn") as HTMLButtonElement;
|
const btn = document.getElementById("ug-btn") as HTMLButtonElement;
|
||||||
const copyBtn = document.getElementById("ug-copy-btn") as HTMLButtonElement;
|
const copyBtn = document.getElementById("ug-copy-btn") as HTMLButtonElement;
|
||||||
const resultEl = document.getElementById("ug-result") as HTMLDivElement;
|
const resultEl = document.getElementById("ug-result") as HTMLDivElement;
|
||||||
const copyLabel = document.getElementById(
|
const copyLabel = document.getElementById("ug-copy-label") as HTMLSpanElement;
|
||||||
"ug-copy-label",
|
|
||||||
) as HTMLSpanElement;
|
|
||||||
const copyIcon = document.getElementById("ug-copy-icon") as HTMLSpanElement;
|
const copyIcon = document.getElementById("ug-copy-icon") as HTMLSpanElement;
|
||||||
const checkIcon = document.getElementById(
|
const checkIcon = document.getElementById("ug-check-icon") as HTMLSpanElement;
|
||||||
"ug-check-icon",
|
|
||||||
) as HTMLSpanElement;
|
|
||||||
|
|
||||||
let lastTokens: string[] = [];
|
let lastTokens: string[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -42,10 +42,8 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<canvas
|
<canvas id="ws-canvas" class="block w-full" aria-label="Wheel of fortune"
|
||||||
id="ws-canvas"
|
></canvas>
|
||||||
class="block w-full"
|
|
||||||
aria-label="Wheel of fortune"></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -91,18 +89,12 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"
|
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect>
|
||||||
></rect>
|
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
||||||
<path
|
|
||||||
d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span id="ws-check-icon" class="hidden text-[#534AB7]" aria-hidden="true">
|
||||||
id="ws-check-icon"
|
|
||||||
class="hidden text-[#534AB7]"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="14"
|
width="14"
|
||||||
@@ -172,16 +164,10 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
"ws-result-text",
|
"ws-result-text",
|
||||||
) as HTMLSpanElement;
|
) as HTMLSpanElement;
|
||||||
const copyBtn = document.getElementById("ws-copy-btn") as HTMLButtonElement;
|
const copyBtn = document.getElementById("ws-copy-btn") as HTMLButtonElement;
|
||||||
const copyLabel = document.getElementById(
|
const copyLabel = document.getElementById("ws-copy-label") as HTMLSpanElement;
|
||||||
"ws-copy-label",
|
|
||||||
) as HTMLSpanElement;
|
|
||||||
const copyIcon = document.getElementById("ws-copy-icon") as HTMLSpanElement;
|
const copyIcon = document.getElementById("ws-copy-icon") as HTMLSpanElement;
|
||||||
const checkIcon = document.getElementById(
|
const checkIcon = document.getElementById("ws-check-icon") as HTMLSpanElement;
|
||||||
"ws-check-icon",
|
const wheelWrap = document.getElementById("ws-wheel-wrap") as HTMLDivElement;
|
||||||
) as HTMLSpanElement;
|
|
||||||
const wheelWrap = document.getElementById(
|
|
||||||
"ws-wheel-wrap",
|
|
||||||
) as HTMLDivElement;
|
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d")!;
|
const ctx = canvas.getContext("2d")!;
|
||||||
let items: string[] = ["Alice", "Bob", "Carol", "Dave", "Eve", "Frank"];
|
let items: string[] = ["Alice", "Bob", "Carol", "Dave", "Eve", "Frank"];
|
||||||
@@ -236,10 +222,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
const mid = a0 + seg / 2;
|
const mid = a0 + seg / 2;
|
||||||
const fontSize = Math.max(
|
const fontSize = Math.max(9, Math.min(14, Math.floor(r * seg * 0.4)));
|
||||||
9,
|
|
||||||
Math.min(14, Math.floor(r * seg * 0.4)),
|
|
||||||
);
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.rotate(mid);
|
ctx.rotate(mid);
|
||||||
ctx.textAlign = "right";
|
ctx.textAlign = "right";
|
||||||
@@ -403,11 +386,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
x: cx,
|
x: cx,
|
||||||
y: cy,
|
y: cy,
|
||||||
vx: Math.cos(angle) * speed,
|
vx: Math.cos(angle) * speed,
|
||||||
vy: -(
|
vy: -(Math.abs(Math.sin(angle)) * speed + 2 + Math.random() * 4),
|
||||||
Math.abs(Math.sin(angle)) * speed +
|
|
||||||
2 +
|
|
||||||
Math.random() * 4
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
export const siteUrl = 'https://randify.pro';
|
export const siteUrl = "https://randify.pro";
|
||||||
|
|
||||||
export const analytics = {
|
export const analytics = {
|
||||||
yandexMetrikaId: '109130319',
|
yandexMetrikaId: "109130319",
|
||||||
topMailRuId: '3765043',
|
topMailRuId: "3765043",
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
+119
-115
@@ -1,133 +1,137 @@
|
|||||||
export type Lang = 'en' | 'ru';
|
export type Lang = "en" | "ru";
|
||||||
|
|
||||||
export const translations = {
|
export const translations = {
|
||||||
en: {
|
en: {
|
||||||
allGenerators: 'All generators',
|
allGenerators: "All generators",
|
||||||
live: 'Live',
|
live: "Live",
|
||||||
comingSoon: 'Coming soon',
|
comingSoon: "Coming soon",
|
||||||
copy: 'Copy',
|
copy: "Copy",
|
||||||
copied: 'Copied',
|
copied: "Copied",
|
||||||
copyAll: 'Copy all',
|
copyAll: "Copy all",
|
||||||
howToUse: 'How to use',
|
howToUse: "How to use",
|
||||||
whenToUse: 'When to use',
|
whenToUse: "When to use",
|
||||||
|
|
||||||
generate: 'Generate',
|
generate: "Generate",
|
||||||
draw: 'Draw',
|
draw: "Draw",
|
||||||
roll: 'Roll',
|
roll: "Roll",
|
||||||
flip: 'Flip',
|
flip: "Flip",
|
||||||
pick: 'Pick',
|
pick: "Pick",
|
||||||
spin: 'Spin',
|
spin: "Spin",
|
||||||
|
|
||||||
from: 'From',
|
from: "From",
|
||||||
to: 'To',
|
to: "To",
|
||||||
length: 'Length',
|
length: "Length",
|
||||||
count: 'Count',
|
count: "Count",
|
||||||
winner: 'Winner',
|
winner: "Winner",
|
||||||
total: 'Total',
|
total: "Total",
|
||||||
sides: 'Sides',
|
sides: "Sides",
|
||||||
numberOfDice: 'Number of dice',
|
numberOfDice: "Number of dice",
|
||||||
numberOfCoins: 'Number of coins',
|
numberOfCoins: "Number of coins",
|
||||||
cardsToDraw: 'Cards to draw',
|
cardsToDraw: "Cards to draw",
|
||||||
allowDuplicates: 'Allow duplicates',
|
allowDuplicates: "Allow duplicates",
|
||||||
uppercase: 'Uppercase (A–Z)',
|
uppercase: "Uppercase (A–Z)",
|
||||||
lowercase: 'Lowercase (a–z)',
|
lowercase: "Lowercase (a–z)",
|
||||||
digits: 'Digits (0–9)',
|
digits: "Digits (0–9)",
|
||||||
symbols: 'Symbols (!@#…)',
|
symbols: "Symbols (!@#…)",
|
||||||
type: 'Type',
|
type: "Type",
|
||||||
lengthBytes: 'Length (bytes)',
|
lengthBytes: "Length (bytes)",
|
||||||
itemsOneLine: 'one per line',
|
itemsOneLine: "one per line",
|
||||||
itemsOneLine224: 'one per line, 2–24',
|
itemsOneLine224: "one per line, 2–24",
|
||||||
items: 'Items',
|
items: "Items",
|
||||||
headsLabel: 'H',
|
headsLabel: "H",
|
||||||
tailsLabel: 'T',
|
tailsLabel: "T",
|
||||||
headsWord: 'heads',
|
headsWord: "heads",
|
||||||
tailsWord: 'tails',
|
tailsWord: "tails",
|
||||||
|
|
||||||
errBothIntegers: 'Both values must be whole numbers.',
|
errBothIntegers: "Both values must be whole numbers.",
|
||||||
errFromLessThanTo: '"From" must be less than "To".',
|
errFromLessThanTo: '"From" must be less than "To".',
|
||||||
errSelectOneType: 'Select at least one character type.',
|
errSelectOneType: "Select at least one character type.",
|
||||||
errAllIntegers: 'All values must be whole numbers.',
|
errAllIntegers: "All values must be whole numbers.",
|
||||||
errPickAtLeastOne: '"Pick" must be at least 1.',
|
errPickAtLeastOne: '"Pick" must be at least 1.',
|
||||||
errDiceAtLeast: 'Number of dice must be at least 1.',
|
errDiceAtLeast: "Number of dice must be at least 1.",
|
||||||
errDiceMax: 'Maximum 20 dice at once.',
|
errDiceMax: "Maximum 20 dice at once.",
|
||||||
errDrawAtLeast: 'Draw at least 1 card.',
|
errDrawAtLeast: "Draw at least 1 card.",
|
||||||
errMaxUniqueCards: 'Cannot draw more than 52 unique cards.',
|
errMaxUniqueCards: "Cannot draw more than 52 unique cards.",
|
||||||
errMax52Cards: 'Maximum 52 cards at once.',
|
errMax52Cards: "Maximum 52 cards at once.",
|
||||||
errAddAtLeast2: 'Add at least 2 items.',
|
errAddAtLeast2: "Add at least 2 items.",
|
||||||
errMax24: 'Maximum 24 items.',
|
errMax24: "Maximum 24 items.",
|
||||||
errAddAtLeast1: 'Add at least one item to the list.',
|
errAddAtLeast1: "Add at least one item to the list.",
|
||||||
errPickAtLeast1: 'Pick at least 1 item.',
|
errPickAtLeast1: "Pick at least 1 item.",
|
||||||
|
|
||||||
homeTitle: 'Random generators for every occasion',
|
homeTitle: "Random generators for every occasion",
|
||||||
homeSubtitle: 'Simple tools for raffles, games, and everything that needs randomness.',
|
homeSubtitle:
|
||||||
defaultTitle: 'Randify — Random Value Generators',
|
"Simple tools for raffles, games, and everything that needs randomness.",
|
||||||
defaultDesc: 'Simple tools for raffles, games, and everything that needs randomness.',
|
defaultTitle: "Randify — Random Value Generators",
|
||||||
brandLabel: 'randify',
|
defaultDesc:
|
||||||
backToAll: 'All generators',
|
"Simple tools for raffles, games, and everything that needs randomness.",
|
||||||
|
brandLabel: "randify",
|
||||||
|
backToAll: "All generators",
|
||||||
},
|
},
|
||||||
ru: {
|
ru: {
|
||||||
allGenerators: 'Все генераторы',
|
allGenerators: "Все генераторы",
|
||||||
live: 'Активен',
|
live: "Активен",
|
||||||
comingSoon: 'Скоро',
|
comingSoon: "Скоро",
|
||||||
copy: 'Копировать',
|
copy: "Копировать",
|
||||||
copied: 'Скопировано',
|
copied: "Скопировано",
|
||||||
copyAll: 'Копировать всё',
|
copyAll: "Копировать всё",
|
||||||
howToUse: 'Как пользоваться',
|
howToUse: "Как пользоваться",
|
||||||
whenToUse: 'Когда использовать',
|
whenToUse: "Когда использовать",
|
||||||
|
|
||||||
generate: 'Сгенерировать',
|
generate: "Сгенерировать",
|
||||||
draw: 'Тянуть',
|
draw: "Тянуть",
|
||||||
roll: 'Бросить',
|
roll: "Бросить",
|
||||||
flip: 'Подбросить',
|
flip: "Подбросить",
|
||||||
pick: 'Выбрать',
|
pick: "Выбрать",
|
||||||
spin: 'Крутить',
|
spin: "Крутить",
|
||||||
|
|
||||||
from: 'От',
|
from: "От",
|
||||||
to: 'До',
|
to: "До",
|
||||||
length: 'Длина',
|
length: "Длина",
|
||||||
count: 'Количество',
|
count: "Количество",
|
||||||
winner: 'Победитель',
|
winner: "Победитель",
|
||||||
total: 'Итого',
|
total: "Итого",
|
||||||
sides: 'Грани',
|
sides: "Грани",
|
||||||
numberOfDice: 'Количество кубиков',
|
numberOfDice: "Количество кубиков",
|
||||||
numberOfCoins: 'Количество монет',
|
numberOfCoins: "Количество монет",
|
||||||
cardsToDraw: 'Карт вытащить',
|
cardsToDraw: "Карт вытащить",
|
||||||
allowDuplicates: 'Разрешить повторения',
|
allowDuplicates: "Разрешить повторения",
|
||||||
uppercase: 'Заглавные (A–Z)',
|
uppercase: "Заглавные (A–Z)",
|
||||||
lowercase: 'Строчные (a–z)',
|
lowercase: "Строчные (a–z)",
|
||||||
digits: 'Цифры (0–9)',
|
digits: "Цифры (0–9)",
|
||||||
symbols: 'Символы (!@#…)',
|
symbols: "Символы (!@#…)",
|
||||||
type: 'Тип',
|
type: "Тип",
|
||||||
lengthBytes: 'Длина (байты)',
|
lengthBytes: "Длина (байты)",
|
||||||
itemsOneLine: 'по одному на строку',
|
itemsOneLine: "по одному на строку",
|
||||||
itemsOneLine224: 'по одному на строку, 2–24',
|
itemsOneLine224: "по одному на строку, 2–24",
|
||||||
items: 'Элементы',
|
items: "Элементы",
|
||||||
headsLabel: 'О',
|
headsLabel: "О",
|
||||||
tailsLabel: 'Р',
|
tailsLabel: "Р",
|
||||||
headsWord: 'орёл',
|
headsWord: "орёл",
|
||||||
tailsWord: 'решка',
|
tailsWord: "решка",
|
||||||
|
|
||||||
errBothIntegers: 'Оба значения должны быть целыми числами.',
|
errBothIntegers: "Оба значения должны быть целыми числами.",
|
||||||
errFromLessThanTo: '«От» должно быть меньше «До».',
|
errFromLessThanTo: "«От» должно быть меньше «До».",
|
||||||
errSelectOneType: 'Выберите хотя бы один тип символов.',
|
errSelectOneType: "Выберите хотя бы один тип символов.",
|
||||||
errAllIntegers: 'Все значения должны быть целыми числами.',
|
errAllIntegers: "Все значения должны быть целыми числами.",
|
||||||
errPickAtLeastOne: '«Выбрать» должно быть не менее 1.',
|
errPickAtLeastOne: "«Выбрать» должно быть не менее 1.",
|
||||||
errDiceAtLeast: 'Количество кубиков должно быть не менее 1.',
|
errDiceAtLeast: "Количество кубиков должно быть не менее 1.",
|
||||||
errDiceMax: 'Максимум 20 кубиков одновременно.',
|
errDiceMax: "Максимум 20 кубиков одновременно.",
|
||||||
errDrawAtLeast: 'Вытащите хотя бы 1 карту.',
|
errDrawAtLeast: "Вытащите хотя бы 1 карту.",
|
||||||
errMaxUniqueCards: 'Нельзя вытащить более 52 уникальных карт.',
|
errMaxUniqueCards: "Нельзя вытащить более 52 уникальных карт.",
|
||||||
errMax52Cards: 'Максимум 52 карты одновременно.',
|
errMax52Cards: "Максимум 52 карты одновременно.",
|
||||||
errAddAtLeast2: 'Добавьте хотя бы 2 элемента.',
|
errAddAtLeast2: "Добавьте хотя бы 2 элемента.",
|
||||||
errMax24: 'Максимум 24 элемента.',
|
errMax24: "Максимум 24 элемента.",
|
||||||
errAddAtLeast1: 'Добавьте хотя бы один элемент в список.',
|
errAddAtLeast1: "Добавьте хотя бы один элемент в список.",
|
||||||
errPickAtLeast1: 'Выберите хотя бы 1 элемент.',
|
errPickAtLeast1: "Выберите хотя бы 1 элемент.",
|
||||||
|
|
||||||
homeTitle: 'Генераторы случайных значений на любой случай',
|
homeTitle: "Генераторы случайных значений на любой случай",
|
||||||
homeSubtitle: 'Простые инструменты для розыгрышей, игр и всего, что требует случайности.',
|
homeSubtitle:
|
||||||
defaultTitle: 'Randify — Генераторы случайных значений',
|
"Простые инструменты для розыгрышей, игр и всего, что требует случайности.",
|
||||||
defaultDesc: 'Простые инструменты для розыгрышей, игр и всего, что требует случайности.',
|
defaultTitle: "Randify — Генераторы случайных значений",
|
||||||
brandLabel: 'randify',
|
defaultDesc:
|
||||||
backToAll: 'Все генераторы',
|
"Простые инструменты для розыгрышей, игр и всего, что требует случайности.",
|
||||||
|
brandLabel: "randify",
|
||||||
|
backToAll: "Все генераторы",
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -57,10 +57,7 @@ const ruUrl = `https://randify.pro${getRelativeLocaleUrl("ru", pathWithoutLocale
|
|||||||
<script>
|
<script>
|
||||||
if (!localStorage.getItem("lang-pref")) {
|
if (!localStorage.getItem("lang-pref")) {
|
||||||
const browserLang = (navigator.language || "").toLowerCase();
|
const browserLang = (navigator.language || "").toLowerCase();
|
||||||
if (
|
if (browserLang.startsWith("ru") && !location.pathname.startsWith("/ru")) {
|
||||||
browserLang.startsWith("ru") &&
|
|
||||||
!location.pathname.startsWith("/ru")
|
|
||||||
) {
|
|
||||||
location.replace("/ru" + location.pathname);
|
location.replace("/ru" + location.pathname);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from './BaseLayout.astro';
|
import BaseLayout from "./BaseLayout.astro";
|
||||||
import AdBanner from '../components/AdBanner.astro';
|
import AdBanner from "../components/AdBanner.astro";
|
||||||
import SeoBlock from '../components/SeoBlock.astro';
|
import SeoBlock from "../components/SeoBlock.astro";
|
||||||
import { useT } from '../i18n/translations';
|
import { useT } from "../i18n/translations";
|
||||||
import type { Lang } from '../i18n/translations';
|
import type { Lang } from "../i18n/translations";
|
||||||
import type { Generator } from '../data/generators';
|
import type { Generator } from "../data/generators";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
generator: Generator;
|
generator: Generator;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { generator } = Astro.props;
|
const { generator } = Astro.props;
|
||||||
const lang = (Astro.currentLocale as Lang) || 'en';
|
const lang = (Astro.currentLocale as Lang) || "en";
|
||||||
const T = useT(lang);
|
const T = useT(lang);
|
||||||
const isRu = lang === 'ru';
|
const isRu = lang === "ru";
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
@@ -23,7 +23,7 @@ const isRu = lang === 'ru';
|
|||||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||||
<nav aria-label="Breadcrumb" class="mb-10">
|
<nav aria-label="Breadcrumb" class="mb-10">
|
||||||
<a
|
<a
|
||||||
href={isRu ? '/ru/' : '/'}
|
href={isRu ? "/ru/" : "/"}
|
||||||
class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded"
|
class="inline-flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded"
|
||||||
aria-label={T.backToAll}
|
aria-label={T.backToAll}
|
||||||
>
|
>
|
||||||
@@ -39,7 +39,7 @@ const isRu = lang === 'ru';
|
|||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<path d="m15 18-6-6 6-6"/>
|
<path d="m15 18-6-6 6-6"></path>
|
||||||
</svg>
|
</svg>
|
||||||
{T.backToAll}
|
{T.backToAll}
|
||||||
</a>
|
</a>
|
||||||
@@ -49,14 +49,18 @@ const isRu = lang === 'ru';
|
|||||||
<div class="flex items-center gap-2 mb-4">
|
<div class="flex items-center gap-2 mb-4">
|
||||||
<span
|
<span
|
||||||
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
|
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
|
||||||
aria-hidden="true"
|
aria-hidden="true"></span>
|
||||||
></span>
|
<span
|
||||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
class="text-sm font-medium text-zinc-400 uppercase tracking-widest"
|
||||||
|
>randify</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
|
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
|
||||||
{isRu ? generator.ruPageTitle : generator.pageTitle}
|
{isRu ? generator.ruPageTitle : generator.pageTitle}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="mt-2 text-base text-zinc-400">{isRu ? generator.ruDescription : generator.description}</p>
|
<p class="mt-2 text-base text-zinc-400">
|
||||||
|
{isRu ? generator.ruDescription : generator.description}
|
||||||
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
export function popElement(el: HTMLElement) {
|
export function popElement(el: HTMLElement) {
|
||||||
el.style.transform = 'scale(1.18)';
|
el.style.transform = "scale(1.18)";
|
||||||
el.style.opacity = '0.7';
|
el.style.opacity = "0.7";
|
||||||
requestAnimationFrame(() =>
|
requestAnimationFrame(() =>
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
el.style.transform = 'scale(1)';
|
el.style.transform = "scale(1)";
|
||||||
el.style.opacity = '1';
|
el.style.opacity = "1";
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ export class CopyFeedback {
|
|||||||
constructor(
|
constructor(
|
||||||
private copyIcon: HTMLElement,
|
private copyIcon: HTMLElement,
|
||||||
private checkIcon: HTMLElement,
|
private checkIcon: HTMLElement,
|
||||||
private duration = 1500
|
private duration = 1500,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
showCopied() {
|
showCopied() {
|
||||||
this.copyIcon.classList.add('hidden');
|
this.copyIcon.classList.add("hidden");
|
||||||
this.checkIcon.classList.remove('hidden');
|
this.checkIcon.classList.remove("hidden");
|
||||||
if (this.timer) clearTimeout(this.timer);
|
if (this.timer) clearTimeout(this.timer);
|
||||||
this.timer = setTimeout(() => this.revert(), this.duration);
|
this.timer = setTimeout(() => this.revert(), this.duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
revert() {
|
revert() {
|
||||||
this.checkIcon.classList.add('hidden');
|
this.checkIcon.classList.add("hidden");
|
||||||
this.copyIcon.classList.remove('hidden');
|
this.copyIcon.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ export function createErrorDisplay(errorEl: HTMLElement) {
|
|||||||
return {
|
return {
|
||||||
show(msg: string) {
|
show(msg: string) {
|
||||||
errorEl.textContent = msg;
|
errorEl.textContent = msg;
|
||||||
errorEl.classList.remove('hidden');
|
errorEl.classList.remove("hidden");
|
||||||
},
|
},
|
||||||
clear() {
|
clear() {
|
||||||
errorEl.textContent = '';
|
errorEl.textContent = "";
|
||||||
errorEl.classList.add('hidden');
|
errorEl.classList.add("hidden");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||||
import {
|
import {
|
||||||
parseDiceNotation,
|
parseDiceNotation,
|
||||||
rollDice,
|
rollDice,
|
||||||
|
|||||||
@@ -89,7 +89,11 @@ export function parseDiceNotation(raw: string): Parsed | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exploding: ! or !>N or !p or !p>N
|
// Exploding: ! or !>N or !p or !p>N
|
||||||
let explode: Explode = { active: false, threshold: null, penetrating: false };
|
const explode: Explode = {
|
||||||
|
active: false,
|
||||||
|
threshold: null,
|
||||||
|
penetrating: false,
|
||||||
|
};
|
||||||
const expMatch = remaining.match(/^(!p|!)(>?)(\d*)/);
|
const expMatch = remaining.match(/^(!p|!)(>?)(\d*)/);
|
||||||
if (expMatch) {
|
if (expMatch) {
|
||||||
explode.active = true;
|
explode.active = true;
|
||||||
@@ -104,7 +108,7 @@ export function parseDiceNotation(raw: string): Parsed | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reroll: rN, roN, r<N, ro<N
|
// Reroll: rN, roN, r<N, ro<N
|
||||||
let reroll: Reroll = {
|
const reroll: Reroll = {
|
||||||
active: false,
|
active: false,
|
||||||
values: new Set(),
|
values: new Set(),
|
||||||
once: false,
|
once: false,
|
||||||
@@ -158,7 +162,7 @@ function rollDie(sides: number, explode: Explode, reroll: Reroll): DieResult {
|
|||||||
let value = Math.floor(Math.random() * sides) + 1;
|
let value = Math.floor(Math.random() * sides) + 1;
|
||||||
const original = value;
|
const original = value;
|
||||||
let rerolledFrom: number | null = null;
|
let rerolledFrom: number | null = null;
|
||||||
let explosions: number[] = [];
|
const explosions: number[] = [];
|
||||||
let exploded = false;
|
let exploded = false;
|
||||||
|
|
||||||
// Handle reroll
|
// Handle reroll
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ const T = useT(lang);
|
|||||||
>{T.brandLabel}</span
|
>{T.brandLabel}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<h1
|
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100 leading-tight">
|
||||||
class="text-3xl sm:text-4xl font-bold text-zinc-100 leading-tight"
|
|
||||||
>
|
|
||||||
{T.homeTitle}
|
{T.homeTitle}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="mt-3 text-base text-zinc-400 max-w-md">
|
<p class="mt-3 text-base text-zinc-400 max-w-md">
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ const T = useT(lang);
|
|||||||
>{T.brandLabel}</span
|
>{T.brandLabel}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<h1
|
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100 leading-tight">
|
||||||
class="text-3xl sm:text-4xl font-bold text-zinc-100 leading-tight"
|
|
||||||
>
|
|
||||||
{T.homeTitle}
|
{T.homeTitle}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="mt-3 text-base text-zinc-400 max-w-md">
|
<p class="mt-3 text-base text-zinc-400 max-w-md">
|
||||||
|
|||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
import { defineConfig } from 'vitest/config';
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: 'node',
|
environment: "node",
|
||||||
include: ['src/**/*.test.ts'],
|
include: ["src/**/*.test.ts"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user