New generators: - Yes or No — random Yes/No/Maybe with confidence bar - Random Name — male/female/any names (EN + RU databases) - Team Splitter — split list into random teams - Random Letter — A-Z / А-Я alphabet picker - Random Date — date between two dates - Rock Paper Scissors — play against random bot Also added: - Privacy Policy page (/privacy, /ru/privacy) - FAQ blocks on all 16 generators (3 Q&A each, EN+RU) - About page (/about, /ru/about) - Improved Yandex RTB ad placement with min-height placeholder - Navigation: About + Privacy links in LanguageSwitcher Build: 38 pages, all passing
194 lines
6.6 KiB
Plaintext
194 lines
6.6 KiB
Plaintext
---
|
|
import { useT } from "../../i18n/translations";
|
|
const isRu = Astro.url.pathname.startsWith("/ru");
|
|
const T = useT(isRu ? "ru" : "en");
|
|
---
|
|
|
|
<div id="date-generator" class="mt-8" data-ru={isRu ? "1" : "0"}>
|
|
<div class="rounded-xl bg-zinc-900/50 border border-zinc-800/60 p-5">
|
|
<div class="flex flex-col sm:flex-row gap-4">
|
|
<div class="flex-1">
|
|
<label for="dg-from" class="block text-sm font-medium text-zinc-400 mb-1"
|
|
>{T.from}</label
|
|
>
|
|
<input
|
|
id="dg-from"
|
|
type="date"
|
|
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-accent focus:ring-1 focus:ring-accent transition-colors"
|
|
/>
|
|
</div>
|
|
<div class="flex-1">
|
|
<label for="dg-to" class="block text-sm font-medium text-zinc-400 mb-1"
|
|
>{T.to}</label
|
|
>
|
|
<input
|
|
id="dg-to"
|
|
type="date"
|
|
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-accent focus:ring-1 focus:ring-accent transition-colors"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p
|
|
id="dg-error"
|
|
role="alert"
|
|
aria-live="polite"
|
|
class="mt-2 text-sm text-red-600 hidden"
|
|
>
|
|
</p>
|
|
|
|
<div class="my-6 rounded-2xl bg-zinc-900/80 border border-zinc-800/80 p-8 h-52 overflow-hidden">
|
|
<div class="h-full flex flex-col items-center justify-center gap-3">
|
|
<button
|
|
id="dg-copy-btn"
|
|
type="button"
|
|
class="group relative invisible cursor-copy rounded-xl px-3 py-1 -mx-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950"
|
|
aria-live="polite"
|
|
>
|
|
<span
|
|
id="dg-result"
|
|
class="text-3xl sm:text-4xl font-semibold text-zinc-100 select-none group-hover:text-zinc-300 text-center"
|
|
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="dg-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"></rect><path
|
|
d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
|
></path></svg
|
|
>
|
|
</span>
|
|
<span
|
|
id="dg-check-icon"
|
|
class="absolute -right-7 top-1/2 -translate-y-1/2 hidden text-accent"
|
|
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"></path></svg
|
|
>
|
|
</span>
|
|
</button>
|
|
<span
|
|
id="dg-copied-label"
|
|
class="text-xs font-medium text-accent opacity-0 transition-opacity duration-200"
|
|
aria-live="polite"
|
|
aria-atomic="true">{T.copied}</span
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-center">
|
|
<button
|
|
id="dg-btn"
|
|
type="button"
|
|
class="w-full sm:w-auto px-8 py-3 bg-accent text-white font-semibold rounded-xl hover:bg-accent-hover active:bg-accent-active focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-950 transition-colors duration-100 cursor-pointer shadow-lg shadow-accent/25"
|
|
>
|
|
{T.generate}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
import { CopyFeedback } from "@/lib/client/clipboard";
|
|
import { createErrorDisplay } from "@/lib/client/validation";
|
|
import { popElement } from "@/lib/client/animations";
|
|
|
|
const isRu = document.documentElement.lang === "ru";
|
|
const ERR_BOTH_DATES = isRu
|
|
? "Укажите обе даты."
|
|
: "Please enter both dates.";
|
|
const ERR_FROM_BEFORE_TO = isRu
|
|
? "«От» должно быть раньше «До»."
|
|
: '"From" must be before "To".';
|
|
|
|
const fromInput = document.getElementById("dg-from") as HTMLInputElement;
|
|
const toInput = document.getElementById("dg-to") as HTMLInputElement;
|
|
const btn = document.getElementById("dg-btn") as HTMLButtonElement;
|
|
const copyBtn = document.getElementById("dg-copy-btn") as HTMLButtonElement;
|
|
const resultEl = document.getElementById("dg-result") as HTMLSpanElement;
|
|
const errorEl = document.getElementById("dg-error") as HTMLParagraphElement;
|
|
const copyIcon = document.getElementById("dg-copy-icon") as HTMLSpanElement;
|
|
const checkIcon = document.getElementById("dg-check-icon") as HTMLSpanElement;
|
|
const copiedLabel = document.getElementById(
|
|
"dg-copied-label",
|
|
) as HTMLSpanElement;
|
|
|
|
const errors = createErrorDisplay(errorEl);
|
|
const clipboard = new CopyFeedback(copyIcon, checkIcon);
|
|
|
|
function generate() {
|
|
const fromVal = fromInput.value;
|
|
const toVal = toInput.value;
|
|
|
|
if (!fromVal || !toVal) {
|
|
errors.show(ERR_BOTH_DATES);
|
|
return;
|
|
}
|
|
|
|
const fromDate = new Date(fromVal + "T00:00:00");
|
|
const toDate = new Date(toVal + "T00:00:00");
|
|
|
|
if (fromDate.getTime() >= toDate.getTime()) {
|
|
errors.show(ERR_FROM_BEFORE_TO);
|
|
return;
|
|
}
|
|
|
|
errors.clear();
|
|
|
|
const fromMs = fromDate.getTime();
|
|
const toMs = toDate.getTime();
|
|
const randomMs = fromMs + Math.random() * (toMs - fromMs);
|
|
const resultDate = new Date(randomMs);
|
|
|
|
const formatted = resultDate.toLocaleDateString(isRu ? "ru-RU" : "en-US", {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
});
|
|
|
|
resultEl.textContent = formatted;
|
|
copyBtn.classList.remove("invisible");
|
|
popElement(resultEl);
|
|
}
|
|
|
|
async function copyDate() {
|
|
const value = resultEl.textContent?.trim();
|
|
if (!value) return;
|
|
await navigator.clipboard.writeText(value);
|
|
clipboard.showCopied();
|
|
copiedLabel.style.opacity = "1";
|
|
setTimeout(() => {
|
|
copiedLabel.style.opacity = "0";
|
|
}, 1500);
|
|
}
|
|
|
|
btn.addEventListener("click", generate);
|
|
copyBtn.addEventListener("click", copyDate);
|
|
[fromInput, toInput].forEach((input) => {
|
|
input.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter") generate();
|
|
});
|
|
});
|
|
</script>
|