Merge pull request #2 from emil28092005/dev

feat: lottery, dice, card generators
This commit is contained in:
Emil Shanaty
2026-05-09 13:49:02 +03:00
committed by GitHub
7 changed files with 728 additions and 3 deletions
@@ -0,0 +1,187 @@
---
// Interactive card generator — runs on the client via inline script
---
<div id="card-generator" class="mt-8">
<div class="flex flex-col sm:flex-row gap-4 items-end">
<div class="flex-1">
<label for="cg-count" class="block text-sm font-medium text-zinc-400 mb-1">Cards to draw</label>
<input
id="cg-count"
type="number"
value="1"
min="1"
max="52"
class="w-full bg-zinc-900 border border-zinc-700 rounded-lg px-3 py-2 text-zinc-100 text-base focus:outline-none focus:border-[#534AB7] focus:ring-1 focus:ring-[#534AB7] transition-colors"
aria-label="Number of cards to draw"
/>
</div>
<label class="flex items-center gap-2.5 cursor-pointer select-none group pb-2">
<input
id="cg-replace"
type="checkbox"
class="w-4 h-4 rounded accent-[#534AB7] cursor-pointer"
/>
<span class="text-sm text-zinc-400 group-hover:text-zinc-200 transition-colors">Allow duplicates</span>
</label>
</div>
<p
id="cg-error"
role="alert"
aria-live="polite"
class="mt-3 text-sm text-red-500 hidden"
></p>
<div class="my-10 min-h-32 flex flex-col items-center justify-center gap-4">
<div
id="cg-cards"
class="flex flex-wrap justify-center gap-2"
aria-live="polite"
aria-label="Drawn cards"
></div>
<button
id="cg-copy-btn"
type="button"
aria-label="Copy cards to clipboard"
class="invisible inline-flex items-center gap-1.5 text-xs font-medium text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded"
>
<span id="cg-copy-label">Copy</span>
<span id="cg-copy-icon" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
</svg>
</span>
<span id="cg-check-icon" class="hidden text-[#534AB7]" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 6 9 17l-5-5"/>
</svg>
</span>
</button>
</div>
<div class="flex justify-center">
<button
id="cg-btn"
type="button"
class="w-full sm:w-auto px-8 py-3 bg-[#534AB7] text-white font-semibold rounded-lg hover:bg-[#4740a0] active:bg-[#3d3990] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 transition-colors duration-100 cursor-pointer"
>
Draw
</button>
</div>
</div>
<script>
const countInput = document.getElementById('cg-count') as HTMLInputElement;
const replaceCb = document.getElementById('cg-replace') as HTMLInputElement;
const btn = document.getElementById('cg-btn') as HTMLButtonElement;
const copyBtn = document.getElementById('cg-copy-btn') as HTMLButtonElement;
const cardsEl = document.getElementById('cg-cards') as HTMLDivElement;
const errorEl = document.getElementById('cg-error') as HTMLParagraphElement;
const copyLabel = document.getElementById('cg-copy-label') as HTMLSpanElement;
const copyIcon = document.getElementById('cg-copy-icon') as HTMLSpanElement;
const checkIcon = document.getElementById('cg-check-icon') as HTMLSpanElement;
const RANKS = ['A','2','3','4','5','6','7','8','9','10','J','Q','K'];
const SUITS = [
{ symbol: '♠', label: 'Spades', red: false },
{ symbol: '♥', label: 'Hearts', red: true },
{ symbol: '♦', label: 'Diamonds', red: true },
{ symbol: '♣', label: 'Clubs', red: false },
];
const DECK = SUITS.flatMap((suit) =>
RANKS.map((rank) => ({ rank, suit }))
);
let lastCards: { rank: string; suit: typeof SUITS[number] }[] = [];
let copyRevertTimer: ReturnType<typeof setTimeout> | null = null;
function showError(msg: string) {
errorEl.textContent = msg;
errorEl.classList.remove('hidden');
}
function clearError() {
errorEl.textContent = '';
errorEl.classList.add('hidden');
}
function draw() {
const count = parseInt(countInput.value, 10);
const withReplacement = replaceCb.checked;
if (!Number.isInteger(count) || count < 1) { showError('Draw at least 1 card.'); return; }
if (!withReplacement && count > 52) { showError('Cannot draw more than 52 unique cards.'); return; }
if (count > 52) { showError('Maximum 52 cards at once.'); return; }
clearError();
if (withReplacement) {
lastCards = Array.from({ length: count }, () => DECK[Math.floor(Math.random() * DECK.length)]);
} else {
// Fisher-Yates partial shuffle
const deck = [...DECK];
for (let i = 0; i < count; i++) {
const j = i + Math.floor(Math.random() * (deck.length - i));
[deck[i], deck[j]] = [deck[j], deck[i]];
}
lastCards = deck.slice(0, count);
}
renderCards();
copyBtn.classList.remove('invisible');
copyLabel.textContent = 'Copy';
}
function renderCards() {
cardsEl.innerHTML = '';
lastCards.forEach(({ rank, suit }) => {
const card = document.createElement('div');
card.setAttribute('aria-label', `${rank} of ${suit.label}`);
card.className = [
'flex flex-col items-center justify-between w-14 h-20 rounded-lg border px-1.5 py-1.5',
'bg-zinc-900 border-zinc-700 select-none',
suit.red ? 'text-red-400' : 'text-zinc-100',
].join(' ');
const top = document.createElement('div');
top.className = 'w-full text-left text-sm font-bold leading-none tabular-nums';
top.textContent = rank;
const mid = document.createElement('div');
mid.className = 'text-2xl leading-none';
mid.textContent = suit.symbol;
const bot = document.createElement('div');
bot.className = 'w-full text-right text-sm font-bold leading-none tabular-nums rotate-180';
bot.textContent = rank;
card.append(top, mid, bot);
cardsEl.appendChild(card);
});
}
async function copyCards() {
if (!lastCards.length) return;
const text = lastCards.map(({ rank, suit }) => `${rank}${suit.symbol}`).join(', ');
await navigator.clipboard.writeText(text);
copyIcon.classList.add('hidden');
checkIcon.classList.remove('hidden');
copyLabel.textContent = 'Copied';
if (copyRevertTimer) clearTimeout(copyRevertTimer);
copyRevertTimer = setTimeout(() => {
checkIcon.classList.add('hidden');
copyIcon.classList.remove('hidden');
copyLabel.textContent = 'Copy';
}, 1500);
}
btn.addEventListener('click', draw);
copyBtn.addEventListener('click', copyCards);
countInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') draw(); });
</script>
@@ -0,0 +1,170 @@
---
// Interactive dice generator — runs on the client via inline script
---
<div id="dice-generator" class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label for="dg-count" class="block text-sm font-medium text-zinc-400 mb-1">Number of dice</label>
<input
id="dg-count"
type="number"
value="2"
min="1"
max="20"
class="w-full bg-zinc-900 border border-zinc-700 rounded-lg px-3 py-2 text-zinc-100 text-base focus:outline-none focus:border-[#534AB7] focus:ring-1 focus:ring-[#534AB7] transition-colors"
aria-label="Number of dice to roll"
/>
</div>
<div>
<label class="block text-sm font-medium text-zinc-400 mb-1">Sides</label>
<div class="flex flex-wrap gap-2" role="radiogroup" aria-label="Dice sides">
{['4','6','8','10','12','20','100'].map((s) => (
<label class="cursor-pointer">
<input type="radio" name="dg-sides" value={s} class="sr-only peer" checked={s === '6'} />
<span class="inline-flex items-center justify-center w-10 h-10 rounded-lg border border-zinc-700 text-sm font-semibold text-zinc-400 peer-checked:border-[#534AB7] peer-checked:text-[#534AB7] peer-checked:bg-[#534AB7]/10 hover:border-zinc-500 transition-colors select-none">
d{s}
</span>
</label>
))}
</div>
</div>
</div>
<p
id="dg-error"
role="alert"
aria-live="polite"
class="mt-3 text-sm text-red-500 hidden"
></p>
<div class="my-10 min-h-28 flex flex-col items-center justify-center gap-4">
<div
id="dg-dice"
class="flex flex-wrap justify-center gap-2"
aria-live="polite"
aria-label="Dice results"
></div>
<div id="dg-total-wrap" class="hidden flex items-center gap-2">
<span class="text-sm text-zinc-500">Total</span>
<span id="dg-total" class="text-2xl font-bold tabular-nums text-zinc-100"></span>
</div>
<button
id="dg-copy-btn"
type="button"
aria-label="Copy results to clipboard"
class="invisible inline-flex items-center gap-1.5 text-xs font-medium text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded"
>
<span id="dg-copy-label">Copy</span>
<span id="dg-copy-icon" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
</svg>
</span>
<span id="dg-check-icon" class="hidden text-[#534AB7]" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 6 9 17l-5-5"/>
</svg>
</span>
</button>
</div>
<div class="flex justify-center">
<button
id="dg-btn"
type="button"
class="w-full sm:w-auto px-8 py-3 bg-[#534AB7] text-white font-semibold rounded-lg hover:bg-[#4740a0] active:bg-[#3d3990] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 transition-colors duration-100 cursor-pointer"
>
Roll
</button>
</div>
</div>
<script>
const countInput = document.getElementById('dg-count') as HTMLInputElement;
const btn = document.getElementById('dg-btn') as HTMLButtonElement;
const copyBtn = document.getElementById('dg-copy-btn') as HTMLButtonElement;
const diceEl = document.getElementById('dg-dice') as HTMLDivElement;
const totalWrap = document.getElementById('dg-total-wrap') as HTMLDivElement;
const totalEl = document.getElementById('dg-total') as HTMLSpanElement;
const errorEl = document.getElementById('dg-error') as HTMLParagraphElement;
const copyLabel = document.getElementById('dg-copy-label') as HTMLSpanElement;
const copyIcon = document.getElementById('dg-copy-icon') as HTMLSpanElement;
const checkIcon = document.getElementById('dg-check-icon') as HTMLSpanElement;
let copyRevertTimer: ReturnType<typeof setTimeout> | null = null;
let lastRolls: number[] = [];
function getSelectedSides(): number {
const radio = document.querySelector('input[name="dg-sides"]:checked') as HTMLInputElement;
return parseInt(radio?.value ?? '6', 10);
}
function showError(msg: string) {
errorEl.textContent = msg;
errorEl.classList.remove('hidden');
}
function clearError() {
errorEl.textContent = '';
errorEl.classList.add('hidden');
}
function roll() {
const count = parseInt(countInput.value, 10);
const sides = getSelectedSides();
if (!Number.isInteger(count) || count < 1) { showError('Number of dice must be at least 1.'); return; }
if (count > 20) { showError('Maximum 20 dice at once.'); return; }
clearError();
lastRolls = Array.from({ length: count }, () => Math.floor(Math.random() * sides) + 1);
const total = lastRolls.reduce((s, n) => s + n, 0);
diceEl.innerHTML = '';
lastRolls.forEach((val) => {
const die = document.createElement('span');
die.textContent = String(val);
const isMax = val === sides;
const isMin = val === 1;
die.className = [
'inline-flex items-center justify-center min-w-[2.75rem] h-11 px-2 rounded-lg border text-sm font-bold tabular-nums transition-colors',
isMax ? 'border-[#534AB7] text-[#534AB7] bg-[#534AB7]/10' :
isMin ? 'border-red-700/50 text-red-400 bg-red-900/10' :
'border-zinc-700 text-zinc-100 bg-zinc-900',
].join(' ');
diceEl.appendChild(die);
});
totalEl.textContent = String(total);
totalWrap.classList.remove('hidden');
copyBtn.classList.remove('invisible');
copyLabel.textContent = 'Copy';
}
async function copyResults() {
if (!lastRolls.length) return;
const sides = getSelectedSides();
const text = `${lastRolls.join(', ')} (${lastRolls.length}d${sides} = ${lastRolls.reduce((s, n) => s + n, 0)})`;
await navigator.clipboard.writeText(text);
copyIcon.classList.add('hidden');
checkIcon.classList.remove('hidden');
copyLabel.textContent = 'Copied';
if (copyRevertTimer) clearTimeout(copyRevertTimer);
copyRevertTimer = setTimeout(() => {
checkIcon.classList.add('hidden');
copyIcon.classList.remove('hidden');
copyLabel.textContent = 'Copy';
}, 1500);
}
btn.addEventListener('click', roll);
copyBtn.addEventListener('click', copyResults);
countInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') roll(); });
</script>
@@ -0,0 +1,179 @@
---
// Interactive lottery generator — runs on the client via inline script
---
<div id="lottery-generator" class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label for="lg-from" class="block text-sm font-medium text-zinc-400 mb-1">From</label>
<input
id="lg-from"
type="number"
value="1"
class="w-full bg-zinc-900 border border-zinc-700 rounded-lg px-3 py-2 text-zinc-100 text-base focus:outline-none focus:border-[#534AB7] focus:ring-1 focus:ring-[#534AB7] transition-colors"
aria-label="Range start"
/>
</div>
<div>
<label for="lg-to" class="block text-sm font-medium text-zinc-400 mb-1">To</label>
<input
id="lg-to"
type="number"
value="49"
class="w-full bg-zinc-900 border border-zinc-700 rounded-lg px-3 py-2 text-zinc-100 text-base focus:outline-none focus:border-[#534AB7] focus:ring-1 focus:ring-[#534AB7] transition-colors"
aria-label="Range end"
/>
</div>
<div>
<label for="lg-pick" class="block text-sm font-medium text-zinc-400 mb-1">Pick</label>
<input
id="lg-pick"
type="number"
value="6"
min="1"
class="w-full bg-zinc-900 border border-zinc-700 rounded-lg px-3 py-2 text-zinc-100 text-base focus:outline-none focus:border-[#534AB7] focus:ring-1 focus:ring-[#534AB7] transition-colors"
aria-label="How many numbers to pick"
/>
</div>
</div>
<p
id="lg-error"
role="alert"
aria-live="polite"
class="mt-3 text-sm text-red-500 hidden"
></p>
<div class="my-10 min-h-24 flex flex-col items-center justify-center gap-4">
<div
id="lg-result"
class="flex flex-wrap justify-center gap-2"
aria-live="polite"
aria-label="Lottery result"
></div>
<button
id="lg-copy-btn"
type="button"
aria-label="Copy numbers to clipboard"
class="invisible inline-flex items-center gap-1.5 text-xs font-medium text-zinc-500 hover:text-zinc-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] rounded"
>
<span id="lg-copy-label">Copy</span>
<span id="lg-copy-icon" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
</svg>
</span>
<span id="lg-check-icon" class="hidden text-[#534AB7]" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 6 9 17l-5-5"/>
</svg>
</span>
</button>
</div>
<div class="flex justify-center">
<button
id="lg-btn"
type="button"
class="w-full sm:w-auto px-8 py-3 bg-[#534AB7] text-white font-semibold rounded-lg hover:bg-[#4740a0] active:bg-[#3d3990] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7] focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 transition-colors duration-100 cursor-pointer"
>
Draw
</button>
</div>
</div>
<script>
const fromInput = document.getElementById('lg-from') as HTMLInputElement;
const toInput = document.getElementById('lg-to') as HTMLInputElement;
const pickInput = document.getElementById('lg-pick') as HTMLInputElement;
const btn = document.getElementById('lg-btn') as HTMLButtonElement;
const copyBtn = document.getElementById('lg-copy-btn') as HTMLButtonElement;
const resultEl = document.getElementById('lg-result') as HTMLDivElement;
const errorEl = document.getElementById('lg-error') as HTMLParagraphElement;
const copyLabel = document.getElementById('lg-copy-label') as HTMLSpanElement;
const copyIcon = document.getElementById('lg-copy-icon') as HTMLSpanElement;
const checkIcon = document.getElementById('lg-check-icon') as HTMLSpanElement;
let copyRevertTimer: ReturnType<typeof setTimeout> | null = null;
function isInteger(val: string) {
return /^-?\d+$/.test(val.trim());
}
function showError(msg: string) {
errorEl.textContent = msg;
errorEl.classList.remove('hidden');
}
function clearError() {
errorEl.textContent = '';
errorEl.classList.add('hidden');
}
function draw() {
if (!isInteger(fromInput.value) || !isInteger(toInput.value) || !isInteger(pickInput.value)) {
showError('All values must be whole numbers.');
return;
}
const min = parseInt(fromInput.value, 10);
const max = parseInt(toInput.value, 10);
const pick = parseInt(pickInput.value, 10);
const pool = max - min + 1;
if (min >= max) { showError('"From" must be less than "To".'); return; }
if (pick < 1) { showError('"Pick" must be at least 1.'); return; }
if (pick > pool) { showError(`Cannot pick ${pick} unique numbers from a range of ${pool}.`); return; }
clearError();
// Fisher-Yates partial shuffle — O(pick)
const nums = Array.from({ length: pool }, (_, i) => min + i);
for (let i = 0; i < pick; i++) {
const j = i + Math.floor(Math.random() * (pool - i));
[nums[i], nums[j]] = [nums[j], nums[i]];
}
const picked = nums.slice(0, pick).sort((a, b) => a - b);
resultEl.innerHTML = '';
picked.forEach((n, idx) => {
const ball = document.createElement('span');
ball.textContent = String(n);
ball.className =
'inline-flex items-center justify-center min-w-[2.5rem] h-10 px-2 rounded-full bg-[#534AB7]/15 border border-[#534AB7]/40 text-zinc-100 font-bold tabular-nums text-sm';
ball.style.animationDelay = `${idx * 40}ms`;
resultEl.appendChild(ball);
});
copyBtn.classList.remove('invisible');
copyLabel.textContent = 'Copy';
}
async function copyNumbers() {
const balls = resultEl.querySelectorAll('span');
const value = Array.from(balls).map((b) => b.textContent).join(', ');
if (!value) return;
await navigator.clipboard.writeText(value);
copyIcon.classList.add('hidden');
checkIcon.classList.remove('hidden');
copyLabel.textContent = 'Copied';
if (copyRevertTimer) clearTimeout(copyRevertTimer);
copyRevertTimer = setTimeout(() => {
checkIcon.classList.add('hidden');
copyIcon.classList.remove('hidden');
copyLabel.textContent = 'Copy';
}, 1500);
}
btn.addEventListener('click', draw);
copyBtn.addEventListener('click', copyNumbers);
[fromInput, toInput, pickInput].forEach((input) => {
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') draw(); });
});
</script>
+3 -3
View File
@@ -33,20 +33,20 @@ export const generators: Generator[] = [
title: 'Lottery',
description: 'Draw a set of unique numbers for lottery-style picks.',
icon: 'ticket',
status: 'coming-soon',
status: 'live',
},
{
slug: 'dice',
title: 'Dice',
description: 'Roll one or more dice with any number of sides.',
icon: 'dice-6',
status: 'coming-soon',
status: 'live',
},
{
slug: 'cards',
title: 'Card',
description: 'Draw a random playing card from a standard deck.',
icon: 'square-stack',
status: 'coming-soon',
status: 'live',
},
];
+63
View File
@@ -0,0 +1,63 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import CardGenerator from '../../components/generators/CardGenerator.astro';
import AdBanner from '../../components/AdBanner.astro';
import { generators } from '../../data/generators';
const generator = generators.find((g) => g.slug === 'cards')!;
---
<BaseLayout
title={`${generator.title} Generator — Randify`}
description={generator.description}
>
<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 grid grid-cols-3 gap-3">
<AdBanner size="tile" />
<AdBanner size="tile" />
<AdBanner size="tile" />
</div>
</div>
</BaseLayout>
+63
View File
@@ -0,0 +1,63 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import DiceGenerator from '../../components/generators/DiceGenerator.astro';
import AdBanner from '../../components/AdBanner.astro';
import { generators } from '../../data/generators';
const generator = generators.find((g) => g.slug === 'dice')!;
---
<BaseLayout
title={`${generator.title} Generator — Randify`}
description={generator.description}
>
<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 grid grid-cols-3 gap-3">
<AdBanner size="tile" />
<AdBanner size="tile" />
<AdBanner size="tile" />
</div>
</div>
</BaseLayout>
+63
View File
@@ -0,0 +1,63 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import LotteryGenerator from '../../components/generators/LotteryGenerator.astro';
import AdBanner from '../../components/AdBanner.astro';
import { generators } from '../../data/generators';
const generator = generators.find((g) => g.slug === 'lottery')!;
---
<BaseLayout
title={`${generator.title} Generator — Randify`}
description={generator.description}
>
<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 grid grid-cols-3 gap-3">
<AdBanner size="tile" />
<AdBanner size="tile" />
<AdBanner size="tile" />
</div>
</div>
</BaseLayout>