Initial commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"_variables": {
|
||||
"lastUpdateCheck": 1778278468140
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="astro/client" />
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.env
|
||||
*.log
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.log
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# Stage 1 — build
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2 — serve
|
||||
FROM nginx:alpine AS runner
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
});
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
absolute_redirect off;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/index.html $uri.html =404;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||
gzip_min_length 1024;
|
||||
}
|
||||
Generated
+6498
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "randify",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^4.16.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"tailwindcss": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="8" fill="#534AB7"/>
|
||||
<text x="16" y="22" font-size="16" font-family="monospace" font-weight="bold" text-anchor="middle" fill="white">#</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 246 B |
@@ -0,0 +1,24 @@
|
||||
---
|
||||
interface Props {
|
||||
size: 'leaderboard' | 'rectangle' | 'tile';
|
||||
}
|
||||
|
||||
const { size } = Astro.props;
|
||||
|
||||
const specs = {
|
||||
leaderboard: { label: '728 × 90', class: 'w-full h-[90px]' },
|
||||
rectangle: { label: '300 × 250', class: 'w-full max-w-[300px] h-[250px]' },
|
||||
tile: { label: '300 × 250', class: 'w-full h-[250px]' },
|
||||
};
|
||||
|
||||
const { label, class: sizeClass } = specs[size];
|
||||
---
|
||||
|
||||
<aside aria-label="Advertisement">
|
||||
<div
|
||||
class={`${sizeClass} flex flex-col items-center justify-center gap-1 rounded-xl border border-dashed border-zinc-800 bg-zinc-900/30`}
|
||||
>
|
||||
<span class="text-[10px] font-medium uppercase tracking-widest text-zinc-600">Advertisement</span>
|
||||
<span class="text-[11px] tabular-nums text-zinc-700">{label}</span>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
import type { Generator } from '../data/generators';
|
||||
|
||||
interface Props {
|
||||
generator: Generator;
|
||||
}
|
||||
|
||||
const { generator } = Astro.props;
|
||||
const { slug, title, description, icon, status } = generator;
|
||||
const isLive = status === 'live';
|
||||
const href = `/generators/${slug}/`;
|
||||
|
||||
const icons: Record<string, string> = {
|
||||
hash: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" x2="20" y1="9" y2="9"/><line x1="4" x2="20" y1="15" y2="15"/><line x1="10" x2="8" y1="3" y2="21"/><line x1="16" x2="14" y1="3" y2="21"/></svg>`,
|
||||
palette: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/></svg>`,
|
||||
lock: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`,
|
||||
ticket: `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"/><path d="M13 5v2"/><path d="M13 17v2"/><path d="M13 11v2"/></svg>`,
|
||||
'dice-6': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="M16 8h.01"/><path d="M16 12h.01"/><path d="M16 16h.01"/><path d="M8 8h.01"/><path d="M8 12h.01"/><path d="M8 16h.01"/></svg>`,
|
||||
'square-stack': `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 10c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2"/><path d="M10 16c-1.1 0-2-.9-2-2v-4c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2"/><rect width="8" height="8" x="14" y="14" rx="2"/></svg>`,
|
||||
};
|
||||
---
|
||||
|
||||
{isLive ? (
|
||||
<a
|
||||
href={href}
|
||||
class="group block border border-zinc-800 rounded-xl p-5 hover:border-[#534AB7] transition-colors duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7]"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="text-[#534AB7]" aria-hidden="true" set:html={icons[icon] ?? icons['hash']} />
|
||||
<span class="text-xs font-medium text-[#534AB7] bg-[#534AB7]/10 px-2 py-0.5 rounded-full">
|
||||
Live
|
||||
</span>
|
||||
</div>
|
||||
<h2 class="mt-3 text-base font-semibold text-zinc-100 group-hover:text-[#534AB7] transition-colors duration-150">
|
||||
{title}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-zinc-400 leading-relaxed">{description}</p>
|
||||
</a>
|
||||
) : (
|
||||
<div
|
||||
class="block border border-zinc-800/50 rounded-xl p-5 opacity-40 cursor-default"
|
||||
aria-label={`${title} — coming soon`}
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="text-zinc-500" aria-hidden="true" set:html={icons[icon] ?? icons['hash']} />
|
||||
<span class="text-xs font-medium text-zinc-500 bg-zinc-800 px-2 py-0.5 rounded-full">
|
||||
Coming soon
|
||||
</span>
|
||||
</div>
|
||||
<h2 class="mt-3 text-base font-semibold text-zinc-400">{title}</h2>
|
||||
<p class="mt-1 text-sm text-zinc-500 leading-relaxed">{description}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -0,0 +1,177 @@
|
||||
---
|
||||
// Interactive number generator — runs on the client via inline script
|
||||
---
|
||||
|
||||
<div id="number-generator" class="mt-8">
|
||||
<div class="flex flex-col sm:flex-row gap-4">
|
||||
<div class="flex-1">
|
||||
<label for="ng-from" class="block text-sm font-medium text-zinc-400 mb-1">From</label>
|
||||
<input
|
||||
id="ng-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="Minimum value"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label for="ng-to" class="block text-sm font-medium text-zinc-400 mb-1">To</label>
|
||||
<input
|
||||
id="ng-to"
|
||||
type="number"
|
||||
value="100"
|
||||
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="Maximum value"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
id="ng-error"
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
class="mt-2 text-sm text-red-600 hidden"
|
||||
></p>
|
||||
|
||||
<div class="my-10 min-h-36 flex flex-col items-center justify-center gap-3">
|
||||
<button
|
||||
id="ng-copy-btn"
|
||||
type="button"
|
||||
aria-label="Copy number to clipboard"
|
||||
class="group relative invisible cursor-copy rounded-xl px-3 py-1 -mx-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#534AB7]"
|
||||
aria-live="polite"
|
||||
>
|
||||
<span
|
||||
id="ng-result"
|
||||
class="text-8xl font-bold tabular-nums tracking-tight text-zinc-100 select-none group-hover:text-zinc-300"
|
||||
style="transition: transform 0.12s cubic-bezier(0.34,1.56,0.64,1), opacity 0.08s ease, color 0.15s ease;"
|
||||
></span>
|
||||
<span
|
||||
id="ng-copy-icon"
|
||||
class="absolute -right-7 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 text-zinc-500"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" 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="ng-check-icon"
|
||||
class="absolute -right-7 top-1/2 -translate-y-1/2 hidden text-[#534AB7]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" 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>
|
||||
|
||||
<span
|
||||
id="ng-copied-label"
|
||||
class="text-xs font-medium text-[#534AB7] opacity-0 transition-opacity duration-200"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>Copied</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<button
|
||||
id="ng-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"
|
||||
>
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const fromInput = document.getElementById('ng-from') as HTMLInputElement;
|
||||
const toInput = document.getElementById('ng-to') as HTMLInputElement;
|
||||
const btn = document.getElementById('ng-btn') as HTMLButtonElement;
|
||||
const copyBtn = document.getElementById('ng-copy-btn') as HTMLButtonElement;
|
||||
const resultEl = document.getElementById('ng-result') as HTMLSpanElement;
|
||||
const errorEl = document.getElementById('ng-error') as HTMLParagraphElement;
|
||||
const copyIcon = document.getElementById('ng-copy-icon') as HTMLSpanElement;
|
||||
const checkIcon = document.getElementById('ng-check-icon') as HTMLSpanElement;
|
||||
const copiedLabel = document.getElementById('ng-copied-label') as HTMLSpanElement;
|
||||
|
||||
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 isInteger(val: string) {
|
||||
return /^-?\d+$/.test(val.trim());
|
||||
}
|
||||
|
||||
function pop() {
|
||||
resultEl.style.transform = 'scale(1.18)';
|
||||
resultEl.style.opacity = '0.7';
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
resultEl.style.transform = 'scale(1)';
|
||||
resultEl.style.opacity = '1';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function generate() {
|
||||
const rawFrom = fromInput.value;
|
||||
const rawTo = toInput.value;
|
||||
|
||||
if (!isInteger(rawFrom) || !isInteger(rawTo)) {
|
||||
showError('Both values must be whole numbers.');
|
||||
return;
|
||||
}
|
||||
|
||||
const min = parseInt(rawFrom, 10);
|
||||
const max = parseInt(rawTo, 10);
|
||||
|
||||
if (min >= max) {
|
||||
showError('"From" must be less than "To".');
|
||||
return;
|
||||
}
|
||||
|
||||
clearError();
|
||||
const result = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
resultEl.textContent = String(result);
|
||||
copyBtn.classList.remove('invisible');
|
||||
pop();
|
||||
}
|
||||
|
||||
async function copyNumber() {
|
||||
const value = resultEl.textContent?.trim();
|
||||
if (!value) return;
|
||||
|
||||
await navigator.clipboard.writeText(value);
|
||||
|
||||
copyIcon.classList.add('hidden');
|
||||
checkIcon.classList.remove('hidden');
|
||||
copiedLabel.style.opacity = '1';
|
||||
|
||||
if (copyRevertTimer) clearTimeout(copyRevertTimer);
|
||||
copyRevertTimer = setTimeout(() => {
|
||||
checkIcon.classList.add('hidden');
|
||||
copyIcon.classList.remove('hidden');
|
||||
copiedLabel.style.opacity = '0';
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
btn.addEventListener('click', generate);
|
||||
copyBtn.addEventListener('click', copyNumber);
|
||||
|
||||
[fromInput, toInput].forEach((input) => {
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') generate();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
export interface Generator {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
status: 'live' | 'coming-soon';
|
||||
}
|
||||
|
||||
export const generators: Generator[] = [
|
||||
{
|
||||
slug: 'numbers',
|
||||
title: 'Number',
|
||||
description: 'Pick a random number within any range you choose.',
|
||||
icon: 'hash',
|
||||
status: 'live',
|
||||
},
|
||||
{
|
||||
slug: 'colors',
|
||||
title: 'Color',
|
||||
description: 'Generate a random color in HEX, RGB, or HSL format.',
|
||||
icon: 'palette',
|
||||
status: 'coming-soon',
|
||||
},
|
||||
{
|
||||
slug: 'password',
|
||||
title: 'Password',
|
||||
description: 'Create a strong, random password with custom rules.',
|
||||
icon: 'lock',
|
||||
status: 'coming-soon',
|
||||
},
|
||||
{
|
||||
slug: 'lottery',
|
||||
title: 'Lottery',
|
||||
description: 'Draw a set of unique numbers for lottery-style picks.',
|
||||
icon: 'ticket',
|
||||
status: 'coming-soon',
|
||||
},
|
||||
{
|
||||
slug: 'dice',
|
||||
title: 'Dice',
|
||||
description: 'Roll one or more dice with any number of sides.',
|
||||
icon: 'dice-6',
|
||||
status: 'coming-soon',
|
||||
},
|
||||
{
|
||||
slug: 'cards',
|
||||
title: 'Card',
|
||||
description: 'Draw a random playing card from a standard deck.',
|
||||
icon: 'square-stack',
|
||||
status: 'coming-soon',
|
||||
},
|
||||
];
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title = 'Randify — Random Value Generators',
|
||||
description = 'Simple tools for raffles, games, and everything that needs randomness.',
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<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" />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body class="bg-zinc-950 text-zinc-100 min-h-screen font-sans antialiased">
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style is:global>
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Space Grotesk', sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
--accent: #534AB7;
|
||||
}
|
||||
|
||||
* {
|
||||
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%);
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import NumberGenerator from '../../components/generators/NumberGenerator.astro';
|
||||
import AdBanner from '../../components/AdBanner.astro';
|
||||
import { generators } from '../../data/generators';
|
||||
|
||||
const generator = generators.find((g) => g.slug === 'numbers')!;
|
||||
---
|
||||
|
||||
<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>
|
||||
<NumberGenerator />
|
||||
</main>
|
||||
|
||||
<div class="mt-12 grid grid-cols-3 gap-3">
|
||||
<AdBanner size="tile" />
|
||||
<AdBanner size="tile" />
|
||||
<AdBanner size="tile" />
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import GeneratorCard from '../components/GeneratorCard.astro';
|
||||
import AdBanner from '../components/AdBanner.astro';
|
||||
import { generators } from '../data/generators';
|
||||
---
|
||||
|
||||
<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">randify</span>
|
||||
</div>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold text-zinc-100 leading-tight">
|
||||
Random generators for<br class="hidden sm:block" /> every occasion
|
||||
</h1>
|
||||
<p class="mt-3 text-base text-zinc-400 max-w-md">
|
||||
Simple tools for raffles, games, and everything that needs randomness.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<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>
|
||||
</BaseLayout>
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user