Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,22 @@
<body class="state-green">
<main class="app" aria-live="polite">
<h1>Happy Button</h1>
<button id="happy-button" type="button" class="happy-button button-green">
Click me
</button>
<p id="click-status" class="status">Total clicks: 0</p>
<p id="celebration-status" class="status">Clicks until celebration: 5</p>
<p id="celebration-message" class="celebration" hidden>
Congratulations! You clicked the button 5 times.
<div class="button-row">
<button id="happy-button" type="button" class="happy-button button-green">
Button 1
</button>
<button id="second-button" type="button" class="happy-button button-red">
Button 2
</button>
</div>
<p id="click-status" class="status">Alternation sequence: 0 / 6</p>
<p id="celebration-status" class="status">
Alternate between both buttons to win.
</p>
<p id="easter-hint" class="hint" tabindex="0">Пасхалка ждет тебя!</p>
<p id="celebration-message" class="celebration" hidden>Ты выйграл</p>
<div id="confetti" class="confetti" aria-hidden="true"></div>
<div id="explosion" class="explosion" aria-hidden="true"></div>
</main>
<script src="script.js"></script>
</body>
Expand Down
119 changes: 95 additions & 24 deletions script.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,121 @@
const TOTAL_KEY = "happyButtonTotalClicks";
const CYCLE_KEY = "happyButtonCelebrationClicks";
const CELEBRATION_INTERVAL = 5;
const SEQUENCE_KEY = "happyButtonAlternationSequence";
const LAST_BUTTON_KEY = "happyButtonLastButton";
const SEQUENCE_TARGET = 6;
const EASTER_TARGET = 3;

const button = document.getElementById("happy-button");
const secondButton = document.getElementById("second-button");
const clickStatus = document.getElementById("click-status");
const celebrationStatus = document.getElementById("celebration-status");
const celebrationMessage = document.getElementById("celebration-message");
const easterHint = document.getElementById("easter-hint");
const confetti = document.getElementById("confetti");
const explosion = document.getElementById("explosion");

function readStoredCount(key) {
const value = Number.parseInt(window.localStorage.getItem(key) ?? "0", 10);
return Number.isFinite(value) && value >= 0 ? value : 0;
}

let totalClicks = readStoredCount(TOTAL_KEY);
let celebrationClicks = readStoredCount(CYCLE_KEY);
let sequenceCount = Math.min(readStoredCount(SEQUENCE_KEY), SEQUENCE_TARGET);
let lastButton = window.localStorage.getItem(LAST_BUTTON_KEY);
let easterClicks = 0;

function renderState(showCelebration = false) {
const isGreenButton = totalClicks % 2 === 0;
const hasStarted = sequenceCount > 0;
const isComplete = sequenceCount >= SEQUENCE_TARGET || showCelebration;

button.classList.toggle("button-green", isGreenButton);
button.classList.toggle("button-red", !isGreenButton);
document.body.classList.toggle("state-green", isGreenButton);
document.body.classList.toggle("state-red", !isGreenButton);
document.body.classList.toggle("state-green", !isComplete && lastButton !== "one");
document.body.classList.toggle("state-red", !isComplete && lastButton === "one");
document.body.classList.toggle("state-win", isComplete);

clickStatus.textContent = `Total clicks: ${totalClicks}`;
celebrationStatus.textContent = `Clicks until celebration: ${
CELEBRATION_INTERVAL - celebrationClicks
}`;
if (showCelebration) {
celebrationMessage.textContent = `Congratulations! You clicked the button ${totalClicks} times.`;
clickStatus.textContent = `Alternation sequence: ${sequenceCount} / ${SEQUENCE_TARGET}`;
celebrationStatus.textContent = hasStarted
? `Next click: ${lastButton === "one" ? "Button 2" : "Button 1"}`
: "Alternate between both buttons to win.";
celebrationMessage.hidden = !isComplete;

if (isComplete) {
celebrationMessage.textContent = "Ты выйграл";
celebrationStatus.textContent = "Celebration unlocked!";
launchConfetti();
}
celebrationMessage.hidden = !showCelebration;
}

button.addEventListener("click", () => {
totalClicks += 1;
celebrationClicks += 1;
function storeSequence() {
window.localStorage.setItem(SEQUENCE_KEY, String(sequenceCount));
if (lastButton) {
window.localStorage.setItem(LAST_BUTTON_KEY, lastButton);
}
}

function handleButtonClick(buttonName) {
if (!lastButton) {
sequenceCount = 1;
} else if (lastButton === buttonName) {
sequenceCount = 1;
} else {
sequenceCount += 1;
}

lastButton = buttonName;

const shouldCelebrate = celebrationClicks === CELEBRATION_INTERVAL;
const shouldCelebrate = sequenceCount >= SEQUENCE_TARGET;
if (shouldCelebrate) {
celebrationClicks = 0;
sequenceCount = SEQUENCE_TARGET;
}

window.localStorage.setItem(TOTAL_KEY, String(totalClicks));
window.localStorage.setItem(CYCLE_KEY, String(celebrationClicks));
storeSequence();
renderState(shouldCelebrate);
}

function launchConfetti() {
confetti.replaceChildren();
for (let index = 0; index < 24; index += 1) {
const piece = document.createElement("span");
piece.style.setProperty("--x", `${Math.random() * 240 - 120}px`);
piece.style.setProperty("--delay", `${Math.random() * 120}ms`);
piece.style.setProperty("--color", confettiColor(index));
confetti.append(piece);
}
}

function confettiColor(index) {
const colors = ["#f97316", "#22c55e", "#3b82f6", "#ec4899", "#eab308"];
return colors[index % colors.length];
}

function triggerExplosion() {
explosion.replaceChildren();
explosion.classList.remove("is-active");
for (let index = 0; index < 16; index += 1) {
const spark = document.createElement("span");
const angle = (index / 16) * Math.PI * 2;
spark.style.setProperty("--dx", `${Math.cos(angle) * 120}px`);
spark.style.setProperty("--dy", `${Math.sin(angle) * 120}px`);
explosion.append(spark);
}
window.requestAnimationFrame(() => {
explosion.classList.add("is-active");
});
}

function handleEasterHint() {
easterClicks += 1;
if (easterClicks >= EASTER_TARGET) {
easterClicks = 0;
triggerExplosion();
}
}

button.addEventListener("click", () => handleButtonClick("one"));
secondButton.addEventListener("click", () => handleButtonClick("two"));
easterHint.addEventListener("click", handleEasterHint);
easterHint.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
handleEasterHint();
}
});

