Wave 4: refactor generators batch 2 to crypto random
- refactor(generators): migrate Gradient, Hash, Letter, List to crypto random - refactor(generators): migrate Lorem, Lottery, Magic8Ball to crypto random
This commit is contained in:
+54
-2
@@ -8,7 +8,7 @@
|
|||||||
"plan_name": "critical-refactor",
|
"plan_name": "critical-refactor",
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"started_at": "2026-05-13T16:25:19.266Z",
|
"started_at": "2026-05-13T16:25:19.266Z",
|
||||||
"updated_at": "2026-05-13T17:16:10.363Z",
|
"updated_at": "2026-05-13T17:25:32.173Z",
|
||||||
"session_ids": [
|
"session_ids": [
|
||||||
"ses_1de6ede59ffexUwt5C0sddEWwR"
|
"ses_1de6ede59ffexUwt5C0sddEWwR"
|
||||||
],
|
],
|
||||||
@@ -68,6 +68,32 @@
|
|||||||
"status": "completed",
|
"status": "completed",
|
||||||
"ended_at": "2026-05-13T17:16:10.363Z",
|
"ended_at": "2026-05-13T17:16:10.363Z",
|
||||||
"elapsed_ms": 1586504
|
"elapsed_ms": 1586504
|
||||||
|
},
|
||||||
|
"todo:13": {
|
||||||
|
"task_key": "todo:13",
|
||||||
|
"task_label": "13",
|
||||||
|
"task_title": "Refactor GradientGenerator.astro",
|
||||||
|
"session_id": "ses_1dda85f21ffeEGeOWrbqmWAk02",
|
||||||
|
"agent": "Sisyphus-Junior",
|
||||||
|
"category": "unspecified-high",
|
||||||
|
"updated_at": "2026-05-13T17:22:16.123Z",
|
||||||
|
"started_at": "2026-05-13T17:19:45.020Z",
|
||||||
|
"status": "completed",
|
||||||
|
"ended_at": "2026-05-13T17:22:16.123Z",
|
||||||
|
"elapsed_ms": 151103
|
||||||
|
},
|
||||||
|
"todo:17": {
|
||||||
|
"task_key": "todo:17",
|
||||||
|
"task_label": "17",
|
||||||
|
"task_title": "Refactor LoremGenerator.astro",
|
||||||
|
"session_id": "ses_1dda37bf2ffexATCZ3W4Z6EDu1",
|
||||||
|
"agent": "Sisyphus-Junior",
|
||||||
|
"category": "unspecified-high",
|
||||||
|
"updated_at": "2026-05-13T17:25:32.173Z",
|
||||||
|
"started_at": "2026-05-13T17:25:11.269Z",
|
||||||
|
"status": "completed",
|
||||||
|
"ended_at": "2026-05-13T17:25:32.173Z",
|
||||||
|
"elapsed_ms": 20904
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,7 +101,7 @@
|
|||||||
"active_plan": "/home/emil/Desktop/Coding/AI/Randify.pro/.sisyphus/plans/critical-refactor.md",
|
"active_plan": "/home/emil/Desktop/Coding/AI/Randify.pro/.sisyphus/plans/critical-refactor.md",
|
||||||
"started_at": "2026-05-13T16:25:19.266Z",
|
"started_at": "2026-05-13T16:25:19.266Z",
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"updated_at": "2026-05-13T17:16:10.363Z",
|
"updated_at": "2026-05-13T17:25:32.173Z",
|
||||||
"session_ids": [
|
"session_ids": [
|
||||||
"ses_1de6ede59ffexUwt5C0sddEWwR"
|
"ses_1de6ede59ffexUwt5C0sddEWwR"
|
||||||
],
|
],
|
||||||
@@ -135,6 +161,32 @@
|
|||||||
"status": "completed",
|
"status": "completed",
|
||||||
"ended_at": "2026-05-13T17:16:10.363Z",
|
"ended_at": "2026-05-13T17:16:10.363Z",
|
||||||
"elapsed_ms": 1586504
|
"elapsed_ms": 1586504
|
||||||
|
},
|
||||||
|
"todo:13": {
|
||||||
|
"task_key": "todo:13",
|
||||||
|
"task_label": "13",
|
||||||
|
"task_title": "Refactor GradientGenerator.astro",
|
||||||
|
"session_id": "ses_1dda85f21ffeEGeOWrbqmWAk02",
|
||||||
|
"agent": "Sisyphus-Junior",
|
||||||
|
"category": "unspecified-high",
|
||||||
|
"updated_at": "2026-05-13T17:22:16.123Z",
|
||||||
|
"started_at": "2026-05-13T17:19:45.020Z",
|
||||||
|
"status": "completed",
|
||||||
|
"ended_at": "2026-05-13T17:22:16.123Z",
|
||||||
|
"elapsed_ms": 151103
|
||||||
|
},
|
||||||
|
"todo:17": {
|
||||||
|
"task_key": "todo:17",
|
||||||
|
"task_label": "17",
|
||||||
|
"task_title": "Refactor LoremGenerator.astro",
|
||||||
|
"session_id": "ses_1dda37bf2ffexATCZ3W4Z6EDu1",
|
||||||
|
"agent": "Sisyphus-Junior",
|
||||||
|
"category": "unspecified-high",
|
||||||
|
"updated_at": "2026-05-13T17:25:32.173Z",
|
||||||
|
"started_at": "2026-05-13T17:25:11.269Z",
|
||||||
|
"status": "completed",
|
||||||
|
"ended_at": "2026-05-13T17:25:32.173Z",
|
||||||
|
"elapsed_ms": 20904
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"agent": "atlas"
|
"agent": "atlas"
|
||||||
|
|||||||
@@ -519,7 +519,7 @@ Max Concurrent: 8 (Wave 3)
|
|||||||
- Message: `refactor(generators): migrate batch 1 to crypto random (Card, Coin, Color, Country, Date, Emoji, FontPair)`
|
- Message: `refactor(generators): migrate batch 1 to crypto random (Card, Coin, Color, Country, Date, Emoji, FontPair)`
|
||||||
- Files: `src/components/generators/{Card,Coin,Color,Country,Date,Emoji,FontPair}Generator.astro`
|
- Files: `src/components/generators/{Card,Coin,Color,Country,Date,Emoji,FontPair}Generator.astro`
|
||||||
|
|
||||||
- [ ] 13. Refactor GradientGenerator.astro
|
- [x] 13. Refactor GradientGenerator.astro
|
||||||
**What to do**: Same pattern as Task 6.
|
**What to do**: Same pattern as Task 6.
|
||||||
**Recommended Agent Profile**: `unspecified-high`
|
**Recommended Agent Profile**: `unspecified-high`
|
||||||
**Parallelization**: YES (with Tasks 14-19, 20-26, 27-32)
|
**Parallelization**: YES (with Tasks 14-19, 20-26, 27-32)
|
||||||
@@ -527,7 +527,7 @@ Max Concurrent: 8 (Wave 3)
|
|||||||
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
||||||
**Commit**: NO
|
**Commit**: NO
|
||||||
|
|
||||||
- [ ] 14. Refactor HashGenerator.astro
|
- [x] 14. Refactor HashGenerator.astro
|
||||||
**What to do**: Same pattern as Task 6.
|
**What to do**: Same pattern as Task 6.
|
||||||
**Recommended Agent Profile**: `unspecified-high`
|
**Recommended Agent Profile**: `unspecified-high`
|
||||||
**Parallelization**: YES (with Tasks 13, 15-19, 20-26, 27-32)
|
**Parallelization**: YES (with Tasks 13, 15-19, 20-26, 27-32)
|
||||||
@@ -535,7 +535,7 @@ Max Concurrent: 8 (Wave 3)
|
|||||||
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
||||||
**Commit**: NO
|
**Commit**: NO
|
||||||
|
|
||||||
- [ ] 15. Refactor LetterGenerator.astro
|
- [x] 15. Refactor LetterGenerator.astro
|
||||||
**What to do**: Same pattern as Task 6.
|
**What to do**: Same pattern as Task 6.
|
||||||
**Recommended Agent Profile**: `unspecified-high`
|
**Recommended Agent Profile**: `unspecified-high`
|
||||||
**Parallelization**: YES (with Tasks 13-14, 16-19, 20-26, 27-32)
|
**Parallelization**: YES (with Tasks 13-14, 16-19, 20-26, 27-32)
|
||||||
@@ -543,7 +543,7 @@ Max Concurrent: 8 (Wave 3)
|
|||||||
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
||||||
**Commit**: NO
|
**Commit**: NO
|
||||||
|
|
||||||
- [ ] 16. Refactor ListGenerator.astro
|
- [x] 16. Refactor ListGenerator.astro
|
||||||
**What to do**: Same pattern as Task 6.
|
**What to do**: Same pattern as Task 6.
|
||||||
**Recommended Agent Profile**: `unspecified-high`
|
**Recommended Agent Profile**: `unspecified-high`
|
||||||
**Parallelization**: YES (with Tasks 13-15, 17-19, 20-26, 27-32)
|
**Parallelization**: YES (with Tasks 13-15, 17-19, 20-26, 27-32)
|
||||||
@@ -551,7 +551,7 @@ Max Concurrent: 8 (Wave 3)
|
|||||||
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
||||||
**Commit**: NO
|
**Commit**: NO
|
||||||
|
|
||||||
- [ ] 17. Refactor LoremGenerator.astro
|
- [x] 17. Refactor LoremGenerator.astro
|
||||||
**What to do**: Same pattern as Task 6.
|
**What to do**: Same pattern as Task 6.
|
||||||
**Recommended Agent Profile**: `unspecified-high`
|
**Recommended Agent Profile**: `unspecified-high`
|
||||||
**Parallelization**: YES (with Tasks 13-16, 18-19, 20-26, 27-32)
|
**Parallelization**: YES (with Tasks 13-16, 18-19, 20-26, 27-32)
|
||||||
@@ -559,7 +559,7 @@ Max Concurrent: 8 (Wave 3)
|
|||||||
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
||||||
**Commit**: NO
|
**Commit**: NO
|
||||||
|
|
||||||
- [ ] 18. Refactor LotteryGenerator.astro
|
- [x] 18. Refactor LotteryGenerator.astro
|
||||||
**What to do**: Same pattern as Task 6.
|
**What to do**: Same pattern as Task 6.
|
||||||
**Recommended Agent Profile**: `unspecified-high`
|
**Recommended Agent Profile**: `unspecified-high`
|
||||||
**Parallelization**: YES (with Tasks 13-17, 19, 20-26, 27-32)
|
**Parallelization**: YES (with Tasks 13-17, 19, 20-26, 27-32)
|
||||||
@@ -567,7 +567,7 @@ Max Concurrent: 8 (Wave 3)
|
|||||||
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
**Acceptance Criteria**: No Math.random, uses random.ts and i18n.ts
|
||||||
**Commit**: NO
|
**Commit**: NO
|
||||||
|
|
||||||
- [ ] 19. Refactor Magic8BallGenerator.astro
|
- [x] 19. Refactor Magic8BallGenerator.astro
|
||||||
**What to do**: Same pattern as Task 6.
|
**What to do**: Same pattern as Task 6.
|
||||||
**Recommended Agent Profile**: `unspecified-high`
|
**Recommended Agent Profile**: `unspecified-high`
|
||||||
**Parallelization**: YES (with Tasks 13-18, 20-26, 27-32)
|
**Parallelization**: YES (with Tasks 13-18, 20-26, 27-32)
|
||||||
|
|||||||
@@ -113,7 +113,11 @@ const i18n = {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const isRu = document.documentElement.lang === "ru";
|
import { randomInt } from "@/lib/client/random";
|
||||||
|
import { getClientLang, getClientT } from "@/lib/client/i18n";
|
||||||
|
|
||||||
|
const lang = getClientLang();
|
||||||
|
const T = getClientT();
|
||||||
|
|
||||||
const typeSelect = document.getElementById("gg-type") as HTMLSelectElement;
|
const typeSelect = document.getElementById("gg-type") as HTMLSelectElement;
|
||||||
const stopsSelect = document.getElementById("gg-stops") as HTMLSelectElement;
|
const stopsSelect = document.getElementById("gg-stops") as HTMLSelectElement;
|
||||||
@@ -130,10 +134,6 @@ const i18n = {
|
|||||||
const copyIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" 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>`;
|
const copyIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" 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>`;
|
||||||
const checkIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="text-accent"><path d="M20 6 9 17l-5-5"/></svg>`;
|
const checkIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="text-accent"><path d="M20 6 9 17l-5-5"/></svg>`;
|
||||||
|
|
||||||
function randomInt(min: number, max: number): number {
|
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hslToHex(h: number, s: number, l: number): string {
|
function hslToHex(h: number, s: number, l: number): string {
|
||||||
s /= 100;
|
s /= 100;
|
||||||
l /= 100;
|
l /= 100;
|
||||||
@@ -223,7 +223,7 @@ const i18n = {
|
|||||||
chip.type = "button";
|
chip.type = "button";
|
||||||
chip.className =
|
chip.className =
|
||||||
"group flex items-center gap-2 px-3 py-2 bg-zinc-950 border border-zinc-800 rounded-xl hover:border-zinc-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 transition-all duration-100 cursor-pointer";
|
"group flex items-center gap-2 px-3 py-2 bg-zinc-950 border border-zinc-800 rounded-xl hover:border-zinc-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 transition-all duration-100 cursor-pointer";
|
||||||
chip.setAttribute("aria-label", isRu ? `Скопировать ${hex}` : `Copy ${hex}`);
|
chip.setAttribute("aria-label", `${T.copy} ${hex}`);
|
||||||
|
|
||||||
const swatch = document.createElement("span");
|
const swatch = document.createElement("span");
|
||||||
swatch.className = "w-5 h-5 rounded-full border border-zinc-700 shrink-0";
|
swatch.className = "w-5 h-5 rounded-full border border-zinc-700 shrink-0";
|
||||||
@@ -276,12 +276,12 @@ const i18n = {
|
|||||||
|
|
||||||
copyCssIcon.classList.add("hidden");
|
copyCssIcon.classList.add("hidden");
|
||||||
checkCssIcon.classList.remove("hidden");
|
checkCssIcon.classList.remove("hidden");
|
||||||
copyCssLabel.textContent = isRu ? "Скопировано!" : "Copied!";
|
copyCssLabel.textContent = T.copied;
|
||||||
if (cssCopyTimer) clearTimeout(cssCopyTimer);
|
if (cssCopyTimer) clearTimeout(cssCopyTimer);
|
||||||
cssCopyTimer = setTimeout(() => {
|
cssCopyTimer = setTimeout(() => {
|
||||||
checkCssIcon.classList.add("hidden");
|
checkCssIcon.classList.add("hidden");
|
||||||
copyCssIcon.classList.remove("hidden");
|
copyCssIcon.classList.remove("hidden");
|
||||||
copyCssLabel.textContent = isRu ? "Копировать" : "Copy";
|
copyCssLabel.textContent = T.copy;
|
||||||
}, 1500);
|
}, 1500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -213,6 +213,12 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { randomInt } from "@/lib/client/random";
|
||||||
|
import { getClientLang, getClientT } from "@/lib/client/i18n";
|
||||||
|
|
||||||
|
const isRu = getClientLang() === "ru";
|
||||||
|
const T = getClientT();
|
||||||
|
|
||||||
// ===================== MD5 Implementation =====================
|
// ===================== MD5 Implementation =====================
|
||||||
function md5(str: string): string {
|
function md5(str: string): string {
|
||||||
const utf8 = new TextEncoder().encode(str);
|
const utf8 = new TextEncoder().encode(str);
|
||||||
@@ -300,33 +306,13 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
function generateRandomString(length: number): string {
|
function generateRandomString(length: number): string {
|
||||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
let result = "";
|
let result = "";
|
||||||
const randomValues = new Uint32Array(length);
|
|
||||||
crypto.getRandomValues(randomValues);
|
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
result += chars[randomValues[i] % chars.length];
|
result += chars[randomInt(0, chars.length - 1)];
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================== DOM Elements =====================
|
// ===================== DOM Elements =====================
|
||||||
const isRu = document.documentElement.lang === "ru";
|
|
||||||
const TEXTS = {
|
|
||||||
generate: isRu ? "Сгенерировать" : "Generate",
|
|
||||||
generated: isRu ? "Сгенерировано" : "Generated",
|
|
||||||
copy: isRu ? "Копировать" : "Copy",
|
|
||||||
copied: isRu ? "Скопировано" : "Copied",
|
|
||||||
match: isRu ? "Совпадение" : "Match",
|
|
||||||
noMatch: isRu ? "Нет совпадения" : "No match",
|
|
||||||
enterText: isRu ? "Ввести текст" : "Enter text",
|
|
||||||
generateRandom: isRu ? "Случайная строка" : "Generate random",
|
|
||||||
uppercase: isRu ? "ВЕРХНИЙ РЕГИСТР" : "UPPERCASE",
|
|
||||||
lowercase: isRu ? "нижний регистр" : "lowercase",
|
|
||||||
compareHint: isRu ? "Вставьте хеш, чтобы проверить совпадение с результатами." : "Paste a hash to check if it matches any result.",
|
|
||||||
refresh: isRu ? "Обновить" : "Refresh",
|
|
||||||
inputMode: isRu ? "Режим ввода" : "Input mode",
|
|
||||||
stringLength: isRu ? "Длина строки" : "String length",
|
|
||||||
};
|
|
||||||
|
|
||||||
const modeTextBtn = document.getElementById("hash-mode-text") as HTMLButtonElement;
|
const modeTextBtn = document.getElementById("hash-mode-text") as HTMLButtonElement;
|
||||||
const modeRandomBtn = document.getElementById("hash-mode-random") as HTMLButtonElement;
|
const modeRandomBtn = document.getElementById("hash-mode-random") as HTMLButtonElement;
|
||||||
const textSection = document.getElementById("hash-text-section") as HTMLDivElement;
|
const textSection = document.getElementById("hash-text-section") as HTMLDivElement;
|
||||||
@@ -411,19 +397,19 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
return {
|
return {
|
||||||
matches,
|
matches,
|
||||||
display: matches
|
display: matches
|
||||||
? `<span class="inline-flex items-center gap-1 text-green-400 text-xs font-medium"><svg 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> ${TEXTS.match}</span>`
|
? `<span class="inline-flex items-center gap-1 text-green-400 text-xs font-medium"><svg 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> ${isRu ? "Совпадение" : "Match"}</span>`
|
||||||
: `<span class="inline-flex items-center gap-1 text-red-400 text-xs font-medium"><svg 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="M18 6 6 18"/><path d="m6 6 12 12"/></svg> ${TEXTS.noMatch}</span>`,
|
: `<span class="inline-flex items-center gap-1 text-red-400 text-xs font-medium"><svg 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="M18 6 6 18"/><path d="m6 6 12 12"/></svg> ${isRu ? "Нет совпадения" : "No match"}</span>`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyToClipboard(text: string, iconEl: HTMLElement, checkEl: HTMLElement, labelEl: HTMLElement) {
|
async function copyToClipboard(text: string, iconEl: HTMLElement, checkEl: HTMLElement, labelEl: HTMLElement) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
labelEl.textContent = TEXTS.copied;
|
labelEl.textContent = T.copied;
|
||||||
iconEl.classList.add("hidden");
|
iconEl.classList.add("hidden");
|
||||||
checkEl.classList.remove("hidden");
|
checkEl.classList.remove("hidden");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
labelEl.textContent = TEXTS.copy;
|
labelEl.textContent = T.copy;
|
||||||
iconEl.classList.remove("hidden");
|
iconEl.classList.remove("hidden");
|
||||||
checkEl.classList.add("hidden");
|
checkEl.classList.add("hidden");
|
||||||
}, 1500);
|
}, 1500);
|
||||||
@@ -488,9 +474,9 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="hash-copy-btn inline-flex items-center gap-1.5 px-3 py-1.5 bg-zinc-900/80 border border-zinc-700/50 rounded-lg text-xs font-medium text-zinc-400 hover:text-zinc-100 hover:bg-zinc-800 transition-colors focus:outline-none focus-visible:ring-2 ${colors.ring} focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 flex-shrink-0 cursor-pointer"
|
class="hash-copy-btn inline-flex items-center gap-1.5 px-3 py-1.5 bg-zinc-900/80 border border-zinc-700/50 rounded-lg text-xs font-medium text-zinc-400 hover:text-zinc-100 hover:bg-zinc-800 transition-colors focus:outline-none focus-visible:ring-2 ${colors.ring} focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 flex-shrink-0 cursor-pointer"
|
||||||
aria-label="${TEXTS.copy} ${algo.toUpperCase()}"
|
aria-label="${T.copy} ${algo.toUpperCase()}"
|
||||||
>
|
>
|
||||||
<span class="hash-copy-label">${TEXTS.copy}</span>
|
<span class="hash-copy-label">${T.copy}</span>
|
||||||
<span class="hash-copy-icon" aria-hidden="true">
|
<span class="hash-copy-icon" aria-hidden="true">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" 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 xmlns="http://www.w3.org/2000/svg" width="13" height="13" 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>
|
||||||
</span>
|
</span>
|
||||||
@@ -572,7 +558,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
|
|
||||||
caseToggleBtn.addEventListener("click", () => {
|
caseToggleBtn.addEventListener("click", () => {
|
||||||
isUppercase = !isUppercase;
|
isUppercase = !isUppercase;
|
||||||
caseLabel.textContent = isUppercase ? TEXTS.uppercase : TEXTS.lowercase;
|
caseLabel.textContent = isUppercase ? (isRu ? "ВЕРХНИЙ РЕГИСТР" : "UPPERCASE") : (isRu ? "нижний регистр" : "lowercase");
|
||||||
updateCase();
|
updateCase();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -79,8 +79,12 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
<script>
|
<script>
|
||||||
import { CopyFeedback } from "@/lib/client/clipboard";
|
import { CopyFeedback } from "@/lib/client/clipboard";
|
||||||
import { popElement } from "@/lib/client/animations";
|
import { popElement } from "@/lib/client/animations";
|
||||||
|
import { randomInt } from "@/lib/client/random";
|
||||||
|
import { getClientLang, getClientT } from "@/lib/client/i18n";
|
||||||
|
|
||||||
const isRu = document.documentElement.lang === "ru";
|
const lang = getClientLang();
|
||||||
|
const isRu = lang === "ru";
|
||||||
|
const T = getClientT();
|
||||||
const ALPHABET = isRu
|
const ALPHABET = isRu
|
||||||
? "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ"
|
? "АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ"
|
||||||
: "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
: "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
@@ -97,7 +101,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
const clipboard = new CopyFeedback(copyIcon, checkIcon);
|
const clipboard = new CopyFeedback(copyIcon, checkIcon);
|
||||||
|
|
||||||
function generate() {
|
function generate() {
|
||||||
const idx = Math.floor(Math.random() * ALPHABET.length);
|
const idx = randomInt(0, ALPHABET.length - 1);
|
||||||
const letter = ALPHABET[idx];
|
const letter = ALPHABET[idx];
|
||||||
resultEl.textContent = letter;
|
resultEl.textContent = letter;
|
||||||
copyBtn.classList.remove("invisible");
|
copyBtn.classList.remove("invisible");
|
||||||
|
|||||||
@@ -135,16 +135,15 @@ David"
|
|||||||
<script>
|
<script>
|
||||||
import { CopyFeedback } from "@/lib/client/clipboard";
|
import { CopyFeedback } from "@/lib/client/clipboard";
|
||||||
import { createErrorDisplay } from "@/lib/client/validation";
|
import { createErrorDisplay } from "@/lib/client/validation";
|
||||||
|
import { randomInt, shuffleArray } from "@/lib/client/random";
|
||||||
|
import { getClientLang, getClientT } from "@/lib/client/i18n";
|
||||||
|
|
||||||
const isRu = document.documentElement.lang === "ru";
|
const isRu = getClientLang() === "ru";
|
||||||
const COPY_LABEL = isRu ? "Копировать" : "Copy";
|
const T = getClientT();
|
||||||
const COPIED_LABEL = isRu ? "Скопировано" : "Copied";
|
const COPY_LABEL = T.copy;
|
||||||
const ERR_ADD_ONE = isRu
|
const COPIED_LABEL = T.copied;
|
||||||
? "Добавьте хотя бы один элемент в список."
|
const ERR_ADD_ONE = T.errAddAtLeast1;
|
||||||
: "Add at least one item to the list.";
|
const ERR_PICK_ONE = T.errPickAtLeast1;
|
||||||
const ERR_PICK_ONE = isRu
|
|
||||||
? "Выберите хотя бы 1 элемент."
|
|
||||||
: "Pick at least 1 item.";
|
|
||||||
|
|
||||||
const itemsInput = document.getElementById(
|
const itemsInput = document.getElementById(
|
||||||
"lst-items",
|
"lst-items",
|
||||||
@@ -198,15 +197,10 @@ David"
|
|||||||
if (withReplacement) {
|
if (withReplacement) {
|
||||||
lastPicked = Array.from(
|
lastPicked = Array.from(
|
||||||
{ length: count },
|
{ length: count },
|
||||||
() => items[Math.floor(Math.random() * items.length)],
|
() => items[randomInt(0, items.length - 1)],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const pool = [...items];
|
lastPicked = shuffleArray(items).slice(0, count);
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
const j = i + Math.floor(Math.random() * (pool.length - i));
|
|
||||||
[pool[i], pool[j]] = [pool[j], pool[i]];
|
|
||||||
}
|
|
||||||
lastPicked = pool.slice(0, count);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resultEl.innerHTML = "";
|
resultEl.innerHTML = "";
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ const startLabel = isRu
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { randomInt, shuffleArray } from "@/lib/client/random";
|
||||||
|
import { getClientLang, getClientT } from "@/lib/client/i18n";
|
||||||
const WORDS = [
|
const WORDS = [
|
||||||
"lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit",
|
"lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit",
|
||||||
"sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore",
|
"sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore",
|
||||||
@@ -159,26 +161,13 @@ const startLabel = isRu
|
|||||||
const copyIcon = document.getElementById("lg-copy-icon") as SVGElement;
|
const copyIcon = document.getElementById("lg-copy-icon") as SVGElement;
|
||||||
const checkIcon = document.getElementById("lg-check-icon") as SVGElement;
|
const checkIcon = document.getElementById("lg-check-icon") as SVGElement;
|
||||||
|
|
||||||
function randInt(min: number, max: number) {
|
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shuffle<T>(arr: T[]): T[] {
|
|
||||||
const a = arr.slice();
|
|
||||||
for (let i = a.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
[a[i], a[j]] = [a[j], a[i]];
|
|
||||||
}
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
function capitalize(word: string) {
|
function capitalize(word: string) {
|
||||||
return word.charAt(0).toUpperCase() + word.slice(1);
|
return word.charAt(0).toUpperCase() + word.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSentence(): string {
|
function generateSentence(): string {
|
||||||
const length = randInt(8, 18);
|
const length = randomInt(8, 18);
|
||||||
const words = shuffle(WORDS).slice(0, length);
|
const words = shuffleArray(WORDS).slice(0, length);
|
||||||
words[0] = capitalize(words[0]);
|
words[0] = capitalize(words[0]);
|
||||||
const sentence = words.join(" ") + ".";
|
const sentence = words.join(" ") + ".";
|
||||||
return sentence;
|
return sentence;
|
||||||
@@ -218,7 +207,7 @@ const startLabel = isRu
|
|||||||
const paragraphs: string[] = [];
|
const paragraphs: string[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const sentenceCount = randInt(minSentences, maxSentences);
|
const sentenceCount = randomInt(minSentences, maxSentences);
|
||||||
paragraphs.push(generateParagraph(sentenceCount, i === 0, startClassic));
|
paragraphs.push(generateParagraph(sentenceCount, i === 0, startClassic));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,8 +215,8 @@ const startLabel = isRu
|
|||||||
resultWrap.style.opacity = "1";
|
resultWrap.style.opacity = "1";
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRu = document.documentElement.lang === "ru";
|
const T = getClientT();
|
||||||
const copyLabel = copyBtn.getAttribute("aria-label") || (isRu ? "Копировать" : "Copy");
|
const copyLabel = copyBtn.getAttribute("aria-label") || T.copy;
|
||||||
let copyTimer: ReturnType<typeof setTimeout> | null = null;
|
let copyTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
copyBtn.addEventListener("click", async () => {
|
copyBtn.addEventListener("click", async () => {
|
||||||
@@ -236,7 +225,7 @@ const startLabel = isRu
|
|||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
copyIcon.classList.add("hidden");
|
copyIcon.classList.add("hidden");
|
||||||
checkIcon.classList.remove("hidden");
|
checkIcon.classList.remove("hidden");
|
||||||
copyBtn.setAttribute("aria-label", isRu ? "Скопировано" : "Copied");
|
copyBtn.setAttribute("aria-label", T.copied);
|
||||||
if (copyTimer) clearTimeout(copyTimer);
|
if (copyTimer) clearTimeout(copyTimer);
|
||||||
copyTimer = setTimeout(() => {
|
copyTimer = setTimeout(() => {
|
||||||
checkIcon.classList.add("hidden");
|
checkIcon.classList.add("hidden");
|
||||||
|
|||||||
@@ -111,19 +111,17 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
<script>
|
<script>
|
||||||
import { CopyFeedback } from "@/lib/client/clipboard";
|
import { CopyFeedback } from "@/lib/client/clipboard";
|
||||||
import { createErrorDisplay, isInteger } from "@/lib/client/validation";
|
import { createErrorDisplay, isInteger } from "@/lib/client/validation";
|
||||||
|
import { shuffleArray } from "@/lib/client/random";
|
||||||
|
import { getClientLang, getClientT } from "@/lib/client/i18n";
|
||||||
|
|
||||||
const isRu = document.documentElement.lang === "ru";
|
const lang = getClientLang();
|
||||||
const ERR_INTEGERS = isRu
|
const T = getClientT();
|
||||||
? "Все значения должны быть целыми числами."
|
const isRu = lang === "ru";
|
||||||
: "All values must be whole numbers.";
|
const ERR_INTEGERS = T.errAllIntegers;
|
||||||
const ERR_FROM_TO = isRu
|
const ERR_FROM_TO = T.errFromLessThanTo;
|
||||||
? "«От» должно быть меньше «До»."
|
const ERR_PICK_MIN = T.errPickAtLeastOne;
|
||||||
: '"From" must be less than "To".';
|
const COPY_LABEL = T.copy;
|
||||||
const ERR_PICK_MIN = isRu
|
const COPIED_LABEL = T.copied;
|
||||||
? "«Выбрать» должно быть не менее 1."
|
|
||||||
: '"Pick" must be at least 1.';
|
|
||||||
const COPY_LABEL = isRu ? "Копировать" : "Copy";
|
|
||||||
const COPIED_LABEL = isRu ? "Скопировано" : "Copied";
|
|
||||||
|
|
||||||
const fromInput = document.getElementById("lg-from") as HTMLInputElement;
|
const fromInput = document.getElementById("lg-from") as HTMLInputElement;
|
||||||
const toInput = document.getElementById("lg-to") as HTMLInputElement;
|
const toInput = document.getElementById("lg-to") as HTMLInputElement;
|
||||||
@@ -170,11 +168,7 @@ const T = useT(isRu ? "ru" : "en");
|
|||||||
}
|
}
|
||||||
errors.clear();
|
errors.clear();
|
||||||
const nums = Array.from({ length: pool }, (_, i) => min + i);
|
const nums = Array.from({ length: pool }, (_, i) => min + i);
|
||||||
for (let i = 0; i < pick; i++) {
|
const picked = shuffleArray(nums).slice(0, pick).sort((a, b) => a - b);
|
||||||
const j = i + Math.floor(Math.random() * (pool - i));
|
|
||||||
[nums[i], nums[j]] = [nums[j], nums[i]];
|
|
||||||
}
|
|
||||||
const picked = nums.slice(0, pick).sort((a, b) => a - b);
|
|
||||||
resultEl.innerHTML = "";
|
resultEl.innerHTML = "";
|
||||||
picked.forEach((n) => {
|
picked.forEach((n) => {
|
||||||
const ball = document.createElement("span");
|
const ball = document.createElement("span");
|
||||||
|
|||||||
@@ -214,8 +214,12 @@ const copiedLabel = isRu ? "Скопировано" : "Copied";
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { CopyFeedback } from "@/lib/client/clipboard";
|
import { CopyFeedback } from "@/lib/client/clipboard";
|
||||||
|
import { randomInt } from "@/lib/client/random";
|
||||||
|
import { getClientLang, getClientT } from "@/lib/client/i18n";
|
||||||
|
|
||||||
const isRu = document.documentElement.lang === "ru";
|
const lang = getClientLang();
|
||||||
|
const T = getClientT();
|
||||||
|
const isRu = lang === "ru";
|
||||||
|
|
||||||
// ─── Answer pools ───────────────────────────────────────────────────────────
|
// ─── Answer pools ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -322,7 +326,7 @@ const copiedLabel = isRu ? "Скопировано" : "Copied";
|
|||||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function getRandomAnswer(): Answer {
|
function getRandomAnswer(): Answer {
|
||||||
return ANSWERS[Math.floor(Math.random() * ANSWERS.length)];
|
return ANSWERS[randomInt(0, ANSWERS.length - 1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadHistory(): { question: string; answer: string; type: AnswerType; time: number }[] {
|
function loadHistory(): { question: string; answer: string; type: AnswerType; time: number }[] {
|
||||||
@@ -430,11 +434,11 @@ const copiedLabel = isRu ? "Скопировано" : "Copied";
|
|||||||
if (!currentAnswer) return;
|
if (!currentAnswer) return;
|
||||||
await navigator.clipboard.writeText(currentAnswer.text);
|
await navigator.clipboard.writeText(currentAnswer.text);
|
||||||
clipboard.showCopied();
|
clipboard.showCopied();
|
||||||
copyLabel.textContent = isRu ? "Скопировано" : "Copied";
|
copyLabel.textContent = T.copied;
|
||||||
copiedLabel.style.opacity = "1";
|
copiedLabel.style.opacity = "1";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
copiedLabel.style.opacity = "0";
|
copiedLabel.style.opacity = "0";
|
||||||
copyLabel.textContent = isRu ? "Копировать" : "Copy";
|
copyLabel.textContent = T.copy;
|
||||||
}, 1500);
|
}, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user