refactor: i18n routing, shared GeneratorLayout, analytics component, content collections
- Add Astro native i18n routing (prefixDefaultLocale: false) - Introduce GeneratorLayout to deduplicate all generator pages - Enrich generator data with pageTitle, ruPageTitle, howTo/whenTo arrays - Make EN and RU generator pages identical using @/ path aliases - Extract analytics (Yandex Metrika + Top.Mail.Ru) into Analytics.astro - Move tracker IDs to src/data/config.ts, inject via define:vars - Migrate generator metadata to Content Collections (src/content/generators/*.json) - Add Zod schema with .strict() validation in src/lib/generator-schema.ts - Update CLAUDE.md with new conventions
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1778278468140
|
||||
}
|
||||
}
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
/// <reference types="astro/client" />
|
||||
@@ -2,3 +2,4 @@ node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.log
|
||||
.astro/
|
||||
|
||||
@@ -6,6 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
Randify is a bilingual (EN/RU) static site (Astro 4 + Tailwind CSS 4) hosting random-value generators. All 10 generators are live. Deployed to randify.pro on reg.ru via GitHub Actions + rsync on push to `main`.
|
||||
|
||||
**i18n is handled via Astro's built-in i18n routing.** English pages live at `src/pages/`, Russian pages at `src/pages/ru/`. Both share identical templates because `BaseLayout`, `GeneratorLayout`, and components derive the active locale from `Astro.currentLocale`.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
@@ -27,16 +29,29 @@ No test runner or linter is configured.
|
||||
|
||||
Every new generator must be created in **both English and Russian simultaneously**.
|
||||
|
||||
1. **`src/data/generators.ts`** — add an entry with all fields: `slug`, `title`, `description`, `icon`, `status: 'live'`, `seoTitle`, `seoDescription`, `ruTitle`, `ruDescription`, `ruSeoTitle`, `ruSeoDescription`.
|
||||
1. **`src/content/generators/<slug>.json`** — create a JSON file with all fields: `slug`, `title`, `description`, `icon`, `status`, `seoTitle`, `seoDescription`, `ruTitle`, `ruDescription`, `ruSeoTitle`, `ruSeoDescription`, `pageTitle`, `ruPageTitle`, `howTo`, `whenTo`, `ruHowTo`, `ruWhenTo`. The schema is defined in `src/lib/generator-schema.ts` and validated automatically at build time via Zod (`.strict()` — unknown fields will fail the build).
|
||||
|
||||
2. **`src/components/generators/<Name>Generator.astro`** — the interactive component. Language detection pattern:
|
||||
- Frontmatter: `const isRu = Astro.url.pathname.startsWith('/ru'); const T = useT(isRu ? 'ru' : 'en');`
|
||||
- Client script: `const isRu = document.documentElement.lang === 'ru';`
|
||||
- Use `T.*` keys for all user-visible strings in the template; use `isRu` ternaries in the `<script>` block.
|
||||
|
||||
3. **`src/pages/generators/<slug>.astro`** — English page with `<BaseLayout lang="en">`.
|
||||
3. **`src/pages/generators/<slug>.astro`** — create the page using `GeneratorLayout`:
|
||||
```astro
|
||||
---
|
||||
import GeneratorLayout from '@/layouts/GeneratorLayout.astro';
|
||||
import <Name>Generator from '@/components/generators/<Name>Generator.astro';
|
||||
import { generators } from '@/data/generators';
|
||||
|
||||
4. **`src/pages/ru/generators/<slug>.astro`** — Russian page with `<BaseLayout lang="ru">`, `ruSeoTitle`/`ruSeoDescription`, breadcrumb back to `/ru/`, Russian `<SeoBlock>` content.
|
||||
const generator = generators.find((g) => g.slug === '<slug>')!;
|
||||
---
|
||||
|
||||
<GeneratorLayout generator={generator}>
|
||||
<<Name>Generator />
|
||||
</GeneratorLayout>
|
||||
```
|
||||
|
||||
4. **`src/pages/ru/generators/<slug>.astro`** — **copy the English file exactly**. Because both files use `@/` path aliases and `GeneratorLayout` derives the locale from `Astro.currentLocale`, the file content is identical for both languages.
|
||||
|
||||
**Critical:** Never use a shared dynamic `[slug].astro` for Russian pages. Astro bundles scripts from all imported components — using a single file that imports all 10 generators causes every Russian page to run all 10 scripts, breaking them. Each page must be its own file importing only its generator.
|
||||
|
||||
@@ -46,13 +61,15 @@ Every new generator must be created in **both English and Russian simultaneously
|
||||
- **`src/components/LanguageSwitcher.astro`** — fixed top-right EN/RU toggle; persists choice in `localStorage('lang-pref')`.
|
||||
- **`src/layouts/BaseLayout.astro`** — accepts `lang` prop (`'en' | 'ru'`), sets `<html lang>`, injects `hreflang` alternates, and includes auto-redirect script (first visit, Russian browser → `/ru/`).
|
||||
- English pages: `/generators/<slug>/` — Russian pages: `/ru/generators/<slug>/`
|
||||
- Path aliases `@/*` resolve to `src/*` and are used in all page files so EN and RU templates can be identical.
|
||||
- `GeneratorLayout.astro` wraps every generator page: breadcrumb, header, AdBanner, SeoBlock, and slot for the interactive component.
|
||||
|
||||
## Key conventions
|
||||
|
||||
- Accent color: `#534AB7` (CSS var `--accent` in BaseLayout).
|
||||
- All SVG icons are inlined strings in the `icons` map inside `GeneratorCard.astro`; add new ones there.
|
||||
- `GeneratorCard.astro` accepts `lang` prop — pass `lang="ru"` on Russian pages to get `ruTitle`/`ruDescription` and `/ru/` links.
|
||||
- `GeneratorCard.astro` auto-detects locale from `Astro.currentLocale` when no `lang` prop is passed.
|
||||
- `SeoBlock.astro` accepts `lang` prop for translated "How to use" / "When to use" headers.
|
||||
- `AdBanner.astro` randomises between two Yandex referral links; three size variants: `leaderboard`, `rectangle`, `tile`.
|
||||
- Yandex Metrika counter (ID 109130319) is in `BaseLayout.astro`.
|
||||
- Yandex Metrika counter (ID `109130319`) and Top.Mail.Ru counter (ID `3765043`) live in `src/components/Analytics.astro`. IDs are configured in `src/data/config.ts` and injected via `define:vars`.
|
||||
- No client-side router — each generator is a separate static HTML page.
|
||||
|
||||
+11
-4
@@ -1,11 +1,18 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import { defineConfig } from "astro/config";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://randify.pro',
|
||||
site: "https://randify.pro",
|
||||
integrations: [sitemap()],
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
i18n: {
|
||||
defaultLocale: "en",
|
||||
locales: ["en", "ru"],
|
||||
routing: {
|
||||
prefixDefaultLocale: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
import { analytics } from "@/data/config";
|
||||
|
||||
const yandexId = analytics.yandexMetrikaId;
|
||||
const mailRuId = analytics.topMailRuId;
|
||||
---
|
||||
|
||||
<!-- Yandex.Metrika counter -->
|
||||
<script type="text/javascript" define:vars={{ yandexId }}>
|
||||
(function (m, e, t, r, i, k, a) {
|
||||
m[i] =
|
||||
m[i] ||
|
||||
function () {
|
||||
(m[i].a = m[i].a || []).push(arguments);
|
||||
};
|
||||
m[i].l = 1 * new Date();
|
||||
for (var j = 0; j < document.scripts.length; j++) {
|
||||
if (document.scripts[j].src === r) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
((k = e.createElement(t)),
|
||||
(a = e.getElementsByTagName(t)[0]),
|
||||
(k.async = 1),
|
||||
(k.src = r),
|
||||
a.parentNode.insertBefore(k, a));
|
||||
})(
|
||||
window,
|
||||
document,
|
||||
"script",
|
||||
"https://mc.yandex.ru/metrika/tag.js?id=" + String(yandexId),
|
||||
"ym",
|
||||
);
|
||||
ym(Number(yandexId), "init", {
|
||||
ssr: true,
|
||||
webvisor: true,
|
||||
clickmap: true,
|
||||
ecommerce: "dataLayer",
|
||||
referrer: document.referrer,
|
||||
url: location.href,
|
||||
accurateTrackBounce: true,
|
||||
trackLinks: true,
|
||||
});
|
||||
</script>
|
||||
<noscript
|
||||
><div>
|
||||
<img
|
||||
src={`https://mc.yandex.ru/watch/${yandexId}`}
|
||||
style="position:absolute; left:-9999px;"
|
||||
alt=""
|
||||
/>
|
||||
</div></noscript
|
||||
>
|
||||
<!-- /Yandex.Metrika counter -->
|
||||
|
||||
<!-- Top.Mail.Ru counter -->
|
||||
<script type="text/javascript" define:vars={{ mailRuId }}>
|
||||
var _tmr = window._tmr || (window._tmr = []);
|
||||
_tmr.push({
|
||||
id: String(mailRuId),
|
||||
type: "pageView",
|
||||
start: new Date().getTime(),
|
||||
});
|
||||
(function (d, w, id) {
|
||||
if (d.getElementById(id)) return;
|
||||
var ts = d.createElement("script");
|
||||
ts.type = "text/javascript";
|
||||
ts.async = true;
|
||||
ts.id = id;
|
||||
ts.src = "https://top-fwz1.mail.ru/js/code.js";
|
||||
var f = function () {
|
||||
var s = d.getElementsByTagName("script")[0];
|
||||
s.parentNode.insertBefore(ts, s);
|
||||
};
|
||||
if (w.opera == "[object Opera]") {
|
||||
d.addEventListener("DOMContentLoaded", f, false);
|
||||
} else {
|
||||
f();
|
||||
}
|
||||
})(document, window, "tmr-code");
|
||||
</script>
|
||||
<noscript
|
||||
><div>
|
||||
<img
|
||||
src={`https://top-fwz1.mail.ru/counter?id=${mailRuId};js=na`}
|
||||
style="position:absolute;left:-9999px;"
|
||||
alt="Top.Mail.Ru"
|
||||
/>
|
||||
</div></noscript
|
||||
>
|
||||
<!-- /Top.Mail.Ru counter -->
|
||||
@@ -8,7 +8,8 @@ interface Props {
|
||||
lang?: Lang;
|
||||
}
|
||||
|
||||
const { generator, lang = 'en' } = Astro.props;
|
||||
const { generator, lang: langProp } = Astro.props;
|
||||
const lang = (langProp || (Astro.currentLocale as Lang) || 'en');
|
||||
const T = useT(lang);
|
||||
const { slug, icon, status } = generator;
|
||||
const isLive = status === 'live';
|
||||
|
||||
@@ -1,29 +1,27 @@
|
||||
---
|
||||
interface Props {
|
||||
lang: 'en' | 'ru';
|
||||
alternatePath: string;
|
||||
}
|
||||
|
||||
const { lang, alternatePath } = Astro.props;
|
||||
const targetLang = lang === 'en' ? 'ru' : 'en';
|
||||
const currentPath = Astro.url.pathname;
|
||||
const isRu = currentPath.startsWith('/ru');
|
||||
const alternatePath = isRu
|
||||
? (currentPath.replace(/^\/ru/, '') || '/')
|
||||
: ('/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">
|
||||
<a
|
||||
href={lang === 'ru' ? alternatePath : '#'}
|
||||
href={isRu ? alternatePath : '#'}
|
||||
id="lang-en"
|
||||
class={`px-2 py-1 rounded-md transition-colors ${lang === 'en' ? 'bg-[#534AB7] text-white' : 'text-zinc-400 hover:text-zinc-200'}`}
|
||||
aria-current={lang === 'en' ? 'true' : undefined}
|
||||
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'}
|
||||
data-target-lang="en"
|
||||
data-alternate={alternatePath}
|
||||
data-alternate={isRu ? alternatePath : ''}
|
||||
>EN</a>
|
||||
<a
|
||||
href={lang === 'en' ? alternatePath : '#'}
|
||||
href={isRu ? '#' : alternatePath}
|
||||
id="lang-ru"
|
||||
class={`px-2 py-1 rounded-md transition-colors ${lang === 'ru' ? 'bg-[#534AB7] text-white' : 'text-zinc-400 hover:text-zinc-200'}`}
|
||||
aria-current={lang === 'ru' ? 'true' : undefined}
|
||||
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}
|
||||
data-target-lang="ru"
|
||||
data-alternate={alternatePath}
|
||||
data-alternate={isRu ? '' : alternatePath}
|
||||
>RU</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineCollection } from "astro:content";
|
||||
import { generatorSchema } from "@/lib/generator-schema";
|
||||
|
||||
const generators = defineCollection({
|
||||
type: "data",
|
||||
schema: generatorSchema,
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
generators,
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"slug": "cards",
|
||||
"title": "Card",
|
||||
"description": "Draw a random playing card from a standard deck.",
|
||||
"icon": "square-stack",
|
||||
"status": "live",
|
||||
"seoTitle": "Random Card Generator — Draw from a Deck | Randify",
|
||||
"seoDescription": "Draw random playing cards from a standard 52-card deck. Pick any number of cards, with or without replacement. Free online card picker.",
|
||||
"ruTitle": "Карта",
|
||||
"ruDescription": "Вытащите случайную карту из стандартной колоды.",
|
||||
"ruSeoTitle": "Генератор случайных карт | Randify",
|
||||
"ruSeoDescription": "Тяните случайные карты из колоды 52 карт. С повторениями или без. Бесплатный онлайн-генератор карт.",
|
||||
"pageTitle": "Card Generator",
|
||||
"ruPageTitle": "Генератор карт",
|
||||
"howTo": [
|
||||
"Set how many cards to draw (1–52).",
|
||||
"Toggle Allow duplicates if you want cards to repeat.",
|
||||
"Click Draw — cards are shown as mini card tiles.",
|
||||
"Click Copy to copy the result (e.g. A♠, K♥) to your clipboard."
|
||||
],
|
||||
"whenTo": [
|
||||
"Drawing a random hand for card games.",
|
||||
"Practising card probability and statistics.",
|
||||
"Simulating a random card pull for magic tricks or puzzles.",
|
||||
"Running fair draws without a physical deck."
|
||||
],
|
||||
"ruHowTo": [
|
||||
"Укажите количество карт для розыгрыша.",
|
||||
"Включите «Разрешить повторения», если нужны дубли.",
|
||||
"Нажмите «Тянуть» — карты появятся на экране.",
|
||||
"Нажмите «Копировать», чтобы скопировать результат."
|
||||
],
|
||||
"ruWhenTo": [
|
||||
"Случайная раздача карт для карточных игр.",
|
||||
"Выбор случайных карт для гаданий.",
|
||||
"Генерация случайных комбинаций карт для тестирования."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"slug": "coin",
|
||||
"title": "Coin Flip",
|
||||
"description": "Flip one or more coins and see heads or tails.",
|
||||
"icon": "circle-dollar-sign",
|
||||
"status": "live",
|
||||
"seoTitle": "Coin Flip Online — Heads or Tails | Randify",
|
||||
"seoDescription": "Flip a virtual coin online. Flip multiple coins at once and see how many heads and tails you get. Free random coin toss simulator.",
|
||||
"ruTitle": "Монетка",
|
||||
"ruDescription": "Подбросьте одну или несколько монет — орёл или решка.",
|
||||
"ruSeoTitle": "Подбросить монетку онлайн — орёл или решка | Randify",
|
||||
"ruSeoDescription": "Подбрасывайте виртуальную монету онлайн. Одну или несколько сразу. Бесплатный симулятор подбрасывания монеты.",
|
||||
"pageTitle": "Coin Flip",
|
||||
"ruPageTitle": "Подбросить монетку",
|
||||
"howTo": [
|
||||
"Drag the slider to choose how many coins to flip (1–20).",
|
||||
"Click Flip — each coin shows H for heads or T for tails.",
|
||||
"When flipping more than one coin, the heads/tails count is shown below.",
|
||||
"Click Copy to copy all results to the clipboard."
|
||||
],
|
||||
"whenTo": [
|
||||
"Making a quick heads-or-tails decision.",
|
||||
"Simulating a coin toss for a sports match or game.",
|
||||
"Demonstrating probability and the law of large numbers.",
|
||||
"Settling a fair 50/50 choice between two options."
|
||||
],
|
||||
"ruHowTo": [
|
||||
"Выберите количество монет с помощью ползунка.",
|
||||
"Нажмите «Подбросить» — орёл или решка.",
|
||||
"При нескольких монетах отображается итоговый счёт.",
|
||||
"Нажмите «Копировать», чтобы сохранить результат."
|
||||
],
|
||||
"ruWhenTo": [
|
||||
"Принятие решений по принципу «орёл или решка».",
|
||||
"Определение очерёдности в игре.",
|
||||
"Симуляция случайных событий с равной вероятностью."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"slug": "colors",
|
||||
"title": "Color",
|
||||
"description": "Generate a random color in HEX, RGB, or HSL format.",
|
||||
"icon": "palette",
|
||||
"status": "live",
|
||||
"seoTitle": "Random Color Generator — HEX, RGB, HSL | Randify",
|
||||
"seoDescription": "Get a random color in HEX, RGB or HSL format with one click. Free online color randomizer for designers and developers.",
|
||||
"ruTitle": "Цвет",
|
||||
"ruDescription": "Генерируйте случайный цвет в форматах HEX, RGB или HSL.",
|
||||
"ruSeoTitle": "Генератор случайных цветов — HEX, RGB, HSL | Randify",
|
||||
"ruSeoDescription": "Получайте случайные цвета в форматах HEX, RGB и HSL одним нажатием. Бесплатный рандомайзер цветов для дизайнеров.",
|
||||
"pageTitle": "Color Generator",
|
||||
"ruPageTitle": "Генератор цветов",
|
||||
"howTo": [
|
||||
"Click Generate to produce a random color.",
|
||||
"Switch between HEX, RGB, and HSL tabs to see the value in your preferred format.",
|
||||
"Click the color value or the copy icon to copy it to the clipboard."
|
||||
],
|
||||
"whenTo": [
|
||||
"Finding inspiration for a new design palette.",
|
||||
"Picking a placeholder color while prototyping.",
|
||||
"Selecting a random theme color for a project or presentation.",
|
||||
"Teaching or exploring color theory and formats."
|
||||
],
|
||||
"ruHowTo": [
|
||||
"Нажмите «Сгенерировать», чтобы получить случайный цвет.",
|
||||
"Цвет отображается в форматах HEX, RGB и HSL одновременно.",
|
||||
"Нажмите на иконку копирования рядом с нужным форматом."
|
||||
],
|
||||
"ruWhenTo": [
|
||||
"Поиск вдохновения для цветовой палитры.",
|
||||
"Случайный выбор цвета для дизайна или иллюстрации.",
|
||||
"Генерация тестовых данных с цветовыми значениями."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"slug": "dice",
|
||||
"title": "Dice",
|
||||
"description": "Roll one or more dice with any number of sides.",
|
||||
"icon": "dice-6",
|
||||
"status": "live",
|
||||
"seoTitle": "Dice Roller — d4 d6 d20 + Full Notation | D&D Dice | Randify",
|
||||
"seoDescription": "Free online dice roller with full XdY+Z notation, keep/drop (4d6dl1), exploding dice (3d6!), reroll, advantage/disadvantage, and roll history. Perfect for D&D, Pathfinder, and Savage Worlds.",
|
||||
"ruTitle": "Кубик",
|
||||
"ruDescription": "Бросьте один или несколько кубиков с любым числом граней.",
|
||||
"ruSeoTitle": "Бросить кубик онлайн — d4 d6 d8 d10 d12 d20 | D&D | Randify",
|
||||
"ruSeoDescription": "Бесплатный онлайн-бросок кубиков с полной нотацией XdY+Z, keep/drop (4d6dl1), взрывающимися кубиками (3d6!), перебросом, преимуществом/помехой и историей. Для D&D, Pathfinder и Savage Worlds.",
|
||||
"pageTitle": "Dice Generator",
|
||||
"ruPageTitle": "Бросить кубик онлайн",
|
||||
"howTo": [
|
||||
"Type a notation like 2d6+3 or 1d20 — or use the die buttons and count.",
|
||||
"Pick a mode: Normal, Advantage, or Disadvantage (single die only).",
|
||||
"Click Roll — each die result appears with the total sum.",
|
||||
"Maximum rolls are highlighted in purple; ones are highlighted in red."
|
||||
],
|
||||
"whenTo": [
|
||||
"Rolling dice for D&D, Pathfinder, or any tabletop RPG.",
|
||||
"Using dice notation shortcuts like 2d6+3 or 1d20 for fast rolling.",
|
||||
"Rolling with advantage or disadvantage for D&D 5e mechanics.",
|
||||
"Generating character stats with 4d6 drop-lowest method.",
|
||||
"Rolling exploding dice for Savage Worlds or house rules (e.g. 3d6!).",
|
||||
"Using keep/drop modifiers like 4d6kh3 to keep highest rolls."
|
||||
],
|
||||
"ruHowTo": [
|
||||
"Введите нотацию, например 2d6+3 или 1d20 — или настройте кубики вручную.",
|
||||
"Выберите режим: Обычный, Преимущество или Помеха (только для одного кубика).",
|
||||
"Нажмите «Бросить» — результаты отобразятся мгновенно.",
|
||||
"Максимумы подсвечены фиолетовым, единицы — красным."
|
||||
],
|
||||
"ruWhenTo": [
|
||||
"Броски в настольных ролевых играх (D&D и другие).",
|
||||
"Быстрые броски через нотацию: 2d6+3, 1d20 и любые другие.",
|
||||
"Преимущество и помеха для механик D&D 5e.",
|
||||
"Бросок характеристик: 4d6, отбросить наименьший (4d6dl1).",
|
||||
"Взрывающиеся кубики для Savage Worlds или хаус-рулов (например, 3d6!).",
|
||||
"Использование keep/drop модификаторов типа 4d6kh3 (оставить три лучших)."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"slug": "list",
|
||||
"title": "List Picker",
|
||||
"description": "Paste a list of items and pick random winners.",
|
||||
"icon": "list",
|
||||
"status": "live",
|
||||
"seoTitle": "Random List Picker — Pick a Random Winner | Randify",
|
||||
"seoDescription": "Paste any list of names or items and pick random winners instantly. Great for giveaways, choosing who goes first, or any random selection.",
|
||||
"ruTitle": "Список",
|
||||
"ruDescription": "Вставьте список элементов и выберите случайных победителей.",
|
||||
"ruSeoTitle": "Случайный выбор из списка | Randify",
|
||||
"ruSeoDescription": "Вставьте список имён или вариантов и выберите случайных победителей мгновенно. Идеально для розыгрышей и жеребьёвок.",
|
||||
"pageTitle": "List Picker",
|
||||
"ruPageTitle": "Случайный выбор из списка",
|
||||
"howTo": [
|
||||
"Type or paste your items into the text area — one item per line.",
|
||||
"Set how many items to pick in the Pick field.",
|
||||
"Toggle Allow duplicates if the same item can be chosen more than once.",
|
||||
"Click Pick (or press Ctrl+Enter) — the selected items appear as chips."
|
||||
],
|
||||
"whenTo": [
|
||||
"Choosing a random winner for a giveaway or contest.",
|
||||
"Picking who presents first in a meeting or class.",
|
||||
"Organising a Secret Santa or gift exchange draw.",
|
||||
"Selecting a random movie, restaurant, or activity from a list."
|
||||
],
|
||||
"ruHowTo": [
|
||||
"Введите список элементов — по одному на строку.",
|
||||
"Укажите, сколько элементов выбрать.",
|
||||
"При необходимости разрешите повторения.",
|
||||
"Нажмите «Выбрать» — результаты появятся на экране."
|
||||
],
|
||||
"ruWhenTo": [
|
||||
"Розыгрыш победителей конкурса.",
|
||||
"Случайное распределение задач между участниками команды.",
|
||||
"Выбор случайного варианта из списка."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"slug": "lottery",
|
||||
"title": "Lottery",
|
||||
"description": "Draw a set of unique numbers for lottery-style picks.",
|
||||
"icon": "ticket",
|
||||
"status": "live",
|
||||
"seoTitle": "Lottery Number Generator — Random Pick | Randify",
|
||||
"seoDescription": "Draw a set of unique random lottery numbers from any range. Perfect for lotteries, raffles, and lucky draws. Free online lottery picker.",
|
||||
"ruTitle": "Лотерея",
|
||||
"ruDescription": "Вытащите набор уникальных чисел в стиле лотереи.",
|
||||
"ruSeoTitle": "Генератор лотерейных номеров | Randify",
|
||||
"ruSeoDescription": "Тяните уникальные случайные числа для лотереи, розыгрыша или жеребьёвки. Бесплатный онлайн-генератор лотерейных номеров.",
|
||||
"pageTitle": "Lottery Generator",
|
||||
"ruPageTitle": "Генератор лотереи",
|
||||
"howTo": [
|
||||
"Set the From and To fields to define your number range (e.g. 1 to 49).",
|
||||
"Enter how many numbers to pick in the Pick field.",
|
||||
"Click Draw — a sorted set of unique numbers appears.",
|
||||
"Click Copy to copy all numbers to the clipboard."
|
||||
],
|
||||
"whenTo": [
|
||||
"Generating lottery or lotto ticket numbers.",
|
||||
"Running a fair raffle draw with unique entries.",
|
||||
"Selecting a random sample from a numbered list.",
|
||||
"Choosing lottery numbers for Powerball, EuroMillions, or local draws."
|
||||
],
|
||||
"ruHowTo": [
|
||||
"Укажите диапазон чисел в полях «От» и «До».",
|
||||
"Задайте количество чисел для розыгрыша.",
|
||||
"Нажмите «Тянуть» — выпавшие числа появятся на экране.",
|
||||
"Нажмите «Копировать», чтобы сохранить результат."
|
||||
],
|
||||
"ruWhenTo": [
|
||||
"Розыгрыш лотерейных номеров.",
|
||||
"Жеребьёвка участников конкурса.",
|
||||
"Случайный выбор нескольких уникальных чисел."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"slug": "numbers",
|
||||
"title": "Number",
|
||||
"description": "Pick a random number within any range you choose.",
|
||||
"icon": "hash",
|
||||
"status": "live",
|
||||
"seoTitle": "Random Number Generator | Randify",
|
||||
"seoDescription": "Generate a random number between any two values instantly. Free online random number picker — perfect for giveaways, games, and decisions.",
|
||||
"ruTitle": "Число",
|
||||
"ruDescription": "Выберите случайное число в любом диапазоне.",
|
||||
"ruSeoTitle": "Генератор случайных чисел | Randify",
|
||||
"ruSeoDescription": "Генерируйте случайное число в любом диапазоне мгновенно. Идеально для розыгрышей, игр и принятия решений.",
|
||||
"pageTitle": "Number Generator",
|
||||
"ruPageTitle": "Генератор чисел",
|
||||
"howTo": [
|
||||
"Enter the minimum value in the From field.",
|
||||
"Enter the maximum value in the To field.",
|
||||
"Click Generate — a random number within your range appears instantly.",
|
||||
"Click the number to copy it to the clipboard."
|
||||
],
|
||||
"whenTo": [
|
||||
"Picking a winner in a giveaway or raffle.",
|
||||
"Choosing a random starting player in a board game.",
|
||||
"Making a quick decision between numbered options.",
|
||||
"Generating test data with values in a specific range."
|
||||
],
|
||||
"ruHowTo": [
|
||||
"Введите минимальное значение в поле «От».",
|
||||
"Введите максимальное значение в поле «До».",
|
||||
"Нажмите «Сгенерировать» — случайное число появится мгновенно.",
|
||||
"Нажмите на число, чтобы скопировать его в буфер обмена."
|
||||
],
|
||||
"ruWhenTo": [
|
||||
"Выбор победителя в розыгрыше или конкурсе.",
|
||||
"Определение случайного первого игрока в настольной игре.",
|
||||
"Быстрое принятие решения между пронумерованными вариантами.",
|
||||
"Генерация тестовых данных с числами в заданном диапазоне."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"slug": "password",
|
||||
"title": "Password",
|
||||
"description": "Create a strong, random password with custom rules.",
|
||||
"icon": "lock",
|
||||
"status": "live",
|
||||
"seoTitle": "Random Password Generator | Randify",
|
||||
"seoDescription": "Create a strong random password with custom length, uppercase, lowercase, digits and symbols. Free and secure — runs entirely in your browser.",
|
||||
"ruTitle": "Пароль",
|
||||
"ruDescription": "Создайте надёжный случайный пароль с настраиваемыми правилами.",
|
||||
"ruSeoTitle": "Генератор случайных паролей | Randify",
|
||||
"ruSeoDescription": "Создавайте надёжные пароли с заглавными буквами, цифрами и символами. Бесплатно и безопасно — работает прямо в браузере.",
|
||||
"pageTitle": "Password Generator",
|
||||
"ruPageTitle": "Генератор паролей",
|
||||
"howTo": [
|
||||
"Drag the length slider to set how many characters you need (4–64).",
|
||||
"Toggle uppercase, lowercase, digits, and symbols on or off.",
|
||||
"Click Generate — a new password appears instantly.",
|
||||
"Click the password to copy it to your clipboard."
|
||||
],
|
||||
"whenTo": [
|
||||
"Creating a strong password for a new account.",
|
||||
"Generating a temporary access credential.",
|
||||
"Producing a random secret key for development or testing.",
|
||||
"Replacing a weak or reused password."
|
||||
],
|
||||
"ruHowTo": [
|
||||
"Задайте длину пароля с помощью ползунка.",
|
||||
"Выберите типы символов: заглавные, строчные, цифры, спецсимволы.",
|
||||
"Нажмите «Сгенерировать» — готовый пароль появится на экране.",
|
||||
"Нажмите на пароль, чтобы скопировать его."
|
||||
],
|
||||
"ruWhenTo": [
|
||||
"Создание надёжного пароля для нового аккаунта.",
|
||||
"Генерация секретного ключа или токена.",
|
||||
"Регулярное обновление паролей для повышения безопасности."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"slug": "uuid",
|
||||
"title": "UUID / Token",
|
||||
"description": "Generate UUIDs, hex tokens, or random base64 strings.",
|
||||
"icon": "fingerprint",
|
||||
"status": "live",
|
||||
"seoTitle": "UUID Generator — UUID v4, Hex & Base64 Tokens | Randify",
|
||||
"seoDescription": "Generate random UUID v4, hex tokens, or base64 strings online. Cryptographically secure, runs in your browser. Free UUID and token generator.",
|
||||
"ruTitle": "UUID / Токен",
|
||||
"ruDescription": "Генерируйте UUID, hex-токены или случайные Base64-строки.",
|
||||
"ruSeoTitle": "Генератор UUID и токенов | Randify",
|
||||
"ruSeoDescription": "Генерируйте UUID v4, hex-токены и Base64-строки онлайн. Криптографически безопасно, работает в браузере.",
|
||||
"pageTitle": "UUID / Token",
|
||||
"ruPageTitle": "Генератор UUID и токенов",
|
||||
"howTo": [
|
||||
"Choose a token type: UUID v4, Hex, or Base64.",
|
||||
"For Hex and Base64, set the byte length (4–64 bytes).",
|
||||
"Enter how many tokens to generate (1–20).",
|
||||
"Click Generate, then Copy all to copy every token to the clipboard."
|
||||
],
|
||||
"whenTo": [
|
||||
"Generating unique IDs for database records or API resources.",
|
||||
"Creating random session tokens or API keys during development.",
|
||||
"Producing test data with realistic-looking identifiers.",
|
||||
"Generating a one-time secret for environment variables or config files."
|
||||
],
|
||||
"ruHowTo": [
|
||||
"Выберите тип: UUID v4, Hex или Base64.",
|
||||
"Для Hex и Base64 задайте длину в байтах.",
|
||||
"Укажите количество токенов.",
|
||||
"Нажмите «Сгенерировать» и скопируйте результат."
|
||||
],
|
||||
"ruWhenTo": [
|
||||
"Генерация уникальных идентификаторов для баз данных.",
|
||||
"Создание токенов для API и сессий.",
|
||||
"Генерация случайных ключей шифрования."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"slug": "wheel",
|
||||
"title": "Spin the Wheel",
|
||||
"description": "Add your items, spin the wheel, and let chance decide.",
|
||||
"icon": "pie-chart",
|
||||
"status": "live",
|
||||
"seoTitle": "Spin the Wheel — Random Picker | Randify",
|
||||
"seoDescription": "Spin a customizable wheel of fortune to pick a random winner, choice, or option. Add your own items and let the wheel decide.",
|
||||
"ruTitle": "Колесо фортуны",
|
||||
"ruDescription": "Добавьте элементы, крутите колесо и пусть случай решит.",
|
||||
"ruSeoTitle": "Колесо фортуны онлайн — случайный выбор | Randify",
|
||||
"ruSeoDescription": "Крутите колесо фортуны онлайн. Добавьте свои варианты и пусть случай решит победителя.",
|
||||
"pageTitle": "Spin the Wheel",
|
||||
"ruPageTitle": "Колесо фортуны онлайн",
|
||||
"howTo": [
|
||||
"Type your items into the text box — one per line.",
|
||||
"Click \"Update wheel\" to apply changes.",
|
||||
"Hit \"Spin\" and watch the wheel decide.",
|
||||
"Copy the result or spin again."
|
||||
],
|
||||
"whenTo": [
|
||||
"Picking a winner for a giveaway or raffle.",
|
||||
"Deciding whose turn it is in a game or meeting.",
|
||||
"Choosing a random activity, movie, or restaurant.",
|
||||
"Any decision you want to leave to chance."
|
||||
],
|
||||
"ruHowTo": [
|
||||
"Введите варианты в поле — по одному на строку (от 2 до 24).",
|
||||
"Колесо обновляется автоматически.",
|
||||
"Нажмите «Крутить» и дождитесь результата.",
|
||||
"Победитель выделяется — скопируйте его кнопкой «Копировать»."
|
||||
],
|
||||
"ruWhenTo": [
|
||||
"Случайный выбор победителя розыгрыша.",
|
||||
"Принятие решений в команде.",
|
||||
"Распределение ролей или задач между участниками."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const siteUrl = 'https://randify.pro';
|
||||
|
||||
export const analytics = {
|
||||
yandexMetrikaId: '109130319',
|
||||
topMailRuId: '3765043',
|
||||
} as const;
|
||||
+19
-144
@@ -1,146 +1,21 @@
|
||||
export interface Generator {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
status: 'live' | 'coming-soon';
|
||||
seoTitle: string;
|
||||
seoDescription: string;
|
||||
ruTitle: string;
|
||||
ruDescription: string;
|
||||
ruSeoTitle: string;
|
||||
ruSeoDescription: string;
|
||||
import { generatorSchema } from "@/lib/generator-schema";
|
||||
import type { Generator } from "@/lib/generator-schema";
|
||||
|
||||
const modules = import.meta.glob<{ default: unknown }>(
|
||||
"../content/generators/*.json",
|
||||
{
|
||||
eager: true,
|
||||
},
|
||||
);
|
||||
|
||||
export const generators: Generator[] = Object.values(modules).map((mod) =>
|
||||
generatorSchema.parse(mod.default),
|
||||
);
|
||||
|
||||
export function findGenerator(slug: string): Generator {
|
||||
const g = generators.find((gen) => gen.slug === slug);
|
||||
if (!g) throw new Error(`Generator not found: ${slug}`);
|
||||
return g;
|
||||
}
|
||||
|
||||
export const generators: Generator[] = [
|
||||
{
|
||||
slug: 'numbers',
|
||||
title: 'Number',
|
||||
description: 'Pick a random number within any range you choose.',
|
||||
icon: 'hash',
|
||||
status: 'live',
|
||||
seoTitle: 'Random Number Generator | Randify',
|
||||
seoDescription: 'Generate a random number between any two values instantly. Free online random number picker — perfect for giveaways, games, and decisions.',
|
||||
ruTitle: 'Число',
|
||||
ruDescription: 'Выберите случайное число в любом диапазоне.',
|
||||
ruSeoTitle: 'Генератор случайных чисел | Randify',
|
||||
ruSeoDescription: 'Генерируйте случайное число в любом диапазоне мгновенно. Идеально для розыгрышей, игр и принятия решений.',
|
||||
},
|
||||
{
|
||||
slug: 'dice',
|
||||
title: 'Dice',
|
||||
description: 'Roll one or more dice with any number of sides.',
|
||||
icon: 'dice-6',
|
||||
status: 'live',
|
||||
seoTitle: 'Dice Roller — d4 d6 d20 + Full Notation | D&D Dice | Randify',
|
||||
seoDescription: 'Free online dice roller with full XdY+Z notation, keep/drop (4d6dl1), exploding dice (3d6!), reroll, advantage/disadvantage, and roll history. Perfect for D&D, Pathfinder, and Savage Worlds.',
|
||||
ruTitle: 'Кубик',
|
||||
ruDescription: 'Бросьте один или несколько кубиков с любым числом граней.',
|
||||
ruSeoTitle: 'Бросить кубик онлайн — d4 d6 d8 d10 d12 d20 | D&D | Randify',
|
||||
ruSeoDescription: 'Бесплатный онлайн-бросок кубиков с полной нотацией XdY+Z, keep/drop (4d6dl1), взрывающимися кубиками (3d6!), перебросом, преимуществом/помехой и историей. Для D&D, Pathfinder и Savage Worlds.',
|
||||
},
|
||||
{
|
||||
slug: 'wheel',
|
||||
title: 'Spin the Wheel',
|
||||
description: 'Add your items, spin the wheel, and let chance decide.',
|
||||
icon: 'pie-chart',
|
||||
status: 'live',
|
||||
seoTitle: 'Spin the Wheel — Random Picker | Randify',
|
||||
seoDescription: 'Spin a customizable wheel of fortune to pick a random winner, choice, or option. Add your own items and let the wheel decide.',
|
||||
ruTitle: 'Колесо фортуны',
|
||||
ruDescription: 'Добавьте элементы, крутите колесо и пусть случай решит.',
|
||||
ruSeoTitle: 'Колесо фортуны онлайн — случайный выбор | Randify',
|
||||
ruSeoDescription: 'Крутите колесо фортуны онлайн. Добавьте свои варианты и пусть случай решит победителя.',
|
||||
},
|
||||
{
|
||||
slug: 'colors',
|
||||
title: 'Color',
|
||||
description: 'Generate a random color in HEX, RGB, or HSL format.',
|
||||
icon: 'palette',
|
||||
status: 'live',
|
||||
seoTitle: 'Random Color Generator — HEX, RGB, HSL | Randify',
|
||||
seoDescription: 'Get a random color in HEX, RGB or HSL format with one click. Free online color randomizer for designers and developers.',
|
||||
ruTitle: 'Цвет',
|
||||
ruDescription: 'Генерируйте случайный цвет в форматах HEX, RGB или HSL.',
|
||||
ruSeoTitle: 'Генератор случайных цветов — HEX, RGB, HSL | Randify',
|
||||
ruSeoDescription: 'Получайте случайные цвета в форматах HEX, RGB и HSL одним нажатием. Бесплатный рандомайзер цветов для дизайнеров.',
|
||||
},
|
||||
{
|
||||
slug: 'password',
|
||||
title: 'Password',
|
||||
description: 'Create a strong, random password with custom rules.',
|
||||
icon: 'lock',
|
||||
status: 'live',
|
||||
seoTitle: 'Random Password Generator | Randify',
|
||||
seoDescription: 'Create a strong random password with custom length, uppercase, lowercase, digits and symbols. Free and secure — runs entirely in your browser.',
|
||||
ruTitle: 'Пароль',
|
||||
ruDescription: 'Создайте надёжный случайный пароль с настраиваемыми правилами.',
|
||||
ruSeoTitle: 'Генератор случайных паролей | Randify',
|
||||
ruSeoDescription: 'Создавайте надёжные пароли с заглавными буквами, цифрами и символами. Бесплатно и безопасно — работает прямо в браузере.',
|
||||
},
|
||||
{
|
||||
slug: 'lottery',
|
||||
title: 'Lottery',
|
||||
description: 'Draw a set of unique numbers for lottery-style picks.',
|
||||
icon: 'ticket',
|
||||
status: 'live',
|
||||
seoTitle: 'Lottery Number Generator — Random Pick | Randify',
|
||||
seoDescription: 'Draw a set of unique random lottery numbers from any range. Perfect for lotteries, raffles, and lucky draws. Free online lottery picker.',
|
||||
ruTitle: 'Лотерея',
|
||||
ruDescription: 'Вытащите набор уникальных чисел в стиле лотереи.',
|
||||
ruSeoTitle: 'Генератор лотерейных номеров | Randify',
|
||||
ruSeoDescription: 'Тяните уникальные случайные числа для лотереи, розыгрыша или жеребьёвки. Бесплатный онлайн-генератор лотерейных номеров.',
|
||||
},
|
||||
{
|
||||
slug: 'cards',
|
||||
title: 'Card',
|
||||
description: 'Draw a random playing card from a standard deck.',
|
||||
icon: 'square-stack',
|
||||
status: 'live',
|
||||
seoTitle: 'Random Card Generator — Draw from a Deck | Randify',
|
||||
seoDescription: 'Draw random playing cards from a standard 52-card deck. Pick any number of cards, with or without replacement. Free online card picker.',
|
||||
ruTitle: 'Карта',
|
||||
ruDescription: 'Вытащите случайную карту из стандартной колоды.',
|
||||
ruSeoTitle: 'Генератор случайных карт | Randify',
|
||||
ruSeoDescription: 'Тяните случайные карты из колоды 52 карт. С повторениями или без. Бесплатный онлайн-генератор карт.',
|
||||
},
|
||||
{
|
||||
slug: 'coin',
|
||||
title: 'Coin Flip',
|
||||
description: 'Flip one or more coins and see heads or tails.',
|
||||
icon: 'circle-dollar-sign',
|
||||
status: 'live',
|
||||
seoTitle: 'Coin Flip Online — Heads or Tails | Randify',
|
||||
seoDescription: 'Flip a virtual coin online. Flip multiple coins at once and see how many heads and tails you get. Free random coin toss simulator.',
|
||||
ruTitle: 'Монетка',
|
||||
ruDescription: 'Подбросьте одну или несколько монет — орёл или решка.',
|
||||
ruSeoTitle: 'Подбросить монетку онлайн — орёл или решка | Randify',
|
||||
ruSeoDescription: 'Подбрасывайте виртуальную монету онлайн. Одну или несколько сразу. Бесплатный симулятор подбрасывания монеты.',
|
||||
},
|
||||
{
|
||||
slug: 'list',
|
||||
title: 'List Picker',
|
||||
description: 'Paste a list of items and pick random winners.',
|
||||
icon: 'list',
|
||||
status: 'live',
|
||||
seoTitle: 'Random List Picker — Pick a Random Winner | Randify',
|
||||
seoDescription: 'Paste any list of names or items and pick random winners instantly. Great for giveaways, choosing who goes first, or any random selection.',
|
||||
ruTitle: 'Список',
|
||||
ruDescription: 'Вставьте список элементов и выберите случайных победителей.',
|
||||
ruSeoTitle: 'Случайный выбор из списка | Randify',
|
||||
ruSeoDescription: 'Вставьте список имён или вариантов и выберите случайных победителей мгновенно. Идеально для розыгрышей и жеребьёвок.',
|
||||
},
|
||||
{
|
||||
slug: 'uuid',
|
||||
title: 'UUID / Token',
|
||||
description: 'Generate UUIDs, hex tokens, or random base64 strings.',
|
||||
icon: 'fingerprint',
|
||||
status: 'live',
|
||||
seoTitle: 'UUID Generator — UUID v4, Hex & Base64 Tokens | Randify',
|
||||
seoDescription: 'Generate random UUID v4, hex tokens, or base64 strings online. Cryptographically secure, runs in your browser. Free UUID and token generator.',
|
||||
ruTitle: 'UUID / Токен',
|
||||
ruDescription: 'Генерируйте UUID, hex-токены или случайные Base64-строки.',
|
||||
ruSeoTitle: 'Генератор UUID и токенов | Randify',
|
||||
ruSeoDescription: 'Генерируйте UUID v4, hex-токены и Base64-строки онлайн. Криптографически безопасно, работает в браузере.',
|
||||
},
|
||||
];
|
||||
export type { Generator };
|
||||
|
||||
@@ -1,107 +1,93 @@
|
||||
---
|
||||
import LanguageSwitcher from '../components/LanguageSwitcher.astro';
|
||||
import { useT } from '../i18n/translations';
|
||||
import type { Lang } from '../i18n/translations';
|
||||
import { getRelativeLocaleUrl } from "astro:i18n";
|
||||
import LanguageSwitcher from "../components/LanguageSwitcher.astro";
|
||||
import Analytics from "../components/Analytics.astro";
|
||||
import { useT } from "../i18n/translations";
|
||||
import type { Lang } from "../i18n/translations";
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
lang?: Lang;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const { lang = 'en' } = Astro.props;
|
||||
const lang = (Astro.currentLocale as Lang) || "en";
|
||||
const T = useT(lang);
|
||||
|
||||
const {
|
||||
title = T.defaultTitle,
|
||||
description = T.defaultDesc,
|
||||
} = Astro.props;
|
||||
const { title = T.defaultTitle, description = T.defaultDesc } = Astro.props;
|
||||
|
||||
const currentPath = Astro.url.pathname;
|
||||
const alternatePath = lang === 'ru'
|
||||
? (currentPath.replace(/^\/ru/, '') || '/')
|
||||
: ('/ru' + currentPath);
|
||||
const pathWithoutLocale = Astro.url.pathname.replace(/^\/ru/, "") || "/";
|
||||
const enUrl = `https://randify.pro${getRelativeLocaleUrl("en", pathWithoutLocale)}`;
|
||||
const ruUrl = `https://randify.pro${getRelativeLocaleUrl("ru", pathWithoutLocale)}`;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang={lang}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content={description} />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet" />
|
||||
<link rel="alternate" hreflang="en" href={`https://randify.pro${lang === 'ru' ? alternatePath : currentPath}`} />
|
||||
<link rel="alternate" hreflang="ru" href={`https://randify.pro${lang === 'ru' ? currentPath : alternatePath}`} />
|
||||
<meta name="verification" content="er9ndnv9ih7agmh8" />
|
||||
<title>{title}</title>
|
||||
<!-- Yandex.Metrika counter -->
|
||||
<script type="text/javascript">
|
||||
(function(m,e,t,r,i,k,a){
|
||||
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
||||
m[i].l=1*new Date();
|
||||
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
||||
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
|
||||
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=109130319', 'ym');
|
||||
ym(109130319, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
|
||||
</script>
|
||||
<noscript><div><img src="https://mc.yandex.ru/watch/109130319" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
|
||||
<!-- /Yandex.Metrika counter -->
|
||||
<!-- Top.Mail.Ru counter -->
|
||||
<script type="text/javascript">
|
||||
var _tmr = window._tmr || (window._tmr = []);
|
||||
_tmr.push({id: "3765043", type: "pageView", start: (new Date()).getTime()});
|
||||
(function (d, w, id) {
|
||||
if (d.getElementById(id)) return;
|
||||
var ts = d.createElement("script"); ts.type = "text/javascript"; ts.async = true; ts.id = id;
|
||||
ts.src = "https://top-fwz1.mail.ru/js/code.js";
|
||||
var f = function () {var s = d.getElementsByTagName("script")[0]; s.parentNode.insertBefore(ts, s);};
|
||||
if (w.opera == "[object Opera]") { d.addEventListener("DOMContentLoaded", f, false); } else { f(); }
|
||||
})(document, window, "tmr-code");
|
||||
</script>
|
||||
<noscript><div><img src="https://top-fwz1.mail.ru/counter?id=3765043;js=na" style="position:absolute;left:-9999px;" alt="Top.Mail.Ru" /></div></noscript>
|
||||
<!-- /Top.Mail.Ru counter -->
|
||||
</head>
|
||||
<body class="bg-zinc-950 text-zinc-100 min-h-screen font-sans antialiased">
|
||||
<LanguageSwitcher lang={lang} alternatePath={alternatePath} />
|
||||
<slot />
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content={description} />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="alternate" hreflang="en" href={enUrl} />
|
||||
<link rel="alternate" hreflang="ru" href={ruUrl} />
|
||||
<meta name="verification" content="er9ndnv9ih7agmh8" />
|
||||
<title>{title}</title>
|
||||
<Analytics />
|
||||
</head>
|
||||
<body class="bg-zinc-950 text-zinc-100 min-h-screen font-sans antialiased">
|
||||
<LanguageSwitcher />
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<!-- Auto-detect language on first visit -->
|
||||
<script>
|
||||
if (!localStorage.getItem('lang-pref')) {
|
||||
const browserLang = (navigator.language || '').toLowerCase();
|
||||
if (browserLang.startsWith('ru') && !location.pathname.startsWith('/ru')) {
|
||||
location.replace('/ru' + location.pathname);
|
||||
if (!localStorage.getItem("lang-pref")) {
|
||||
const browserLang = (navigator.language || "").toLowerCase();
|
||||
if (
|
||||
browserLang.startsWith("ru") &&
|
||||
!location.pathname.startsWith("/ru")
|
||||
) {
|
||||
location.replace("/ru" + location.pathname);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style is:global>
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Space Grotesk', sans-serif;
|
||||
}
|
||||
@theme {
|
||||
--font-sans: "Space Grotesk", sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
--accent: #534AB7;
|
||||
}
|
||||
:root {
|
||||
--accent: #534ab7;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-image:
|
||||
radial-gradient(ellipse 90% 45% at 50% -5%, rgba(83, 74, 183, 0.2) 0%, transparent 65%),
|
||||
radial-gradient(ellipse 90% 45% at 50% 105%, rgba(83, 74, 183, 0.13) 0%, transparent 65%);
|
||||
}
|
||||
body {
|
||||
background-image:
|
||||
radial-gradient(
|
||||
ellipse 90% 45% at 50% -5%,
|
||||
rgba(83, 74, 183, 0.2) 0%,
|
||||
transparent 65%
|
||||
),
|
||||
radial-gradient(
|
||||
ellipse 90% 45% at 50% 105%,
|
||||
rgba(83, 74, 183, 0.13) 0%,
|
||||
transparent 65%
|
||||
);
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
input[type="number"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
import BaseLayout from './BaseLayout.astro';
|
||||
import AdBanner from '../components/AdBanner.astro';
|
||||
import SeoBlock from '../components/SeoBlock.astro';
|
||||
import { useT } from '../i18n/translations';
|
||||
import type { Lang } from '../i18n/translations';
|
||||
import type { Generator } from '../data/generators';
|
||||
|
||||
interface Props {
|
||||
generator: Generator;
|
||||
}
|
||||
|
||||
const { generator } = Astro.props;
|
||||
const lang = (Astro.currentLocale as Lang) || 'en';
|
||||
const T = useT(lang);
|
||||
const isRu = lang === 'ru';
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={isRu ? generator.ruSeoTitle : generator.seoTitle}
|
||||
description={isRu ? generator.ruSeoDescription : generator.seoDescription}
|
||||
>
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a
|
||||
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"
|
||||
aria-label={T.backToAll}
|
||||
>
|
||||
<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"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m15 18-6-6 6-6"/>
|
||||
</svg>
|
||||
{T.backToAll}
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span
|
||||
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
|
||||
{isRu ? generator.ruPageTitle : generator.pageTitle}
|
||||
</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{isRu ? generator.ruDescription : generator.description}</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<div class="mt-12 flex justify-center">
|
||||
<AdBanner size="tile" />
|
||||
</div>
|
||||
|
||||
<SeoBlock
|
||||
lang={lang}
|
||||
howTo={isRu ? generator.ruHowTo : generator.howTo}
|
||||
whenTo={isRu ? generator.ruWhenTo : generator.whenTo}
|
||||
/>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const generatorSchema = z
|
||||
.object({
|
||||
slug: z.string(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
icon: z.string(),
|
||||
status: z.enum(["live", "coming-soon"]),
|
||||
seoTitle: z.string(),
|
||||
seoDescription: z.string(),
|
||||
ruTitle: z.string(),
|
||||
ruDescription: z.string(),
|
||||
ruSeoTitle: z.string(),
|
||||
ruSeoDescription: z.string(),
|
||||
pageTitle: z.string(),
|
||||
ruPageTitle: z.string(),
|
||||
howTo: z.array(z.string()),
|
||||
whenTo: z.array(z.string()),
|
||||
ruHowTo: z.array(z.string()),
|
||||
ruWhenTo: z.array(z.string()),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type Generator = z.infer<typeof generatorSchema>;
|
||||
@@ -1,77 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import CardGenerator from '../../components/generators/CardGenerator.astro';
|
||||
import AdBanner from '../../components/AdBanner.astro';
|
||||
import { generators } from '../../data/generators';
|
||||
import SeoBlock from '../../components/SeoBlock.astro';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import CardGenerator from "@/components/generators/CardGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'cards')!;
|
||||
const generator = generators.find((g) => g.slug === "cards")!;
|
||||
---
|
||||
|
||||
<BaseLayout lang="en"
|
||||
title={generator.seoTitle}
|
||||
description={generator.seoDescription}
|
||||
>
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a
|
||||
href="/"
|
||||
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="Back to all generators"
|
||||
>
|
||||
<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"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m15 18-6-6 6-6"/>
|
||||
</svg>
|
||||
All generators
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span
|
||||
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
|
||||
{generator.title} Generator
|
||||
</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<CardGenerator />
|
||||
</main>
|
||||
|
||||
<div class="mt-12 flex justify-center">
|
||||
<AdBanner size="tile" />
|
||||
</div>
|
||||
|
||||
<SeoBlock
|
||||
howTo={[
|
||||
'Set how many cards to draw (1–52).',
|
||||
'Toggle Allow duplicates if you want cards to repeat.',
|
||||
'Click Draw — cards are shown as mini card tiles.',
|
||||
'Click Copy to copy the result (e.g. A♠, K♥) to your clipboard.',
|
||||
]}
|
||||
whenTo={[
|
||||
'Drawing a random hand for card games.',
|
||||
'Practising card probability and statistics.',
|
||||
'Simulating a random card pull for magic tricks or puzzles.',
|
||||
'Running fair draws without a physical deck.',
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<CardGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,56 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import CoinGenerator from '../../components/generators/CoinGenerator.astro';
|
||||
import AdBanner from '../../components/AdBanner.astro';
|
||||
import { generators } from '../../data/generators';
|
||||
import SeoBlock from '../../components/SeoBlock.astro';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import CoinGenerator from "@/components/generators/CoinGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'coin')!;
|
||||
const generator = generators.find((g) => g.slug === "coin")!;
|
||||
---
|
||||
|
||||
<BaseLayout lang="en"
|
||||
title={generator.seoTitle}
|
||||
description={generator.seoDescription}
|
||||
>
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a
|
||||
href="/"
|
||||
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="Back to all generators"
|
||||
>
|
||||
<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" aria-hidden="true">
|
||||
<path d="m15 18-6-6 6-6"/>
|
||||
</svg>
|
||||
All generators
|
||||
</a>
|
||||
</nav>
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">{generator.title}</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
|
||||
</header>
|
||||
<main><CoinGenerator /></main>
|
||||
<div class="mt-12 flex justify-center">
|
||||
<AdBanner size="tile" />
|
||||
</div>
|
||||
|
||||
<SeoBlock
|
||||
howTo={[
|
||||
'Drag the slider to choose how many coins to flip (1–20).',
|
||||
'Click Flip — each coin shows H for heads or T for tails.',
|
||||
'When flipping more than one coin, the heads/tails count is shown below.',
|
||||
'Click Copy to copy all results to the clipboard.',
|
||||
]}
|
||||
whenTo={[
|
||||
'Making a quick heads-or-tails decision.',
|
||||
'Simulating a coin toss for a sports match or game.',
|
||||
'Demonstrating probability and the law of large numbers.',
|
||||
'Settling a fair 50/50 choice between two options.',
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<CoinGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,76 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import ColorGenerator from '../../components/generators/ColorGenerator.astro';
|
||||
import AdBanner from '../../components/AdBanner.astro';
|
||||
import { generators } from '../../data/generators';
|
||||
import SeoBlock from '../../components/SeoBlock.astro';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import ColorGenerator from "@/components/generators/ColorGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'colors')!;
|
||||
const generator = generators.find((g) => g.slug === "colors")!;
|
||||
---
|
||||
|
||||
<BaseLayout lang="en"
|
||||
title={generator.seoTitle}
|
||||
description={generator.seoDescription}
|
||||
>
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a
|
||||
href="/"
|
||||
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="Back to all generators"
|
||||
>
|
||||
<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"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m15 18-6-6 6-6"/>
|
||||
</svg>
|
||||
All generators
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span
|
||||
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
|
||||
{generator.title} Generator
|
||||
</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<ColorGenerator />
|
||||
</main>
|
||||
|
||||
<div class="mt-12 flex justify-center">
|
||||
<AdBanner size="tile" />
|
||||
</div>
|
||||
|
||||
<SeoBlock
|
||||
howTo={[
|
||||
'Click Generate to produce a random color.',
|
||||
'Switch between HEX, RGB, and HSL tabs to see the value in your preferred format.',
|
||||
'Click the color value or the copy icon to copy it to the clipboard.',
|
||||
]}
|
||||
whenTo={[
|
||||
'Finding inspiration for a new design palette.',
|
||||
'Picking a placeholder color while prototyping.',
|
||||
'Selecting a random theme color for a project or presentation.',
|
||||
'Teaching or exploring color theory and formats.',
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<ColorGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,79 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import DiceGenerator from '../../components/generators/DiceGenerator.astro';
|
||||
import AdBanner from '../../components/AdBanner.astro';
|
||||
import { generators } from '../../data/generators';
|
||||
import SeoBlock from '../../components/SeoBlock.astro';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import DiceGenerator from "@/components/generators/DiceGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'dice')!;
|
||||
const generator = generators.find((g) => g.slug === "dice")!;
|
||||
---
|
||||
|
||||
<BaseLayout lang="en"
|
||||
title={generator.seoTitle}
|
||||
description={generator.seoDescription}
|
||||
>
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a
|
||||
href="/"
|
||||
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="Back to all generators"
|
||||
>
|
||||
<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"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m15 18-6-6 6-6"/>
|
||||
</svg>
|
||||
All generators
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span
|
||||
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
|
||||
{generator.title} Generator
|
||||
</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<DiceGenerator />
|
||||
</main>
|
||||
|
||||
<div class="mt-12 flex justify-center">
|
||||
<AdBanner size="tile" />
|
||||
</div>
|
||||
|
||||
<SeoBlock
|
||||
howTo={[
|
||||
'Type a notation like 2d6+3 or 1d20 — or use the die buttons and count.',
|
||||
'Pick a mode: Normal, Advantage, or Disadvantage (single die only).',
|
||||
'Click Roll — each die result appears with the total sum.',
|
||||
'Maximum rolls are highlighted in purple; ones are highlighted in red.',
|
||||
]}
|
||||
whenTo={[
|
||||
'Rolling dice for D&D, Pathfinder, or any tabletop RPG.',
|
||||
'Using dice notation shortcuts like 2d6+3 or 1d20 for fast rolling.',
|
||||
'Rolling with advantage or disadvantage for D&D 5e mechanics.',
|
||||
'Generating character stats with 4d6 drop-lowest method.',
|
||||
'Rolling exploding dice for Savage Worlds or house rules (e.g., 3d6!).',
|
||||
'Using keep/drop modifiers like 4d6kh3 to keep highest rolls.',
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<DiceGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,56 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import ListGenerator from '../../components/generators/ListGenerator.astro';
|
||||
import AdBanner from '../../components/AdBanner.astro';
|
||||
import { generators } from '../../data/generators';
|
||||
import SeoBlock from '../../components/SeoBlock.astro';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import ListGenerator from "@/components/generators/ListGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'list')!;
|
||||
const generator = generators.find((g) => g.slug === "list")!;
|
||||
---
|
||||
|
||||
<BaseLayout lang="en"
|
||||
title={generator.seoTitle}
|
||||
description={generator.seoDescription}
|
||||
>
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a
|
||||
href="/"
|
||||
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="Back to all generators"
|
||||
>
|
||||
<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" aria-hidden="true">
|
||||
<path d="m15 18-6-6 6-6"/>
|
||||
</svg>
|
||||
All generators
|
||||
</a>
|
||||
</nav>
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">{generator.title}</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
|
||||
</header>
|
||||
<main><ListGenerator /></main>
|
||||
<div class="mt-12 flex justify-center">
|
||||
<AdBanner size="tile" />
|
||||
</div>
|
||||
|
||||
<SeoBlock
|
||||
howTo={[
|
||||
'Type or paste your items into the text area — one item per line.',
|
||||
'Set how many items to pick in the Pick field.',
|
||||
'Toggle Allow duplicates if the same item can be chosen more than once.',
|
||||
'Click Pick (or press Ctrl+Enter) — the selected items appear as chips.',
|
||||
]}
|
||||
whenTo={[
|
||||
'Choosing a random winner for a giveaway or contest.',
|
||||
'Picking who presents first in a meeting or class.',
|
||||
'Organising a Secret Santa or gift exchange draw.',
|
||||
'Selecting a random movie, restaurant, or activity from a list.',
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<ListGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,77 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import LotteryGenerator from '../../components/generators/LotteryGenerator.astro';
|
||||
import AdBanner from '../../components/AdBanner.astro';
|
||||
import { generators } from '../../data/generators';
|
||||
import SeoBlock from '../../components/SeoBlock.astro';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import LotteryGenerator from "@/components/generators/LotteryGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'lottery')!;
|
||||
const generator = generators.find((g) => g.slug === "lottery")!;
|
||||
---
|
||||
|
||||
<BaseLayout lang="en"
|
||||
title={generator.seoTitle}
|
||||
description={generator.seoDescription}
|
||||
>
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a
|
||||
href="/"
|
||||
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="Back to all generators"
|
||||
>
|
||||
<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"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m15 18-6-6 6-6"/>
|
||||
</svg>
|
||||
All generators
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span
|
||||
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
|
||||
{generator.title} Generator
|
||||
</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<LotteryGenerator />
|
||||
</main>
|
||||
|
||||
<div class="mt-12 flex justify-center">
|
||||
<AdBanner size="tile" />
|
||||
</div>
|
||||
|
||||
<SeoBlock
|
||||
howTo={[
|
||||
'Set the From and To fields to define your number range (e.g. 1 to 49).',
|
||||
'Enter how many numbers to pick in the Pick field.',
|
||||
'Click Draw — a sorted set of unique numbers appears.',
|
||||
'Click Copy to copy all numbers to the clipboard.',
|
||||
]}
|
||||
whenTo={[
|
||||
'Generating lottery or lotto ticket numbers.',
|
||||
'Running a fair raffle draw with unique entries.',
|
||||
'Selecting a random sample from a numbered list.',
|
||||
'Choosing lottery numbers for Powerball, EuroMillions, or local draws.',
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<LotteryGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,77 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import NumberGenerator from '../../components/generators/NumberGenerator.astro';
|
||||
import AdBanner from '../../components/AdBanner.astro';
|
||||
import { generators } from '../../data/generators';
|
||||
import SeoBlock from '../../components/SeoBlock.astro';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import NumberGenerator from "@/components/generators/NumberGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'numbers')!;
|
||||
const generator = generators.find((g) => g.slug === "numbers")!;
|
||||
---
|
||||
|
||||
<BaseLayout lang="en"
|
||||
title={generator.seoTitle}
|
||||
description={generator.seoDescription}
|
||||
>
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a
|
||||
href="/"
|
||||
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="Back to all generators"
|
||||
>
|
||||
<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"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m15 18-6-6 6-6"/>
|
||||
</svg>
|
||||
All generators
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span
|
||||
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
|
||||
{generator.title} Generator
|
||||
</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<NumberGenerator />
|
||||
</main>
|
||||
|
||||
<div class="mt-12 flex justify-center">
|
||||
<AdBanner size="tile" />
|
||||
</div>
|
||||
|
||||
<SeoBlock
|
||||
howTo={[
|
||||
'Enter the minimum value in the From field.',
|
||||
'Enter the maximum value in the To field.',
|
||||
'Click Generate — a random number within your range appears instantly.',
|
||||
'Click the number to copy it to the clipboard.',
|
||||
]}
|
||||
whenTo={[
|
||||
'Picking a winner in a giveaway or raffle.',
|
||||
'Choosing a random starting player in a board game.',
|
||||
'Making a quick decision between numbered options.',
|
||||
'Generating test data with values in a specific range.',
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<NumberGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,77 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import PasswordGenerator from '../../components/generators/PasswordGenerator.astro';
|
||||
import AdBanner from '../../components/AdBanner.astro';
|
||||
import { generators } from '../../data/generators';
|
||||
import SeoBlock from '../../components/SeoBlock.astro';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import PasswordGenerator from "@/components/generators/PasswordGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'password')!;
|
||||
const generator = generators.find((g) => g.slug === "password")!;
|
||||
---
|
||||
|
||||
<BaseLayout lang="en"
|
||||
title={generator.seoTitle}
|
||||
description={generator.seoDescription}
|
||||
>
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a
|
||||
href="/"
|
||||
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="Back to all generators"
|
||||
>
|
||||
<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"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m15 18-6-6 6-6"/>
|
||||
</svg>
|
||||
All generators
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span
|
||||
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
|
||||
{generator.title} Generator
|
||||
</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<PasswordGenerator />
|
||||
</main>
|
||||
|
||||
<div class="mt-12 flex justify-center">
|
||||
<AdBanner size="tile" />
|
||||
</div>
|
||||
|
||||
<SeoBlock
|
||||
howTo={[
|
||||
'Drag the length slider to set how many characters you need (4–64).',
|
||||
'Toggle uppercase, lowercase, digits, and symbols on or off.',
|
||||
'Click Generate — a new password appears instantly.',
|
||||
'Click the password to copy it to your clipboard.',
|
||||
]}
|
||||
whenTo={[
|
||||
'Creating a strong password for a new account.',
|
||||
'Generating a temporary access credential.',
|
||||
'Producing a random secret key for development or testing.',
|
||||
'Replacing a weak or reused password.',
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<PasswordGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,56 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import UuidGenerator from '../../components/generators/UuidGenerator.astro';
|
||||
import AdBanner from '../../components/AdBanner.astro';
|
||||
import { generators } from '../../data/generators';
|
||||
import SeoBlock from '../../components/SeoBlock.astro';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import UuidGenerator from "@/components/generators/UuidGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'uuid')!;
|
||||
const generator = generators.find((g) => g.slug === "uuid")!;
|
||||
---
|
||||
|
||||
<BaseLayout lang="en"
|
||||
title={generator.seoTitle}
|
||||
description={generator.seoDescription}
|
||||
>
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a
|
||||
href="/"
|
||||
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="Back to all generators"
|
||||
>
|
||||
<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" aria-hidden="true">
|
||||
<path d="m15 18-6-6 6-6"/>
|
||||
</svg>
|
||||
All generators
|
||||
</a>
|
||||
</nav>
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">{generator.title}</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
|
||||
</header>
|
||||
<main><UuidGenerator /></main>
|
||||
<div class="mt-12 flex justify-center">
|
||||
<AdBanner size="tile" />
|
||||
</div>
|
||||
|
||||
<SeoBlock
|
||||
howTo={[
|
||||
'Choose a token type: UUID v4, Hex, or Base64.',
|
||||
'For Hex and Base64, set the byte length (4–64 bytes).',
|
||||
'Enter how many tokens to generate (1–20).',
|
||||
'Click Generate, then Copy all to copy every token to the clipboard.',
|
||||
]}
|
||||
whenTo={[
|
||||
'Generating unique IDs for database records or API resources.',
|
||||
'Creating random session tokens or API keys during development.',
|
||||
'Producing test data with realistic-looking identifiers.',
|
||||
'Generating a one-time secret for environment variables or config files.',
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<UuidGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,77 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import WheelSpinner from '../../components/generators/WheelSpinner.astro';
|
||||
import AdBanner from '../../components/AdBanner.astro';
|
||||
import SeoBlock from '../../components/SeoBlock.astro';
|
||||
import { generators } from '../../data/generators';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import WheelSpinner from "@/components/generators/WheelSpinner.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'wheel')!;
|
||||
const generator = generators.find((g) => g.slug === "wheel")!;
|
||||
---
|
||||
|
||||
<BaseLayout lang="en"
|
||||
title={generator.seoTitle}
|
||||
description={generator.seoDescription}
|
||||
>
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a
|
||||
href="/"
|
||||
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="Back to all generators"
|
||||
>
|
||||
<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"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="m15 18-6-6 6-6"/>
|
||||
</svg>
|
||||
All generators
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span
|
||||
class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">
|
||||
{generator.title}
|
||||
</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.description}</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<WheelSpinner />
|
||||
</main>
|
||||
|
||||
<div class="mt-12 flex justify-center">
|
||||
<AdBanner size="tile" />
|
||||
</div>
|
||||
|
||||
<SeoBlock
|
||||
howTo={[
|
||||
'Type your items into the text box — one per line.',
|
||||
'Click "Update wheel" to apply changes.',
|
||||
'Hit "Spin" and watch the wheel decide.',
|
||||
'Copy the result or spin again.',
|
||||
]}
|
||||
whenTo={[
|
||||
'Picking a winner for a giveaway or raffle.',
|
||||
'Deciding whose turn it is in a game or meeting.',
|
||||
'Choosing a random activity, movie, or restaurant.',
|
||||
'Any decision you want to leave to chance.',
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<WheelSpinner />
|
||||
</GeneratorLayout>
|
||||
|
||||
+46
-39
@@ -1,46 +1,53 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import GeneratorCard from '../components/GeneratorCard.astro';
|
||||
import AdBanner from '../components/AdBanner.astro';
|
||||
import { generators } from '../data/generators';
|
||||
import { useT } from '../i18n/translations';
|
||||
import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||
import GeneratorCard from "@/components/GeneratorCard.astro";
|
||||
import AdBanner from "@/components/AdBanner.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
import { useT } from "@/i18n/translations";
|
||||
import type { Lang } from "@/i18n/translations";
|
||||
|
||||
const T = useT('en');
|
||||
const lang = (Astro.currentLocale as Lang) || "en";
|
||||
const T = useT(lang);
|
||||
---
|
||||
|
||||
<BaseLayout lang="en">
|
||||
<div class="max-w-4xl mx-auto px-4 py-12 sm:py-20">
|
||||
<header class="mb-12">
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
<span
|
||||
class="inline-block w-3 h-3 rounded-full bg-[#534AB7]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="text-xl font-bold tracking-tight text-zinc-100">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100 leading-tight">
|
||||
{T.homeTitle}
|
||||
</h1>
|
||||
<p class="mt-3 text-base text-zinc-400 max-w-md">
|
||||
{T.homeSubtitle}
|
||||
</p>
|
||||
</header>
|
||||
<BaseLayout>
|
||||
<div class="max-w-4xl mx-auto px-4 py-12 sm:py-20">
|
||||
<header class="mb-12">
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
<span
|
||||
class="inline-block w-3 h-3 rounded-full bg-[#534AB7]"
|
||||
aria-hidden="true"></span>
|
||||
<span class="text-xl font-bold tracking-tight text-zinc-100"
|
||||
>{T.brandLabel}</span
|
||||
>
|
||||
</div>
|
||||
<h1
|
||||
class="text-3xl sm:text-4xl font-bold text-zinc-100 leading-tight"
|
||||
>
|
||||
{T.homeTitle}
|
||||
</h1>
|
||||
<p class="mt-3 text-base text-zinc-400 max-w-md">
|
||||
{T.homeSubtitle}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<AdBanner size="leaderboard" />
|
||||
<AdBanner size="leaderboard" />
|
||||
|
||||
<main class="mt-8">
|
||||
<div
|
||||
class="grid gap-3"
|
||||
style="grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));"
|
||||
role="list"
|
||||
aria-label="Generator catalog"
|
||||
>
|
||||
{generators.map((generator) => (
|
||||
<div role="listitem">
|
||||
<GeneratorCard generator={generator} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<main class="mt-8">
|
||||
<div
|
||||
class="grid gap-3"
|
||||
style="grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));"
|
||||
role="list"
|
||||
aria-label="Generator catalog"
|
||||
>
|
||||
{
|
||||
generators.map((generator) => (
|
||||
<div role="listitem">
|
||||
<GeneratorCard generator={generator} />
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -1,40 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
import CardGenerator from '../../../components/generators/CardGenerator.astro';
|
||||
import AdBanner from '../../../components/AdBanner.astro';
|
||||
import SeoBlock from '../../../components/SeoBlock.astro';
|
||||
import { generators } from '../../../data/generators';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import CardGenerator from "@/components/generators/CardGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'cards')!;
|
||||
const generator = generators.find((g) => g.slug === "cards")!;
|
||||
---
|
||||
|
||||
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a href="/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" aria-label="Все генераторы">
|
||||
<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" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
|
||||
Все генераторы
|
||||
</a>
|
||||
</nav>
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Генератор карт</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
|
||||
</header>
|
||||
<main><CardGenerator /></main>
|
||||
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
|
||||
<SeoBlock lang="ru" howTo={[
|
||||
'Укажите количество карт для розыгрыша.',
|
||||
'Включите «Разрешить повторения», если нужны дубли.',
|
||||
'Нажмите «Тянуть» — карты появятся на экране.',
|
||||
'Нажмите «Копировать», чтобы скопировать результат.',
|
||||
]} whenTo={[
|
||||
'Случайная раздача карт для карточных игр.',
|
||||
'Выбор случайных карт для гаданий.',
|
||||
'Генерация случайных комбинаций карт для тестирования.',
|
||||
]} />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<CardGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,40 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
import CoinGenerator from '../../../components/generators/CoinGenerator.astro';
|
||||
import AdBanner from '../../../components/AdBanner.astro';
|
||||
import SeoBlock from '../../../components/SeoBlock.astro';
|
||||
import { generators } from '../../../data/generators';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import CoinGenerator from "@/components/generators/CoinGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'coin')!;
|
||||
const generator = generators.find((g) => g.slug === "coin")!;
|
||||
---
|
||||
|
||||
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a href="/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" aria-label="Все генераторы">
|
||||
<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" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
|
||||
Все генераторы
|
||||
</a>
|
||||
</nav>
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Подбросить монетку</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
|
||||
</header>
|
||||
<main><CoinGenerator /></main>
|
||||
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
|
||||
<SeoBlock lang="ru" howTo={[
|
||||
'Выберите количество монет с помощью ползунка.',
|
||||
'Нажмите «Подбросить» — орёл или решка.',
|
||||
'При нескольких монетах отображается итоговый счёт.',
|
||||
'Нажмите «Копировать», чтобы сохранить результат.',
|
||||
]} whenTo={[
|
||||
'Принятие решений по принципу «орёл или решка».',
|
||||
'Определение очерёдности в игре.',
|
||||
'Симуляция случайных событий с равной вероятностью.',
|
||||
]} />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<CoinGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,39 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
import ColorGenerator from '../../../components/generators/ColorGenerator.astro';
|
||||
import AdBanner from '../../../components/AdBanner.astro';
|
||||
import SeoBlock from '../../../components/SeoBlock.astro';
|
||||
import { generators } from '../../../data/generators';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import ColorGenerator from "@/components/generators/ColorGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'colors')!;
|
||||
const generator = generators.find((g) => g.slug === "colors")!;
|
||||
---
|
||||
|
||||
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a href="/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" aria-label="Все генераторы">
|
||||
<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" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
|
||||
Все генераторы
|
||||
</a>
|
||||
</nav>
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Генератор цветов</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
|
||||
</header>
|
||||
<main><ColorGenerator /></main>
|
||||
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
|
||||
<SeoBlock lang="ru" howTo={[
|
||||
'Нажмите «Сгенерировать», чтобы получить случайный цвет.',
|
||||
'Цвет отображается в форматах HEX, RGB и HSL одновременно.',
|
||||
'Нажмите на иконку копирования рядом с нужным форматом.',
|
||||
]} whenTo={[
|
||||
'Поиск вдохновения для цветовой палитры.',
|
||||
'Случайный выбор цвета для дизайна или иллюстрации.',
|
||||
'Генерация тестовых данных с цветовыми значениями.',
|
||||
]} />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<ColorGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,43 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
import DiceGenerator from '../../../components/generators/DiceGenerator.astro';
|
||||
import AdBanner from '../../../components/AdBanner.astro';
|
||||
import SeoBlock from '../../../components/SeoBlock.astro';
|
||||
import { generators } from '../../../data/generators';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import DiceGenerator from "@/components/generators/DiceGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'dice')!;
|
||||
const generator = generators.find((g) => g.slug === "dice")!;
|
||||
---
|
||||
|
||||
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a href="/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" aria-label="Все генераторы">
|
||||
<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" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
|
||||
Все генераторы
|
||||
</a>
|
||||
</nav>
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Бросить кубик онлайн</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
|
||||
</header>
|
||||
<main><DiceGenerator /></main>
|
||||
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
|
||||
<SeoBlock lang="ru" howTo={[
|
||||
'Введите нотацию, например 2d6+3 или 1d20 — или настройте кубики вручную.',
|
||||
'Выберите режим: Обычный, Преимущество или Помеха (только для одного кубика).',
|
||||
'Нажмите «Бросить» — результаты отобразятся мгновенно.',
|
||||
'Максимумы подсвечены фиолетовым, единицы — красным.',
|
||||
]} whenTo={[
|
||||
'Броски в настольных ролевых играх (D&D и другие).',
|
||||
'Быстрые броски через нотацию: 2d6+3, 1d20 и любые другие.',
|
||||
'Преимущество и помеха для механик D&D 5e.',
|
||||
'Бросок характеристик: 4d6, отбросить наименьший (4d6dl1).',
|
||||
'Взрывающиеся кубики для Savage Worlds или хаус-рулов (например, 3d6!).',
|
||||
'Использование keep/drop модификаторов типа 4d6kh3 (оставить три лучших).',
|
||||
]} />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<DiceGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,40 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
import ListGenerator from '../../../components/generators/ListGenerator.astro';
|
||||
import AdBanner from '../../../components/AdBanner.astro';
|
||||
import SeoBlock from '../../../components/SeoBlock.astro';
|
||||
import { generators } from '../../../data/generators';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import ListGenerator from "@/components/generators/ListGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'list')!;
|
||||
const generator = generators.find((g) => g.slug === "list")!;
|
||||
---
|
||||
|
||||
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a href="/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" aria-label="Все генераторы">
|
||||
<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" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
|
||||
Все генераторы
|
||||
</a>
|
||||
</nav>
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Случайный выбор из списка</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
|
||||
</header>
|
||||
<main><ListGenerator /></main>
|
||||
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
|
||||
<SeoBlock lang="ru" howTo={[
|
||||
'Введите список элементов — по одному на строку.',
|
||||
'Укажите, сколько элементов выбрать.',
|
||||
'При необходимости разрешите повторения.',
|
||||
'Нажмите «Выбрать» — результаты появятся на экране.',
|
||||
]} whenTo={[
|
||||
'Розыгрыш победителей конкурса.',
|
||||
'Случайное распределение задач между участниками команды.',
|
||||
'Выбор случайного варианта из списка.',
|
||||
]} />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<ListGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,40 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
import LotteryGenerator from '../../../components/generators/LotteryGenerator.astro';
|
||||
import AdBanner from '../../../components/AdBanner.astro';
|
||||
import SeoBlock from '../../../components/SeoBlock.astro';
|
||||
import { generators } from '../../../data/generators';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import LotteryGenerator from "@/components/generators/LotteryGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'lottery')!;
|
||||
const generator = generators.find((g) => g.slug === "lottery")!;
|
||||
---
|
||||
|
||||
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a href="/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" aria-label="Все генераторы">
|
||||
<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" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
|
||||
Все генераторы
|
||||
</a>
|
||||
</nav>
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Генератор лотереи</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
|
||||
</header>
|
||||
<main><LotteryGenerator /></main>
|
||||
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
|
||||
<SeoBlock lang="ru" howTo={[
|
||||
'Укажите диапазон чисел в полях «От» и «До».',
|
||||
'Задайте количество чисел для розыгрыша.',
|
||||
'Нажмите «Тянуть» — выпавшие числа появятся на экране.',
|
||||
'Нажмите «Копировать», чтобы сохранить результат.',
|
||||
]} whenTo={[
|
||||
'Розыгрыш лотерейных номеров.',
|
||||
'Жеребьёвка участников конкурса.',
|
||||
'Случайный выбор нескольких уникальных чисел.',
|
||||
]} />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<LotteryGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,41 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
import NumberGenerator from '../../../components/generators/NumberGenerator.astro';
|
||||
import AdBanner from '../../../components/AdBanner.astro';
|
||||
import SeoBlock from '../../../components/SeoBlock.astro';
|
||||
import { generators } from '../../../data/generators';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import NumberGenerator from "@/components/generators/NumberGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'numbers')!;
|
||||
const generator = generators.find((g) => g.slug === "numbers")!;
|
||||
---
|
||||
|
||||
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a href="/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" aria-label="Все генераторы">
|
||||
<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" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
|
||||
Все генераторы
|
||||
</a>
|
||||
</nav>
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Генератор чисел</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
|
||||
</header>
|
||||
<main><NumberGenerator /></main>
|
||||
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
|
||||
<SeoBlock lang="ru" howTo={[
|
||||
'Введите минимальное значение в поле «От».',
|
||||
'Введите максимальное значение в поле «До».',
|
||||
'Нажмите «Сгенерировать» — случайное число появится мгновенно.',
|
||||
'Нажмите на число, чтобы скопировать его в буфер обмена.',
|
||||
]} whenTo={[
|
||||
'Выбор победителя в розыгрыше или конкурсе.',
|
||||
'Определение случайного первого игрока в настольной игре.',
|
||||
'Быстрое принятие решения между пронумерованными вариантами.',
|
||||
'Генерация тестовых данных с числами в заданном диапазоне.',
|
||||
]} />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<NumberGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,40 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
import PasswordGenerator from '../../../components/generators/PasswordGenerator.astro';
|
||||
import AdBanner from '../../../components/AdBanner.astro';
|
||||
import SeoBlock from '../../../components/SeoBlock.astro';
|
||||
import { generators } from '../../../data/generators';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import PasswordGenerator from "@/components/generators/PasswordGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'password')!;
|
||||
const generator = generators.find((g) => g.slug === "password")!;
|
||||
---
|
||||
|
||||
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a href="/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" aria-label="Все генераторы">
|
||||
<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" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
|
||||
Все генераторы
|
||||
</a>
|
||||
</nav>
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Генератор паролей</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
|
||||
</header>
|
||||
<main><PasswordGenerator /></main>
|
||||
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
|
||||
<SeoBlock lang="ru" howTo={[
|
||||
'Задайте длину пароля с помощью ползунка.',
|
||||
'Выберите типы символов: заглавные, строчные, цифры, спецсимволы.',
|
||||
'Нажмите «Сгенерировать» — готовый пароль появится на экране.',
|
||||
'Нажмите на пароль, чтобы скопировать его.',
|
||||
]} whenTo={[
|
||||
'Создание надёжного пароля для нового аккаунта.',
|
||||
'Генерация секретного ключа или токена.',
|
||||
'Регулярное обновление паролей для повышения безопасности.',
|
||||
]} />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<PasswordGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,40 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
import UuidGenerator from '../../../components/generators/UuidGenerator.astro';
|
||||
import AdBanner from '../../../components/AdBanner.astro';
|
||||
import SeoBlock from '../../../components/SeoBlock.astro';
|
||||
import { generators } from '../../../data/generators';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import UuidGenerator from "@/components/generators/UuidGenerator.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'uuid')!;
|
||||
const generator = generators.find((g) => g.slug === "uuid")!;
|
||||
---
|
||||
|
||||
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a href="/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" aria-label="Все генераторы">
|
||||
<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" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
|
||||
Все генераторы
|
||||
</a>
|
||||
</nav>
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Генератор UUID и токенов</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
|
||||
</header>
|
||||
<main><UuidGenerator /></main>
|
||||
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
|
||||
<SeoBlock lang="ru" howTo={[
|
||||
'Выберите тип: UUID v4, Hex или Base64.',
|
||||
'Для Hex и Base64 задайте длину в байтах.',
|
||||
'Укажите количество токенов.',
|
||||
'Нажмите «Сгенерировать» и скопируйте результат.',
|
||||
]} whenTo={[
|
||||
'Генерация уникальных идентификаторов для баз данных.',
|
||||
'Создание токенов для API и сессий.',
|
||||
'Генерация случайных ключей шифрования.',
|
||||
]} />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<UuidGenerator />
|
||||
</GeneratorLayout>
|
||||
|
||||
@@ -1,40 +1,11 @@
|
||||
---
|
||||
import BaseLayout from '../../../layouts/BaseLayout.astro';
|
||||
import WheelSpinner from '../../../components/generators/WheelSpinner.astro';
|
||||
import AdBanner from '../../../components/AdBanner.astro';
|
||||
import SeoBlock from '../../../components/SeoBlock.astro';
|
||||
import { generators } from '../../../data/generators';
|
||||
import GeneratorLayout from "@/layouts/GeneratorLayout.astro";
|
||||
import WheelSpinner from "@/components/generators/WheelSpinner.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'wheel')!;
|
||||
const generator = generators.find((g) => g.slug === "wheel")!;
|
||||
---
|
||||
|
||||
<BaseLayout title={generator.ruSeoTitle} description={generator.ruSeoDescription} lang="ru">
|
||||
<div class="max-w-2xl mx-auto px-4 py-12 sm:py-20">
|
||||
<nav aria-label="Breadcrumb" class="mb-10">
|
||||
<a href="/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" aria-label="Все генераторы">
|
||||
<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" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
|
||||
Все генераторы
|
||||
</a>
|
||||
</nav>
|
||||
<header class="mb-2">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-[#534AB7]" aria-hidden="true"></span>
|
||||
<span class="text-sm font-medium text-zinc-400 uppercase tracking-widest">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100">Колесо фортуны онлайн</h1>
|
||||
<p class="mt-2 text-base text-zinc-400">{generator.ruDescription}</p>
|
||||
</header>
|
||||
<main><WheelSpinner /></main>
|
||||
<div class="mt-12 flex justify-center"><AdBanner size="tile" /></div>
|
||||
<SeoBlock lang="ru" howTo={[
|
||||
'Введите варианты в поле — по одному на строку (от 2 до 24).',
|
||||
'Колесо обновляется автоматически.',
|
||||
'Нажмите «Крутить» и дождитесь результата.',
|
||||
'Победитель выделяется — скопируйте его кнопкой «Копировать».',
|
||||
]} whenTo={[
|
||||
'Случайный выбор победителя розыгрыша.',
|
||||
'Принятие решений в команде.',
|
||||
'Распределение ролей или задач между участниками.',
|
||||
]} />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
<GeneratorLayout generator={generator}>
|
||||
<WheelSpinner />
|
||||
</GeneratorLayout>
|
||||
|
||||
+46
-43
@@ -1,50 +1,53 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import GeneratorCard from '../../components/GeneratorCard.astro';
|
||||
import AdBanner from '../../components/AdBanner.astro';
|
||||
import { generators } from '../../data/generators';
|
||||
import { useT } from '../../i18n/translations';
|
||||
import BaseLayout from "@/layouts/BaseLayout.astro";
|
||||
import GeneratorCard from "@/components/GeneratorCard.astro";
|
||||
import AdBanner from "@/components/AdBanner.astro";
|
||||
import { generators } from "@/data/generators";
|
||||
import { useT } from "@/i18n/translations";
|
||||
import type { Lang } from "@/i18n/translations";
|
||||
|
||||
const T = useT('ru');
|
||||
const lang = (Astro.currentLocale as Lang) || "en";
|
||||
const T = useT(lang);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={T.defaultTitle}
|
||||
description={T.defaultDesc}
|
||||
lang="ru"
|
||||
>
|
||||
<div class="max-w-4xl mx-auto px-4 py-12 sm:py-20">
|
||||
<header class="mb-12">
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
<span
|
||||
class="inline-block w-3 h-3 rounded-full bg-[#534AB7]"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="text-xl font-bold tracking-tight text-zinc-100">{T.brandLabel}</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100 leading-tight">
|
||||
{T.homeTitle}
|
||||
</h1>
|
||||
<p class="mt-3 text-base text-zinc-400 max-w-md">
|
||||
{T.homeSubtitle}
|
||||
</p>
|
||||
</header>
|
||||
<BaseLayout>
|
||||
<div class="max-w-4xl mx-auto px-4 py-12 sm:py-20">
|
||||
<header class="mb-12">
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
<span
|
||||
class="inline-block w-3 h-3 rounded-full bg-[#534AB7]"
|
||||
aria-hidden="true"></span>
|
||||
<span class="text-xl font-bold tracking-tight text-zinc-100"
|
||||
>{T.brandLabel}</span
|
||||
>
|
||||
</div>
|
||||
<h1
|
||||
class="text-3xl sm:text-4xl font-bold text-zinc-100 leading-tight"
|
||||
>
|
||||
{T.homeTitle}
|
||||
</h1>
|
||||
<p class="mt-3 text-base text-zinc-400 max-w-md">
|
||||
{T.homeSubtitle}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<AdBanner size="leaderboard" />
|
||||
<AdBanner size="leaderboard" />
|
||||
|
||||
<main class="mt-8">
|
||||
<div
|
||||
class="grid gap-3"
|
||||
style="grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));"
|
||||
role="list"
|
||||
aria-label="Generator catalog"
|
||||
>
|
||||
{generators.map((generator) => (
|
||||
<div role="listitem">
|
||||
<GeneratorCard generator={generator} lang="ru" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<main class="mt-8">
|
||||
<div
|
||||
class="grid gap-3"
|
||||
style="grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));"
|
||||
role="list"
|
||||
aria-label="Generator catalog"
|
||||
>
|
||||
{
|
||||
generators.map((generator) => (
|
||||
<div role="listitem">
|
||||
<GeneratorCard generator={generator} />
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
Reference in New Issue
Block a user