renderState(false);
94 changes: 93 additions & 1 deletion style.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ body.state-red {
background: #16a34a;
}

body.state-win {
background: #fde047;
}

.app {
position: relative;
overflow: hidden;
width: min(92vw, 28rem);
padding: 2rem;
text-align: center;
Expand All @@ -43,6 +49,12 @@ h1 {
line-height: 1;
}

.button-row {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.75rem;
}

.happy-button {
width: 100%;
min-height: 4rem;
Expand Down Expand Up @@ -80,6 +92,19 @@ h1 {
font-weight: 600;
}

.hint {
margin: 1rem 0 0;
color: #4b5563;
cursor: pointer;
font-size: 0.95rem;
font-weight: 700;
}

.hint:focus-visible {
outline: 3px solid #111827;
outline-offset: 3px;
}

.celebration {
margin: 1.25rem 0 0;
padding: 0.875rem 1rem;
Expand All @@ -90,6 +115,42 @@ h1 {
animation: celebrate-in 240ms ease-out;
}

.confetti,
.explosion {
pointer-events: none;
position: absolute;
inset: 0;
overflow: hidden;
}

.confetti span {
position: absolute;
top: 1rem;
left: 50%;
width: 0.6rem;
height: 0.9rem;
background: var(--color);
opacity: 0;
transform: translate(-50%, 0) rotate(0deg);
animation: confetti-drop 900ms ease-out var(--delay) forwards;
}

.explosion span {
position: absolute;
top: 58%;
left: 50%;
width: 0.75rem;
height: 0.75rem;
border-radius: 999px;
background: #f97316;
opacity: 0;
transform: translate(-50%, -50%) scale(0.4);
}

.explosion.is-active span {
animation: explode 620ms ease-out forwards;
}

@keyframes celebrate-in {
from {
opacity: 0;
Expand All @@ -101,8 +162,39 @@ h1 {
}
}

@keyframes confetti-drop {
0% {
opacity: 1;
transform: translate(-50%, -1rem) rotate(0deg);
}
100% {
opacity: 0;
transform: translate(calc(-50% + var(--x)), 11rem) rotate(260deg);
}
}

@keyframes explode {
0% {
opacity: 1;
transform: translate(-50%, -50%) scale(0.4);
}
100% {
opacity: 0;
transform: translate(calc(-50% + var(--dx)), calc(-50% + var(--dy)))
scale(1.2);
}
}

@media (prefers-reduced-motion: reduce) {
.celebration {
.celebration,
.confetti span,
.explosion.is-active span {
animation: none;
}
}

@media (max-width: 28rem) {
.button-row {
grid-template-columns: 1fr;
}
}