Celebrate the Spirit of Fridays
FedFredag shares unforgettable Friday moments that brighten your week
import React, { useEffect, useMemo, useRef, useState } from "react";
// --- CONFIG ---
const PRIZES = [
{ label: "Gratis kaffe", type: "win", probability: 0.05 },
{ label: "Rabattkode 10%", type: "win", probability: 0.1 },
{ label: "Ingenting", type: "lose", probability: 0.5 },
{ label: "Nøgle-ring", type: "win", probability: 0.05 },
{ label: "Ingenting", type: "lose", probability: 0.2 },
{ label: "T-shirt", type: "win", probability: 0.03 },
{ label: "Ingenting", type: "lose", probability: 0.07 },
];
// Normalize probabilities and build segments
function buildSegments(config) {
const total = config.reduce((s, p) => s + p.probability, 0);
const normalized = config.map((p) => ({ ...p, probability: p.probability / total }));
return normalized;
}
const SEGMENTS = buildSegments(PRIZES);
function chooseSegment(segments) {
const r = Math.random();
let acc = 0;
for (let i = 0; i < segments.length; i++) {
acc += segments[i].probability;
if (r <= acc) return i;
}
return segments.length - 1;
}
function emailValid(email) {
return /[^@\s]+@[^@\s]+\.[^@\s]+/.test(email);
}
function clamp(n, min, max) {
return Math.max(min, Math.min(max, n));
}
export default function SpinTheWheelApp() {
const [user, setUser] = useState(() => {
try {
const cached = localStorage.getItem("stw_user");
return cached ? JSON.parse(cached) : null;
} catch (e) {
return null;
}
});
const [hasSpun, setHasSpun] = useState(() => {
try {
const cached = localStorage.getItem("stw_hasSpun");
return cached ? JSON.parse(cached) : false;
} catch (e) {
return false;
}
});
const [spinning, setSpinning] = useState(false);
const [result, setResult] = useState(null);
const [angle, setAngle] = useState(0);
const [error, setError] = useState("");
const canvasRef = useRef(null);
const colors = useMemo(() => {
// generate alternating colors for segments
const arr = [];
for (let i = 0; i < SEGMENTS.length; i++) {
arr.push(i % 2 === 0 ? "#f4f4f5" : "#e5e7eb"); // zinc-100 / gray-200
}
return arr;
}, []);
const segmentAngle = (2 * Math.PI) / SEGMENTS.length;
useEffect(() => {
drawWheel(angle);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [angle]);
function drawWheel(rotation = 0) {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
const size = Math.min(canvas.width, canvas.height);
const radius = size / 2 - 10;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(rotation);
// draw segments
for (let i = 0; i < SEGMENTS.length; i++) {
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arc(0, 0, radius, i * segmentAngle, (i + 1) * segmentAngle);
ctx.closePath();
ctx.fillStyle = colors[i];
ctx.fill();
// text
ctx.save();
ctx.rotate(i * segmentAngle + segmentAngle / 2);
ctx.textAlign = "right";
ctx.fillStyle = "#111827"; // gray-900
ctx.font = "bold 14px system-ui, -apple-system, Segoe UI, Roboto, Arial";
const txt = SEGMENTS[i].label;
ctx.fillText(txt, radius - 10, 5);
ctx.restore();
}
// center circle
ctx.beginPath();
ctx.arc(0, 0, 36, 0, Math.PI * 2);
ctx.fillStyle = "white";
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = "#e5e7eb";
ctx.stroke();
ctx.restore();
// pointer at top
ctx.beginPath();
const cx = canvas.width / 2;
const cy = canvas.height / 2;
ctx.moveTo(cx, cy - radius - 2);
ctx.lineTo(cx - 10, cy - radius - 22);
ctx.lineTo(cx + 10, cy - radius - 22);
ctx.closePath();
ctx.fillStyle = "#ef4444"; // red-500
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = "#7f1d1d";
ctx.stroke();
}
function handleSignup(e) {
e.preventDefault();
const form = new FormData(e.currentTarget);
const name = String(form.get("name") || "").trim();
const email = String(form.get("email") || "").trim().toLowerCase();
const consent = Boolean(form.get("consent"));
if (!name) return setError("Skriv dit navn.");
if (!emailValid(email)) return setError("Indtast en gyldig e‑mail.");
if (!consent) return setError("Du skal give samtykke til at deltage.");
setError("");
const newUser = { name, email, ts: Date.now() };
setUser(newUser);
localStorage.setItem("stw_user", JSON.stringify(newUser));
// Optionally send to backend (uncomment and implement API)
// fetch("/api/signup", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newUser) });
}
function spin() {
if (spinning) return;
if (!user) return setError("Udfyld tilmeldingsformularen først.");
if (hasSpun) return setError("Du har allerede spinnet.");
setError("");
setSpinning(true);
// choose prize index according to probabilities
const index = chooseSegment(SEGMENTS);
// compute target angle so that chosen index lands at pointer (top)
// pointer is at 0 rad in our drawing space; we rotate wheel so that the middle of the segment aligns to pointer
const segCenter = index * segmentAngle + segmentAngle / 2;
// add extra spins for flair (5–8 full turns)
const extraTurns = 5 + Math.floor(Math.random() * 3);
const finalAngle = extraTurns * 2 * Math.PI + (2 * Math.PI - segCenter);
const duration = 4500 + Math.random() * 1000; // 4.5–5.5s
// animate
const start = performance.now();
const startAngle = angle % (2 * Math.PI);
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
function frame(now) {
const t = clamp((now - start) / duration, 0, 1);
const eased = easeOutCubic(t);
const current = startAngle + (finalAngle - startAngle) * eased;
setAngle(current);
if (t < 1) {
requestAnimationFrame(frame);
} else {
const prize = SEGMENTS[index];
setResult(prize);
setHasSpun(true);
localStorage.setItem("stw_hasSpun", JSON.stringify(true));
setSpinning(false);
// Optionally notify backend of spin + result
// fetch("/api/spin", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: user.email, prize: prize.label }) });
}
}
requestAnimationFrame(frame);
}
function resetForTesting() {
localStorage.removeItem("stw_hasSpun");
setHasSpun(false);
setResult(null);
setError("");
}
return (
<div className="min-h-screen w-full bg-gradient-to-b from-white to-gray-50 text-gray-900">
<div className="max-w-4xl mx-auto px-4 py-10">
<header className="mb-6">
<h1 className="text-3xl md:text-4xl font-bold tracking-tight">Spin the Wheel 🎡</h1>
<p className="text-gray-600 mt-2">Skriv dig op med navn og e‑mail for at få ét spin og chancen for at vinde præmier.</p>
</header>
{!user ? (
<form onSubmit={handleSignup} className="bg-white rounded-2xl shadow p-6 grid gap-4 md:grid-cols-2">
<div className="md:col-span-1">
<label className="block text-sm font-medium mb-1">Navn</label>
<input name="name" type="text" className="w-full rounded-xl border border-gray-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Mads Holfort" />
</div>
<div className="md:col-span-1">
<label className="block text-sm font-medium mb-1">E‑mail</label>
<input name="email" type="email" className="w-full rounded-xl border border-gray-300 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="mads@example.com" />
</div>
<div className="md:col-span-2 flex items-start gap-3">
<input id="consent" name="consent" type="checkbox" className="mt-1 h-4 w-4" />
<label htmlFor="consent" className="text-sm text-gray-700">Jeg giver samtykke til at modtage information om konkurrencen og accepterer vilkår og privatlivspolitik.</label>
</div>
{error && (
<div className="md:col-span-2 text-sm text-red-600">{error}</div>
)}
<div className="md:col-span-2">
<button type="submit" className="w-full md:w-auto rounded-xl bg-indigo-600 hover:bg-indigo-700 text-white font-semibold px-5 py-2.5 transition">Tilmeld og gå til hjulet</button>
</div>
</form>
) : (
<div className="grid md:grid-cols-2 gap-8 items-center">
<div className="bg-white rounded-2xl shadow p-6">
<div className="flex items-center justify-between mb-4">
<div>
<p className="text-sm text-gray-500">Tilmeldt som</p>
<p className="font-semibold">{user.name} · {user.email}</p>
</div>
<button onClick={resetForTesting} className="text-xs text-gray-500 hover:text-gray-700 underline" title="Nulstil spin (kun til test)">Nulstil</button>
</div>
<div className="relative mx-auto" style={{ width: 360, height: 360 }}>
<canvas ref={canvasRef} width={360} height={360} className="w-full h-full" />
<div className="absolute inset-0 flex items-center justify-center">
<button
onClick={spin}
disabled={spinning || hasSpun}
className={`rounded-full px-6 py-3 text-sm font-semibold shadow ${
spinning || hasSpun ? "bg-gray-300 text-gray-600 cursor-not-allowed" : "bg-indigo-600 text-white hover:bg-indigo-700"
}`}
>
{spinning ? "Spinner…" : hasSpun ? "Allerede spinnet" : "SPIN"}
</button>
</div>
</div>
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
</div>
<div className="bg-white rounded-2xl shadow p-6">
<h2 className="text-lg font-semibold mb-2">Præmier & sandsynligheder</h2>
<ul className="space-y-2">
{SEGMENTS.map((s, i) => (
<li key={i} className="flex items-center justify-between text-sm">
<span>{s.label}</span>
<span className="tabular-nums text-gray-600">{Math.round(s.probability * 1000) / 10}%</span>
</li>
))}
</ul>
<p className="text-xs text-gray-500 mt-4">Sandsynligheder normaliseres automatisk til 100%. Justér i koden i PRIZES.</p>
</div>
</div>
)}
{/* Result modal */}
{result && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-xl max-w-md w-full p-6">
<h3 className="text-xl font-bold mb-2">{result.type === "win" ? "Tillykke!" : "Øv!"}</h3>
<p className="text-gray-700">
{result.type === "win"
? `Du vandt: ${result.label}. Tjek din e‑mail for detaljer (eller vis koden på skærmen).`
: "Det blev desværre ikke til en præmie denne gang."}
</p>
<div className="mt-5 flex justify-end gap-3">
<button
onClick={() => setResult(null)}
className="rounded-xl border border-gray-300 px-4 py-2 text-sm hover:bg-gray-50"
>Luk</button>
</div>
</div>
</div>
)}
<footer className="mt-10 text-xs text-gray-500">
<p>
Tip: Til produktion bør du forbinde formularen til et backend‑endpoint (f.eks. /api/signup) for at gemme deltagere, og registrere spins/resultater server‑side for at forhindre snyd.
</p>
</footer>
</div>
</div>
);
}
Celebrate Fridays with Joy and Connection
Discover how FedFredag brings memorable Friday experiences that brighten your week and foster community spirit.
- Share Your Favorite Friday Moments
- Connect with Others Who Love Fridays
- Find Inspiration for Weekend Plans
- Create Lasting Traditions Every Week
Celebrate Memorable Fridays
Hear from our community as they share joyful moments and memorable Friday experiences with FedFredag.
FedFredag brings a fresh perspective to ending the week, turning Fridays into cherished memories for us all.
Maria Jensen
Creative Storyteller
Joining FedFredag made my Fridays something I truly look forward to every week!
Liam O’Connor
Community Contributor
The platform’s ability to capture joyful Friday moments is unmatched and deeply inspiring.
Sophia Lee
Experience Curator
Every Friday feels special thanks to the stories and events shared through FedFredag.
Ethan Brooks
Event Enthusiast
Celebrate Every Friday with Us
Discover moments that make Fridays unforgettable.
Joyful Stories
Sharing inspiring moments from memorable Fridays.
Community Vibes
Connecting people through shared Friday experiences.
Creative Highlights
Showcasing unique and fun Friday activities.
Exclusive Events
Bringing special Friday happenings to you.
Celebrate Every Friday Moment
Find quick answers to your questions about making your Fridays more joyful and memorable.
What kind of Friday experiences does FedFredag feature?
We showcase inspiring, fun, and unique Friday stories shared by our community.
How can I share my own Friday experience?
Simply submit your story through our platform to inspire others and join the celebration.
Is there a cost to access FedFredag content?
Access to our stories and features is completely free for all visitors.
Can I subscribe to receive Friday highlights?
Yes, sign up for our newsletter to get joyful Friday moments delivered to your inbox.
Celebrating the Joy of Every Friday
Explore our flexible plans designed to enhance your Friday experiences with unique perks.
Essential Access
$29.99
Unlock exclusive Friday content and community perks.
Memorable Moments Curated
Discover unique stories and events that make every Friday unforgettable.
Community Engagement
Connect with others who share the excitement of Friday celebrations.
Exclusive Content Access
Enjoy early access to special Friday features and joyful experiences.
