first upload

This commit is contained in:
Welton Moura
2026-02-27 13:19:26 -03:00
committed by GitHub
commit e48b4809c0
36 changed files with 6182 additions and 0 deletions

79
src/pages/BioPage.jsx Normal file
View File

@@ -0,0 +1,79 @@
import { useNavigate } from 'react-router-dom'
import styles from './BioPage.module.css'
const socialLinks = [
{ href: 'https://www.facebook.com/raulrockbar', icon: 'fa-brands fa-facebook-f', label: 'Facebook' },
{ href: 'https://www.instagram.com/raulrockbar', icon: 'fa-brands fa-instagram', label: 'Instagram' },
{ href: 'https://www.youtube.com/channel/UC90VdEfI9aszgxO9cg1B06A', icon: 'fa-brands fa-youtube', label: 'YouTube' },
{ href: 'mailto:raulrockbar@outlook.com', icon: 'fa-solid fa-envelope', label: 'Email' },
{ href: '#', icon: 'fa-brands fa-whatsapp', label: 'WhatsApp' },
]
const actionButtons = [
{ path: '/cardapio', icon: 'fa-solid fa-utensils', label: 'Cardápio' },
{ path: '/karaoke', icon: 'fa-solid fa-microphone', label: 'Karaokê' },
{ path: '/jogos', icon: 'fa-solid fa-gamepad', label: 'Jogos de Bar' },
]
export default function BioPage() {
const navigate = useNavigate()
return (
<div className={styles.page}>
{/* Hero com imagem de fundo */}
<div className={styles.hero}>
<div className={styles.heroOverlay} />
<h1 className={styles.heroTitle}>Raul Rock Bar<br /><span>& Café</span></h1>
{/* Logo flutuante */}
<div className={styles.logoWrapper}>
<img
src="https://files.menudino.com/cardapios/53448/logo.png"
alt="Raul Rock Bar Logo"
className={styles.logo}
/>
</div>
</div>
{/* Conteúdo abaixo do hero */}
<div className={styles.content}>
{/* Ícones sociais */}
<div className={styles.socialRow}>
{socialLinks.map(link => (
<a
key={link.label}
href={link.href}
target="_blank"
rel="noopener noreferrer"
aria-label={link.label}
className={styles.socialIcon}
>
<i className={link.icon} />
</a>
))}
</div>
{/* Descrição */}
<p className={styles.description}>
O melhor bar rock da cidade.<br />
Cerveja gelada, boa música e muito Rock! 🤘
</p>
{/* Botões de ação */}
<div className={styles.actions}>
{actionButtons.map(btn => (
<button
key={btn.path}
onClick={() => navigate(btn.path)}
className={styles.actionBtn}
>
<i className={btn.icon} />
<span>{btn.label}</span>
<i className="fas fa-chevron-right" style={{ marginLeft: 'auto', opacity: 0.5 }} />
</button>
))}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,158 @@
.page {
min-height: calc(100vh - var(--header-height) - var(--bottom-nav-height));
display: flex;
flex-direction: column;
}
/* ── Hero ─────────────────────────────────────── */
.hero {
position: relative;
background-image: url('https://img.freepik.com/fotos-gratis/coqueteis-e-coqueteis-em-casa-noturna_23-2149093603.jpg?semt=ais_hybrid&w=740');
background-size: cover;
background-position: center;
min-height: 260px;
display: flex;
align-items: flex-end;
justify-content: center;
padding-bottom: 130px;
}
.heroOverlay {
position: absolute;
inset: 0;
background: linear-gradient(to bottom,
rgba(73, 41, 28, 0.3) 0%,
rgba(73, 41, 28, 0.7) 60%,
rgba(81, 78, 78, 1) 100%);
}
.heroTitle {
position: relative;
z-index: 2;
font-family: 'Playfair Display', serif;
font-size: clamp(2rem, 8vw, 3.2rem);
font-weight: 900;
color: var(--secondary);
text-align: center;
text-shadow: 0 4px 16px rgba(0, 0, 0, 0.7);
line-height: 1.15;
}
.heroTitle span {
color: var(--text-light);
font-weight: 700;
}
/* ── Logo flutuante ───────────────────────────── */
.logoWrapper {
position: absolute;
bottom: -60px;
left: 50%;
transform: translateX(-50%);
z-index: 10;
animation: fallIn 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) 0.3s both;
}
.logo {
width: 180px;
height: 180px;
border-radius: 50%;
padding: 0px;
object-fit: cover;
filter: drop-shadow(0 12px 24px rgba(0, 0, 0, 0.6));
border: none;
background: transparent;
}
/* ── Conteúdo ─────────────────────────────────── */
.content {
flex: 1;
padding: 80px 1.25rem 2rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.75rem;
max-width: 480px;
margin: 0 auto;
width: 100%;
}
/* ── Social Icons ─────────────────────────────── */
.socialRow {
display: flex;
gap: 1.25rem;
}
.socialIcon {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--card-bg);
border: 1px solid var(--card-border);
display: flex;
align-items: center;
justify-content: center;
color: var(--secondary);
font-size: 1.15rem;
transition: var(--transition);
backdrop-filter: blur(6px);
}
.socialIcon:hover {
background: var(--accent);
border-color: var(--secondary);
transform: translateY(-3px);
box-shadow: var(--shadow-md);
}
/* ── Description ──────────────────────────────── */
.description {
font-size: 0.95rem;
color: rgba(245, 240, 238, 0.7);
text-align: center;
line-height: 1.6;
}
/* ── Action Buttons ───────────────────────────── */
.actions {
width: 100%;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.actionBtn {
width: 100%;
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem 1.25rem;
background: var(--glass-bg);
border: 1px solid var(--card-border);
border-radius: var(--radius-md);
color: var(--secondary);
font-size: 1rem;
font-weight: 600;
transition: var(--transition);
backdrop-filter: blur(8px);
cursor: pointer;
text-align: left;
}
.actionBtn i:first-child {
font-size: 1.1rem;
width: 22px;
text-align: center;
color: var(--secondary);
}
.actionBtn:hover {
background: var(--accent);
border-color: var(--secondary);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.actionBtn:active {
transform: scale(0.98);
}

129
src/pages/CardapioPage.jsx Normal file
View File

@@ -0,0 +1,129 @@
import { useState, useMemo, useCallback } from 'react'
import Swal from 'sweetalert2'
import { menuData } from '../data/menuData'
import styles from './CardapioPage.module.css'
export default function CardapioPage() {
const [search, setSearch] = useState('')
const [selectedCategory, setSelectedCategory] = useState(null)
const categories = useMemo(
() => [...new Set(menuData.map(item => item.category))],
[]
)
const filtered = useMemo(() => {
return menuData.filter(item => {
const matchSearch =
item.name.toLowerCase().includes(search.toLowerCase()) ||
item.description.toLowerCase().includes(search.toLowerCase())
const matchCat = selectedCategory === null || item.category === selectedCategory
return matchSearch && matchCat
})
}, [search, selectedCategory])
const grouped = useMemo(() => {
if (selectedCategory) {
return { [selectedCategory]: filtered }
}
return filtered.reduce((acc, item) => {
if (!acc[item.category]) acc[item.category] = []
acc[item.category].push(item)
return acc
}, {})
}, [filtered, selectedCategory])
const openDetails = useCallback((item) => {
if ((item.description?.length ?? 0) < 1) return
Swal.fire({
title: item.name,
html: `<div style="text-align:left">${item.description}</div>`,
imageUrl: item.image,
showCloseButton: true,
showConfirmButton: false,
background: '#2a2a2a',
color: '#efc7b8',
imageAlt: item.name,
})
}, [])
return (
<div className={styles.page}>
{/* Filtro de categorias horizontal (pills) */}
<div className={styles.pillRow}>
<button
onClick={() => setSelectedCategory(null)}
className={`${styles.pill} ${selectedCategory === null ? styles.pillActive : ''}`}
>
Todas
</button>
{categories.map(cat => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`${styles.pill} ${selectedCategory === cat ? styles.pillActive : ''}`}
>
{cat}
</button>
))}
</div>
{/* Campo de busca */}
<div className={styles.searchWrapper}>
<i className="fas fa-search" />
<input
type="text"
placeholder="Pesquisar por nome..."
value={search}
onChange={e => {
setSearch(e.target.value)
setSelectedCategory(null)
}}
className={styles.searchInput}
/>
{search && (
<button onClick={() => setSearch('')} className={styles.clearBtn}>
<i className="fas fa-times" />
</button>
)}
</div>
{/* Resultados */}
<div className={styles.content}>
{Object.keys(grouped).length === 0 && (
<p className={styles.empty}>Nenhum produto encontrado.</p>
)}
{Object.entries(grouped).map(([cat, items]) => (
<section key={cat} className={`${styles.section} fade-in-up`}>
<h2 className={styles.catTitle}>{cat}</h2>
<div className={styles.grid}>
{items.map(item => (
<div
key={item.id}
className={styles.card}
onClick={() => openDetails(item)}
>
<div className={styles.cardImgWrap}>
<img src={item.image} alt={item.name} className={styles.cardImg} loading="lazy" />
</div>
<div className={styles.cardBody}>
<h3 className={styles.cardName}>{item.name}</h3>
{item.description && (
<p className={styles.cardDesc}>{item.description}</p>
)}
<div className={styles.cardFooter}>
<span className={styles.cardPrice}>
R$ {item.price.toFixed(2).replace('.', ',')}
</span>
</div>
</div>
</div>
))}
</div>
</section>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,218 @@
.page {
min-height: calc(100vh - var(--header-height) - var(--bottom-nav-height));
padding-bottom: 2rem;
}
/* ── Pills de categoria ───────────────────────── */
.pillRow {
display: flex;
gap: 0.5rem;
overflow-x: auto;
padding: 1rem 1rem 0;
scrollbar-width: none;
-ms-overflow-style: none;
}
.pillRow::-webkit-scrollbar {
display: none;
}
.pill {
flex-shrink: 0;
padding: 0.45rem 1rem;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 600;
background: var(--card-bg);
border: 1px solid var(--card-border);
color: rgba(245, 240, 238, 0.7);
transition: var(--transition);
white-space: nowrap;
cursor: pointer;
}
.pill:hover {
background: var(--accent);
color: var(--text-light);
}
.pillActive {
background: var(--primary) !important;
border-color: var(--secondary) !important;
color: var(--secondary) !important;
}
/* ── Search ───────────────────────────────────── */
.searchWrapper {
position: relative;
padding: 0.75rem 1rem 0;
display: flex;
align-items: center;
}
.searchWrapper>i {
position: absolute;
left: 1.85rem;
color: rgba(245, 240, 238, 0.4);
font-size: 0.9rem;
pointer-events: none;
}
.searchInput {
width: 100%;
padding: 0.75rem 0.75rem 0.75rem 2.5rem;
border-radius: var(--radius-md);
border: 1px solid var(--card-border);
background: rgba(255, 255, 255, 0.07);
backdrop-filter: blur(8px);
color: var(--text-light);
font-size: 0.95rem;
font-family: inherit;
outline: none;
transition: var(--transition);
}
.searchInput::placeholder {
color: rgba(245, 240, 238, 0.35);
}
.searchInput:focus {
border-color: var(--secondary);
background: rgba(255, 255, 255, 0.1);
}
.clearBtn {
position: absolute;
right: 1.75rem;
color: rgba(245, 240, 238, 0.5);
font-size: 0.85rem;
padding: 0.25rem;
cursor: pointer;
}
.clearBtn:hover {
color: var(--secondary);
}
/* ── Conteúdo ─────────────────────────────────── */
.content {
padding: 1.25rem 1rem;
max-width: 1200px;
margin: 0 auto;
}
.section {
margin-bottom: 2rem;
}
.catTitle {
font-size: 1.2rem;
font-weight: 700;
color: var(--secondary);
padding-bottom: 0.5rem;
margin-bottom: 1rem;
border-bottom: 2px solid var(--accent);
display: inline-block;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 0.85rem;
}
/* ── Card ─────────────────────────────────────── */
.card {
background: var(--glass-bg);
border: 1px solid var(--card-border);
border-radius: var(--radius-md);
overflow: hidden;
transition: var(--transition);
cursor: pointer;
backdrop-filter: blur(6px);
}
.card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
border-color: var(--secondary);
}
.card:active {
transform: scale(0.98);
}
.cardImgWrap {
width: 100%;
aspect-ratio: 4/3;
overflow: hidden;
}
.cardImg {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.4s ease;
}
.card:hover .cardImg {
transform: scale(1.06);
}
.cardBody {
padding: 0.75rem;
}
.cardName {
font-size: 0.85rem;
font-weight: 700;
color: var(--text-light);
line-height: 1.3;
margin-bottom: 0.25rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.cardDesc {
font-size: 0.75rem;
color: rgba(245, 240, 238, 0.55);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-bottom: 0.5rem;
}
.cardFooter {
display: flex;
justify-content: space-between;
align-items: center;
}
.cardPrice {
font-size: 1rem;
font-weight: 800;
color: var(--secondary);
}
.empty {
text-align: center;
color: rgba(245, 240, 238, 0.5);
margin-top: 3rem;
font-size: 1rem;
}
/* Tablet+ */
@media (min-width: 640px) {
.grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
}
@media (min-width: 1024px) {
.page {
padding-left: 1rem;
}
}

172
src/pages/DedoNoCopo.jsx Normal file
View File

@@ -0,0 +1,172 @@
import React, { useState, useEffect, useRef } from 'react'
import { Link } from 'react-router-dom'
import styles from './DedoNoCopo.module.css'
export default function DedoNoCopo() {
const [touches, setTouches] = useState([])
const [status, setStatus] = useState('waiting') // 'waiting', 'counting', 'result'
const [winnerId, setWinnerId] = useState(null)
const [progress, setProgress] = useState(0)
const [autoStartTimer, setAutoStartTimer] = useState(null)
const touchAreaRef = useRef(null)
const handleTouch = (e) => {
if (status === 'result') return
updateTouches(e.targetTouches)
}
const updateTouches = (touchList) => {
if (!touchAreaRef.current) return
const rect = touchAreaRef.current.getBoundingClientRect()
const newTouches = Array.from(touchList).map(t => ({
id: t.identifier,
x: t.clientX - rect.left,
y: t.clientY - rect.top
}))
setTouches(newTouches)
}
// Gerenciador de Estados e Timers
useEffect(() => {
if (status === 'result') return
// Se desistirem (menos de 2 dedos), reseta
if (touches.length < 2) {
if (status === 'counting') setStatus('waiting')
setAutoStartTimer(null)
setProgress(0)
return
}
let interval = null
if (status === 'waiting') {
// Fase de Sincronização: 5 segundos
// Sempre que o número de toques muda, o timer reinicia (devido à dependência [touches.length])
setAutoStartTimer(5)
let timeLeft = 5
interval = setInterval(() => {
timeLeft -= 1
setAutoStartTimer(timeLeft)
if (timeLeft <= 0) {
clearInterval(interval)
setStatus('counting')
setAutoStartTimer(null)
}
}, 1000)
} else if (status === 'counting') {
// Fase de Carga (Roleta): ~2.5 segundos
setProgress(0)
let p = 0
interval = setInterval(() => {
p += 2
setProgress(p)
if (p >= 100) {
clearInterval(interval)
finishGame()
}
}, 50)
}
return () => {
if (interval) clearInterval(interval)
}
}, [touches.length, status])
const finishGame = () => {
setTouches(prev => {
if (prev.length === 0) return prev
const winner = prev[Math.floor(Math.random() * prev.length)]
setWinnerId(winner.id)
return prev
})
setStatus('result')
if (navigator.vibrate) navigator.vibrate([100, 50, 200])
}
const resetGame = () => {
setStatus('waiting')
setWinnerId(null)
setProgress(0)
setTouches([])
setAutoStartTimer(null)
}
return (
<div className={styles.page}>
<Link to="/jogos" className={styles.backLink} onClick={resetGame}>
<i className="fas fa-arrow-left"></i> Voltar
</Link>
<div className={styles.titleWrapper}>
<h1 className={styles.title}>Roleta de Toque</h1>
</div>
<div
ref={touchAreaRef}
className={styles.touchArea}
onTouchStart={handleTouch}
onTouchMove={handleTouch}
onTouchEnd={handleTouch}
// Para testes em Desktop (apenas 1 círculo simulado)
onMouseDown={(e) => {
if (status === 'result' || !touchAreaRef.current) return
const rect = touchAreaRef.current.getBoundingClientRect()
setTouches([{ id: 'mouse', x: e.clientX - rect.left, y: e.clientY - rect.top }])
}}
onMouseUp={() => { if (status !== 'result') setTouches([]) }}
>
{touches.map(t => (
<div
key={t.id}
className={`
${styles.fingerCircle}
${status === 'result' && t.id === winnerId ? styles.selected : ''}
${status === 'result' && t.id !== winnerId ? styles.eliminated : ''}
`}
style={{ left: t.x, top: t.y }}
>
{(status === 'counting' || (status === 'result' && t.id === winnerId)) && (
<div
className={`${styles.loadingRing} ${status === 'result' ? styles.winnerRing : ''}`}
style={{
background: status === 'result'
? '#efcd28'
: `conic-gradient(#efcd28 ${progress}%, transparent ${progress}%)`
}}
/>
)}
</div>
))}
{status === 'waiting' && (
<div className={styles.instructions}>
{touches.length < 2 ? (
touches.length === 0 ? "COLOQUEM OS DEDOS NA TELA" : "FALTA MAIS ALGUÉM..."
) : (
<div className={styles.timerAnnounce}>
SINCRONIZANDO... {autoStartTimer}s
</div>
)}
</div>
)}
{status === 'counting' && (
<div className={styles.instructions}>
NÃO TIREM OS DEDOS!
</div>
)}
{status === 'result' && (
<div className={styles.resultOverlay}>
<div className={styles.resultTitle}>ELEITO! 🍺</div>
<p>Quem parou no círculo dourado, bebe!</p>
<button className={styles.resultBtn} onClick={resetGame}>JOGAR NOVAMENTE</button>
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,226 @@
.page {
padding: 0;
margin: 0;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
color: #efc7b8;
background: #0d0d0d;
position: fixed;
top: 0;
left: 0;
user-select: none;
touch-action: none;
}
.titleWrapper {
padding: 20px;
padding-top: 60px;
text-align: center;
z-index: 10;
}
.title {
font-size: 2rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 2px;
text-shadow: 0 0 15px rgba(239, 199, 184, 0.3);
margin: 0;
}
.touchArea {
flex: 1;
width: 100%;
position: relative;
z-index: 5;
overflow: hidden;
}
.fingerCircle {
position: absolute;
width: 100px;
height: 100px;
border-radius: 50%;
border: 4px solid #efc7b8;
transform: translate(-50%, -50%);
transition: transform 0.1s ease-out, border-color 0.3s;
z-index: 100;
}
.fingerCircle::before {
content: '';
position: absolute;
top: -10px;
left: -10px;
right: -10px;
bottom: -10px;
border-radius: 50%;
border: 2px solid rgba(239, 199, 184, 0.3);
animation: pulseAnim 1.5s infinite;
}
.loadingRing {
position: absolute;
top: -10px;
left: -10px;
width: 120px;
height: 120px;
border-radius: 50%;
-webkit-mask-image: radial-gradient(transparent 55px, #000 56px);
mask-image: radial-gradient(transparent 55px, #000 56px);
z-index: 90;
transition: background 0.3s;
}
.winnerRing {
animation: winnerPulse 0.5s infinite alternate;
box-shadow: 0 0 30px #efcd28;
}
@keyframes winnerPulse {
from {
transform: scale(1);
opacity: 1;
}
to {
transform: scale(1.1);
opacity: 0.8;
}
}
.selected {
border-color: #efcd28;
transform: translate(-50%, -50%) scale(1.3);
box-shadow: 0 0 40px rgba(239, 205, 40, 0.6);
z-index: 200;
}
.eliminated {
opacity: 0.2;
transform: translate(-50%, -50%) scale(0.8);
border-color: #333;
}
.instructions {
position: absolute;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
text-align: center;
width: 80%;
z-index: 10;
pointer-events: none;
color: rgba(239, 199, 184, 0.6);
font-size: 1.1rem;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.timerAnnounce {
color: #efcd28;
font-size: 1.8rem;
font-weight: 900;
animation: pulseAnnouncement 1s infinite alternate;
}
@keyframes pulseAnnouncement {
from {
transform: scale(1);
opacity: 0.8;
}
to {
transform: scale(1.1);
opacity: 1;
}
}
.backLink {
position: absolute;
top: 20px;
left: 20px;
color: rgba(239, 199, 184, 0.5);
text-decoration: none;
z-index: 100;
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
}
.backLink:hover {
color: #efc7b8;
}
.resultOverlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.9);
padding: 30px;
border-radius: 30px;
border: 2px solid #efc7b8;
z-index: 300;
text-align: center;
box-shadow: 0 0 50px rgba(0, 0, 0, 1);
animation: popIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.resultTitle {
color: #efcd28;
font-size: 2.5rem;
font-weight: 900;
margin-bottom: 10px;
text-transform: uppercase;
}
.resultBtn {
background: #efc7b8;
color: #1a1a1a;
border: none;
padding: 15px 40px;
border-radius: 50px;
font-weight: 900;
cursor: pointer;
margin-top: 20px;
font-size: 1rem;
text-transform: uppercase;
transition: transform 0.2s;
}
.resultBtn:hover {
transform: scale(1.05);
}
@keyframes pulseAnim {
0% {
transform: scale(1);
opacity: 0.8;
}
100% {
transform: scale(1.4);
opacity: 0;
}
}
@keyframes popIn {
from {
transform: translate(-50%, -50%) scale(0.5);
opacity: 0;
}
to {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}
}

116
src/pages/EuNunca.jsx Normal file
View File

@@ -0,0 +1,116 @@
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import styles from './EuNunca.module.css'
const FRASES = {
light: [
"Eu nunca... esqueci o nome de alguém que acabei de conhecer no bar.",
"Eu nunca... bebi direto da garrafa quando ninguém estava olhando.",
"Eu nunca... fingi estar bêbado para fugir de uma situação.",
"Eu nunca... tentei abrir uma garrafa com o dente ou sapato.",
"Eu nunca... postei algo nas redes sociais e apaguei no dia seguinte.",
"Eu nunca... caí da cadeira no bar.",
"Eu nunca... cantei no karaokê e achei que arrasei (mas não).",
"Eu nunca... bebi algo que não sabia o que era.",
"Eu nunca... dormi em um lugar público após beber.",
"Eu nunca... entrei no banheiro errado.",
"Eu nunca... pedi uma música pro DJ que ele se recusou a tocar.",
"Eu nunca... ganhei um drink de um desconhecido.",
],
hard: [
"Eu nunca... mandei mensagem para o ex depois de beber.",
"Eu nunca... dancei em cima de uma mesa ou balcão.",
"Eu nunca... fiz um 'shot' e me arrependi imediatamente.",
"Eu nunca... saí de fininho para não pagar a conta (ou esqueci).",
"Eu nunca... flertei com o garçom/garçonete para ganhar desconto.",
"Eu nunca... misturei mais de 5 tipos de bebida na mesma noite.",
"Eu nunca... peguei o drink de outra pessoa sem querer.",
"Eu nunca... chorei no bar por motivo nenhum.",
"Eu nunca... fiz um juramento de 'nunca mais vou beber' e quebrei no dia seguinte.",
"Eu nunca... liguei para o chefe enquanto estava no bar.",
]
}
export default function EuNunca() {
const [category, setCategory] = useState("light")
const [currentPhrase, setCurrentPhrase] = useState("")
const [isSliding, setIsSliding] = useState(false)
const [usedIndexes, setUsedIndexes] = useState({ light: [], hard: [] })
const getNewPhrase = (newCategory = category) => {
setIsSliding(true)
setTimeout(() => {
const phrasesList = FRASES[newCategory]
let nextIndex
let currentUsed = usedIndexes[newCategory]
if (currentUsed.length === phrasesList.length) {
currentUsed = []
}
do {
nextIndex = Math.floor(Math.random() * phrasesList.length)
} while (currentUsed.includes(nextIndex))
setCurrentPhrase(phrasesList[nextIndex])
setUsedIndexes(prev => ({
...prev,
[newCategory]: [...currentUsed, nextIndex]
}))
setCategory(newCategory)
setIsSliding(false)
}, 400)
}
useEffect(() => {
getNewPhrase()
}, [])
return (
<div className={styles.page}>
<Link to="/jogos" className={styles.backLink}>
<i className="fas fa-arrow-left"></i> Voltar para Jogos
</Link>
<h1 className={styles.title}>Eu Nunca</h1>
<div className={styles.categorySelector}>
<button
className={`${styles.catBtn} ${category === 'light' ? styles.active : ''}`}
onClick={() => category !== 'light' && getNewPhrase('light')}
>
<i className="fas fa-laugh-beam"></i> Light
</button>
<button
className={`${styles.catBtn} ${category === 'hard' ? styles.active : ''}`}
onClick={() => category !== 'hard' && getNewPhrase('hard')}
>
<i className="fas fa-fire"></i> Hard
</button>
</div>
<div className={styles.cardContainer}>
<div className={`${styles.card} ${isSliding ? styles.slidingOut : ''}`} onClick={() => getNewPhrase()}>
<div className={styles.cardIcon}>
<i className="fas fa-glass-cheers"></i>
</div>
<div className={styles.phrase}>
{currentPhrase}
</div>
<button className={styles.nextBtn}>PRÓXIMA</button>
</div>
</div>
<div className={styles.instructions}>
<strong>Como Jogar:</strong> Leia a frase em voz alta. <br />
Todos que <strong> FIZERAM</strong> o que diz a frase, devem beber um gole! 🥃
</div>
<div style={{ marginTop: '20px', fontSize: '0.8rem', color: 'rgba(239, 199, 184, 0.4)', textAlign: 'center' }}>
Respeite seus limites. Jogo destinado a maiores de 18 anos.
</div>
</div>
)
}

View File

@@ -0,0 +1,179 @@
.page {
padding: 20px;
min-height: calc(100vh - 80px);
display: flex;
flex-direction: column;
align-items: center;
gap: 30px;
color: #efc7b8;
background: linear-gradient(135deg, #1a1a1a 0%, #0d0d0d 100%);
}
.title {
font-size: 2.5rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 2px;
text-shadow: 0 0 15px rgba(239, 199, 184, 0.3);
margin-bottom: 20px;
text-align: center;
color: #efc7b8;
}
.cardContainer {
position: relative;
width: 100%;
max-width: 400px;
height: 500px;
perspective: 1000px;
display: flex;
align-items: center;
justify-content: center;
}
@media (max-width: 500px) {
.cardContainer {
height: 400px;
}
}
.card {
width: 100%;
height: 100%;
background: rgba(42, 42, 42, 0.8);
border-radius: 30px;
border: 2px solid rgba(239, 199, 184, 0.2);
backdrop-filter: blur(15px);
padding: 40px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
transition: transform 0.6s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.3s;
cursor: pointer;
position: relative;
}
.card:hover {
transform: translateY(-10px) rotateX(5deg);
border-color: rgba(239, 199, 184, 0.5);
}
.cardIcon {
font-size: 4rem;
margin-bottom: 30px;
color: #efc7b8;
opacity: 0.8;
}
.phrase {
font-size: 1.5rem;
font-weight: 700;
line-height: 1.4;
color: #fff;
margin-bottom: 40px;
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
}
.nextBtn {
background: #efc7b8;
color: #1a1a1a;
border: none;
padding: 15px 40px;
border-radius: 50px;
font-weight: 800;
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 2px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
}
.nextBtn:hover {
transform: scale(1.05);
box-shadow: 0 15px 30px rgba(239, 199, 184, 0.3);
}
.nextBtn:active {
transform: scale(0.95);
}
.backLink {
margin-top: 30px;
color: rgba(239, 199, 184, 0.5);
text-decoration: none;
font-size: 0.9rem;
transition: color 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.backLink:hover {
color: #efc7b8;
}
.instructions {
margin-top: 40px;
max-width: 500px;
text-align: center;
background: rgba(255, 255, 255, 0.03);
padding: 20px;
border-radius: 15px;
font-size: 0.9rem;
color: rgba(239, 199, 184, 0.6);
line-height: 1.6;
}
.categorySelector {
display: flex;
gap: 15px;
margin-bottom: 20px;
}
.catBtn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(239, 199, 184, 0.2);
color: rgba(239, 199, 184, 0.6);
padding: 10px 25px;
border-radius: 30px;
cursor: pointer;
font-weight: 700;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s;
}
.catBtn:hover {
background: rgba(239, 199, 184, 0.1);
}
.catBtn.active {
background: #efc7b8;
color: #1a1a1a;
border-color: #efc7b8;
box-shadow: 0 0 15px rgba(239, 199, 184, 0.3);
}
@keyframes slideOut {
0% {
transform: translateX(0) rotate(0);
opacity: 1;
}
100% {
transform: translateX(100%) rotate(20deg);
opacity: 0;
}
}
.slidingOut {
animation: slideOut 0.5s forwards;
}

139
src/pages/JogosPage.jsx Normal file
View File

@@ -0,0 +1,139 @@
import { Link } from 'react-router-dom'
import Swal from 'sweetalert2'
import styles from './JogosPage.module.css'
const jogos = [
{
id: 1,
title: 'Tarefa de Bêbado',
description: 'Objetivo: Ficar bêbado 🥴 e pagar mico 🤣.',
image: '/rrbec/images/jogos/tarefa_bebado.png',
playUrl: '/jogos/tarefa-de-bebado',
internal: true,
rules: `
<strong>Objetivo:</strong> Ficar bêbado 🥴 e pagar mico 🤣.<br><br>
<strong>Como Jogar:</strong><br><br>
1 - Reúna os jogadores em volta da mesa.<br><br>
2 - Cada jogador sorteia apenas uma tarefa por rodada.<br><br>
3 - O jogo segue em sentido horário.<br><br>
4 - O jogador que iniciar o jogo clica em SORTEAR.<br><br>
5 - O jogador deverá cumprir a tarefa que aparecer.<br><br>
6 - Se não cumprir, os outros jogadores escolhem um prenda a ser paga.<br><br>
7 - Se a prenda não for paga, o jogador será eliminado.<br><br>
<strong>Liberdade Total:</strong> Sintam-se à vontade para mudar as regras e usar a criatividade!<br><br>
Divirtam-se! 🥳
`,
},
{
id: 2,
title: 'Eu Nunca',
description: 'Frases picantes e engraçadas para animar a mesa.',
image: '/rrbec/images/jogos/eu_nunca.png',
playUrl: '/jogos/eu-nunca',
internal: true,
rules: `
<strong>Como Jogar:</strong><br><br>
1 - Um jogador lê a frase da carta.<br><br>
2 - Todos os jogadores que <strong>JÁ FIZERAM</strong> o que está escrito na carta devem beber um gole.<br><br>
3 - Quem nunca fez, não bebe.<br><br>
4 - Clique em PRÓXIMA para mudar a frase.<br><br>
Divirtam-se com moderação! 🥃
`,
},
{
id: 3,
title: 'Quem é Mais Provável?',
description: 'Um jogo de apontar dedos e descobrir o que seus amigos pensam de você.',
image: '/rrbec/images/jogos/quem_mais_provavel.png',
playUrl: '/jogos/quem-e-mais-provavel',
internal: true,
rules: `
<strong>Como Jogar:</strong><br><br>
1 - O grupo lê a pergunta da tela.<br><br>
2 - Alguém conta "3... 2... 1...".<br><br>
3 - No "JÁ!", todos devem apontar para a pessoa que acham que mais provavelmente faria aquilo.<br><br>
4 - A pessoa mais votada bebe um gole (ou paga uma prenda)!<br><br>
Divirtam-se! 🍻
`,
},
{
id: 4,
title: 'Roleta de Toque',
description: 'Decida quem paga a rodada com um toque na tela. Rápido e emocionante!',
image: '/rrbec/images/jogos/roleta_toque.png',
playUrl: '/jogos/dedo-no-copo',
internal: true,
rules: `
<strong>Como Jogar:</strong><br><br>
1 - Todos os jogadores colocam um dedo na tela.<br><br>
2 - Quando houver 2 ou mais dedos, uma contagem de <strong>5 segundos</strong> iniciará automaticamente.<br><br>
3 - Se um novo amigo entrar na roda durante a contagem, o tempo reinicia para dar chance a todos!<br><br>
4 - Após os 5 segundos, o anel dourado começará a carregar. Não tire o dedo!<br><br>
5 - No final, apenas um círculo ficará iluminado. O "Eleito" paga a rodada! 🍺<br><br>
<em>Dica: Se alguém tirar o dedo antes da roleta terminar, o jogo volta para o início.</em>
`,
},
]
export default function JogosPage() {
function showRules(jogo) {
Swal.fire({
title: 'Regras do Jogo',
html: `<div style="text-align:left;font-size:15px;line-height:1.6">${jogo.rules}</div>`,
showCloseButton: true,
showConfirmButton: false,
background: '#2a2a2a',
color: '#efc7b8',
})
}
return (
<div className={styles.page}>
<h2 className={styles.heading}>
<i className="fas fa-gamepad" /> Jogos de Bar
</h2>
<div className={styles.grid}>
{jogos.map(jogo => (
<div key={jogo.id} className={styles.card}>
<div className={styles.imgWrap}>
<img src={jogo.image} alt={jogo.title} className={styles.img} />
<div className={styles.imgOverlay} />
</div>
<div className={styles.body}>
<h3 className={styles.title}>{jogo.title}</h3>
<p className={styles.desc}>{jogo.description}</p>
<div className={styles.actions}>
<button
onClick={() => showRules(jogo)}
className={styles.rulesBtn}
>
<i className="fas fa-book" /> Regras
</button>
{jogo.internal ? (
<Link
to={jogo.playUrl}
className={styles.playBtn}
>
<i className="fas fa-play" /> Jogar
</Link>
) : (
<a
href={jogo.playUrl}
target="_blank"
rel="noopener noreferrer"
className={styles.playBtn}
>
<i className="fas fa-play" /> Jogar
</a>
)}
</div>
</div>
</div>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,119 @@
.page {
min-height: calc(100vh - var(--header-height) - var(--bottom-nav-height));
padding: 1.5rem 1rem;
}
.heading {
font-size: 1.3rem;
font-weight: 700;
color: var(--secondary);
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.6rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.25rem;
max-width: 900px;
}
/* ── Card ─────────────────────────────────────── */
.card {
background: var(--glass-bg);
border: 1px solid var(--card-border);
border-radius: var(--radius-lg);
overflow: hidden;
transition: var(--transition);
backdrop-filter: blur(8px);
}
.card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-lg);
border-color: var(--secondary);
}
.imgWrap {
position: relative;
aspect-ratio: 16/9;
overflow: hidden;
}
.img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.4s ease;
}
.card:hover .img {
transform: scale(1.06);
}
.imgOverlay {
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(42, 42, 42, 0.85) 0%, transparent 60%);
}
.body {
padding: 1.1rem;
}
.title {
font-size: 1.1rem;
font-weight: 700;
color: var(--text-light);
margin-bottom: 0.4rem;
}
.desc {
font-size: 0.875rem;
color: rgba(245, 240, 238, 0.55);
margin-bottom: 1rem;
}
.actions {
display: flex;
gap: 0.75rem;
}
.rulesBtn,
.playBtn {
flex: 1;
padding: 0.6rem 0.75rem;
border-radius: var(--radius-md);
font-size: 0.875rem;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
transition: var(--transition);
cursor: pointer;
}
.rulesBtn {
background: var(--card-bg);
border: 1px solid var(--card-border);
color: var(--secondary);
}
.rulesBtn:hover {
background: var(--accent);
border-color: var(--secondary);
}
.playBtn {
background: var(--primary);
border: 1px solid var(--primary);
color: var(--secondary);
}
.playBtn:hover {
background: var(--primary-hover);
box-shadow: var(--shadow-sm);
}

120
src/pages/KaraokePage.jsx Normal file
View File

@@ -0,0 +1,120 @@
import { useState, useEffect, useRef } from 'react'
import { fetchKaraokeData, calculateAllEstimates } from '../services/karaokeService'
import styles from './KaraokePage.module.css'
export default function KaraokePage() {
const [queue, setQueue] = useState([])
const [search, setSearch] = useState('')
const [status, setStatus] = useState('loading') // loading | ok | error
const intervalRef = useRef(null)
async function loadQueue() {
try {
const raw = await fetchKaraokeData()
const processed = calculateAllEstimates(raw)
setQueue(processed)
setStatus('ok')
} catch (e) {
console.error(e)
setStatus('error')
}
}
useEffect(() => {
loadQueue()
intervalRef.current = setInterval(loadQueue, 20000)
return () => clearInterval(intervalRef.current)
}, [])
const filtered = queue.filter(item => {
const term = search.toLowerCase()
return (
(item.nome && item.nome.toLowerCase().includes(term)) ||
(item.musica && item.musica.toLowerCase().includes(term))
)
})
return (
<div className={styles.page}>
<div className={styles.card}>
{/* Header da fila */}
<div className={styles.cardHeader}>
<h2 className={styles.cardTitle}>
<i className="fas fa-microphone" /> Fila do Karaokê
</h2>
<button
onClick={loadQueue}
className={styles.refreshBtn}
title="Atualizar lista"
>
<i className="fas fa-rotate-right" />
</button>
</div>
{/* Campo de busca */}
<div className={styles.searchWrapper}>
<i className="fas fa-search" />
<input
type="text"
placeholder="Buscar por nome ou música..."
value={search}
onChange={e => setSearch(e.target.value)}
className={styles.searchInput}
/>
</div>
{/* Lista */}
<div className={styles.list}>
{status === 'loading' && (
<div className={styles.loading}>
{[1, 2, 3].map(i => (
<div key={i} className={`${styles.skeletonItem} skeleton`} />
))}
</div>
)}
{status === 'error' && (
<p className={styles.errorMsg}>
<i className="fas fa-circle-exclamation" />
&nbsp;Erro ao carregar a fila. Tente novamente.
</p>
)}
{status === 'ok' && filtered.length === 0 && (
<p className={styles.emptyMsg}>
{search ? 'Nenhum resultado encontrado.' : 'A fila está vazia no momento.'}
</p>
)}
{status === 'ok' && filtered.map((item, index) => (
<div key={item.id ?? index} className={`${styles.item} fade-in-up`}>
<div className={styles.itemPosition}>
{index === 0 ? (
<span className={styles.nowBadge}>
<i className="fas fa-microphone-lines" />
</span>
) : (
<span className={styles.posNumber}>{index + 1}</span>
)}
</div>
<div className={styles.itemInfo}>
<div className={styles.itemTop}>
<span className={styles.itemName}>{item.nome}</span>
<span className={styles.itemSong}> {item.musica}</span>
{(String(item.primeiraVez).toUpperCase() === 'TRUE' || String(item.primeiraVez) === '1') && (
<span className={styles.firstBadge}>1ª vez! 🎉</span>
)}
</div>
<p className={styles.itemMeta}>
{item.peopleRemaining}
<span className={styles.itemEta}> · Estimado: {item.estimatedTime}</span>
</p>
</div>
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,206 @@
.page {
min-height: calc(100vh - var(--header-height) - var(--bottom-nav-height));
padding: 1.25rem;
display: flex;
justify-content: center;
}
.card {
width: 100%;
max-width: 600px;
background: var(--glass-bg);
border: 1px solid var(--card-border);
border-radius: var(--radius-lg);
overflow: hidden;
backdrop-filter: blur(10px);
box-shadow: var(--shadow-lg);
height: fit-content;
}
.cardHeader {
padding: 1.25rem 1.25rem 0;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--card-border);
padding-bottom: 1rem;
}
.cardTitle {
font-size: 1.15rem;
font-weight: 700;
color: var(--secondary);
display: flex;
align-items: center;
gap: 0.6rem;
}
.refreshBtn {
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: rgba(245, 240, 238, 0.5);
background: var(--card-bg);
border: 1px solid var(--card-border);
transition: var(--transition);
cursor: pointer;
font-size: 0.85rem;
}
.refreshBtn:hover {
color: var(--secondary);
background: var(--accent);
transform: rotate(90deg);
}
/* ── Search ────────────────────────────────────── */
.searchWrapper {
position: relative;
padding: 1rem 1.25rem 0;
display: flex;
align-items: center;
}
.searchWrapper>i {
position: absolute;
left: 2rem;
color: rgba(245, 240, 238, 0.4);
font-size: 0.85rem;
pointer-events: none;
}
.searchInput {
width: 100%;
padding: 0.65rem 0.75rem 0.65rem 2.25rem;
border-radius: var(--radius-md);
border: 1px solid var(--card-border);
background: rgba(255, 255, 255, 0.06);
color: var(--text-light);
font-size: 0.9rem;
font-family: inherit;
outline: none;
transition: var(--transition);
}
.searchInput::placeholder {
color: rgba(245, 240, 238, 0.35);
}
.searchInput:focus {
border-color: var(--secondary);
background: rgba(255, 255, 255, 0.09);
}
/* ── List ─────────────────────────────────────── */
.list {
padding: 0.75rem 1.25rem 1.25rem;
}
.loading {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding-top: 0.75rem;
}
.skeletonItem {
height: 68px;
width: 100%;
}
.errorMsg,
.emptyMsg {
text-align: center;
padding: 2rem 0;
font-size: 0.9rem;
color: rgba(245, 240, 238, 0.45);
}
.errorMsg {
color: #f87171;
}
/* ── Item ─────────────────────────────────────── */
.item {
display: flex;
align-items: flex-start;
gap: 0.85rem;
padding: 0.9rem 0;
border-bottom: 1px solid var(--card-border);
}
.item:last-child {
border-bottom: none;
}
.itemPosition {
flex-shrink: 0;
width: 34px;
display: flex;
align-items: center;
justify-content: center;
}
.nowBadge {
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 50%;
background: var(--primary);
color: var(--secondary);
font-size: 0.85rem;
animation: pulse 1.5s ease-in-out infinite;
}
.posNumber {
font-size: 0.9rem;
font-weight: 700;
color: rgba(245, 240, 238, 0.3);
}
.itemInfo {
flex: 1;
min-width: 0;
}
.itemTop {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.3rem;
margin-bottom: 0.25rem;
}
.itemName {
font-size: 1rem;
font-weight: 700;
color: var(--secondary);
}
.itemSong {
font-size: 0.85rem;
color: rgba(245, 240, 238, 0.6);
}
.firstBadge {
font-size: 0.7rem;
font-weight: 700;
background: #eab308;
color: #1a1a1a;
padding: 0.15rem 0.5rem;
border-radius: 999px;
}
.itemMeta {
font-size: 0.8rem;
color: rgba(245, 240, 238, 0.45);
}
.itemEta {
color: rgba(245, 240, 238, 0.3);
}

View File

@@ -0,0 +1,86 @@
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import styles from './QuemEMaisProvavel.module.css'
const PERGUNTAS = [
"Quem é mais provável de... ser expulso do bar primeiro?",
"Quem é mais provável de... subir no palco do karaokê sem ter sido convidado?",
"Quem é mais provável de... esquecer de pagar a conta?",
"Quem é mais provável de... se tornar o melhor amigo do garçom?",
"Quem é mais provável de... perder o celular até o fim da noite?",
"Quem é mais provável de... dormir na mesa do bar?",
"Quem é mais provável de... começar uma briga (ou tentar separar uma)?",
"Quem é mais provável de... pedir a música mais chata pro DJ?",
"Quem é mais provável de... ligar pro ex chorando às 3 da manhã?",
"Quem é mais provável de... desaparecer do nada e aparecer em outra festa?",
"Quem é mais provável de... convencer todo mundo a tomar um shot?",
"Quem é mais provável de... postar um vídeo vergonhoso no Instagram?",
"Quem é mais provável de... casar com alguém que conheceu em um bar?",
"Quem é mais provável de... ganhar uma competição de quem bebe mais rápido?",
"Quem é mais provável de... esquecer onde estacionou o carro?",
"Quem é mais provável de... ser a 'mãe/pai' do grupo e cuidar dos bêbados?",
"Quem é mais provável de... gastar todo o dinheiro em rodadas de bebida?",
"Quem é mais provável de... confundir o banheiro masculino com o feminino?",
"Quem é mais provável de... tentar xavecar a pessoa errada?",
"Quem é mais provável de... ser o último a sair do bar?",
]
export default function QuemEMaisProvavel() {
const [currentQuestion, setCurrentQuestion] = useState("")
const [usedIndexes, setUsedIndexes] = useState([])
const [key, setKey] = useState(0) // Para forçar a animação
const getNextQuestion = () => {
let nextIndex
if (usedIndexes.length === PERGUNTAS.length) {
nextIndex = Math.floor(Math.random() * PERGUNTAS.length)
setUsedIndexes([nextIndex])
} else {
do {
nextIndex = Math.floor(Math.random() * PERGUNTAS.length)
} while (usedIndexes.includes(nextIndex))
setUsedIndexes(prev => [...prev, nextIndex])
}
setCurrentQuestion(PERGUNTAS[nextIndex])
setKey(prev => prev + 1)
}
useEffect(() => {
getNextQuestion()
}, [])
return (
<div className={styles.page}>
<Link to="/jogos" className={styles.backLink}>
<i className="fas fa-arrow-left"></i> Voltar para Jogos
</Link>
<h1 className={styles.title}>Quem é mais provável?</h1>
<div className={styles.container}>
<div className={styles.icon}>
<i className="fas fa-users-cog"></i>
</div>
<div key={key} className={styles.question}>
{currentQuestion}
</div>
<button className={styles.nextBtn} onClick={getNextQuestion}>
PRÓXIMA PERGUNTA
</button>
</div>
<div className={styles.instructions}>
<strong>Como Jogar:</strong> <br />
No 3... 2... 1... todos devem <strong>APONTAR</strong> para a pessoa que acham mais provável de fazer o que diz a frase!
</div>
<div style={{ marginTop: '20px', fontSize: '0.8rem', color: 'rgba(239, 199, 184, 0.4)', textAlign: 'center' }}>
Apenas para diversão. Brinquem com respeito!
</div>
</div>
)
}

View File

@@ -0,0 +1,137 @@
.page {
padding: 20px;
min-height: calc(100vh - 80px);
display: flex;
flex-direction: column;
align-items: center;
gap: 30px;
color: #efc7b8;
background: linear-gradient(135deg, #1a1a1a 0%, #0d0d0d 100%);
}
.title {
font-size: 2.5rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 2px;
text-shadow: 0 0 15px rgba(239, 199, 184, 0.3);
margin-bottom: 20px;
text-align: center;
margin-top: 10px;
}
.container {
width: 100%;
max-width: 500px;
background: rgba(42, 42, 42, 0.8);
border-radius: 30px;
border: 2px solid rgba(239, 199, 184, 0.2);
backdrop-filter: blur(15px);
padding: 50px 30px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
position: relative;
min-height: 350px;
}
.icon {
font-size: 3.5rem;
margin-bottom: 30px;
color: #efc7b8;
opacity: 0.9;
filter: drop-shadow(0 0 10px rgba(239, 199, 184, 0.4));
}
.question {
font-size: 1.8rem;
font-weight: 700;
line-height: 1.3;
color: #fff;
margin-bottom: 40px;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.5s cubic-bezier(0.23, 1, 0.32, 1);
}
.nextBtn {
background: #efc7b8;
color: #1a1a1a;
border: none;
padding: 18px 50px;
border-radius: 50px;
font-weight: 900;
font-size: 1.1rem;
text-transform: uppercase;
letter-spacing: 2px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
}
.nextBtn:hover {
transform: scale(1.05);
box-shadow: 0 15px 30px rgba(239, 199, 184, 0.4);
}
.nextBtn:active {
transform: scale(0.95);
}
.backLink {
margin-top: 20px;
color: rgba(239, 199, 184, 0.5);
text-decoration: none;
font-size: 0.9rem;
transition: color 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.backLink:hover {
color: #efc7b8;
}
.instructions {
margin-top: 40px;
max-width: 500px;
text-align: center;
background: rgba(255, 255, 255, 0.03);
padding: 25px;
border-radius: 20px;
font-size: 1rem;
color: rgba(239, 199, 184, 0.7);
line-height: 1.6;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@media (max-width: 480px) {
.title {
font-size: 1.8rem;
}
.question {
font-size: 1.4rem;
}
.container {
padding: 30px 20px;
}
}

View File

@@ -0,0 +1,131 @@
import React, { useState, useRef } from 'react'
import { Link } from 'react-router-dom'
import styles from './TarefaDeBebado.module.css'
const TAREFAS = [
{ id: 1, text: 'Dose Dupla', color: '#e74c3c' },
{ id: 2, text: 'Mestre do Silêncio', color: '#3498db' },
{ id: 3, text: 'Verdade ou Consequência', color: '#9b59b6' },
{ id: 4, text: 'Eu Nunca', color: '#f1c40f' },
{ id: 5, text: 'Cante um Refrão', color: '#2ecc71' },
{ id: 6, text: 'Vire o Copo', color: '#e67e22' },
{ id: 7, text: 'Social (Todos bebem!)', color: '#1abc9c' },
{ id: 8, text: 'Imite um Bêbado', color: '#34495e' },
{ id: 9, text: 'Desafio de Trava-Língua', color: '#d35400' },
{ id: 10, text: 'Sorte (Escolha alguém)', color: '#27ae60' },
]
export default function TarefaDeBebado() {
const [isSpinning, setIsSpinning] = useState(false)
const [rotation, setRotation] = useState(0)
const [resultado, setResultado] = useState(null)
const wheelRef = useRef(null)
const spin = () => {
if (isSpinning) return
setIsSpinning(true)
setResultado(null)
// Sorteia um ângulo extra (pelo menos 5 voltas completas + ângulo aleatório)
const extraDegrees = Math.floor(Math.random() * 360)
const spinDegrees = rotation + 1800 + extraDegrees
setRotation(spinDegrees)
// Calcula qual tarefa caiu
// Como a roleta gira no sentido horário, mas o ângulo aumenta positivamente,
// precisamos ajustar o cálculo para o topo (onde está o ponteiro)
setTimeout(() => {
setIsSpinning(false)
const actualDegrees = spinDegrees % 360
const n = TAREFAS.length
const sectorSize = 360 / n
// O ponteiro está no topo (270 graus ou -90 no círculo trigonométrico padrão)
// Mas o CSS transform rotate gira tudo.
// A fórmula abaixo mapeia o ângulo final para o índice correto
const index = Math.floor(((360 - actualDegrees) % 360) / sectorSize)
setResultado(TAREFAS[index])
}, 5000)
}
return (
<div className={styles.page}>
<Link to="/jogos" className={styles.backLink}>
<i className="fas fa-arrow-left"></i> Voltar para Jogos
</Link>
<h1 className={styles.title}>Tarefa de Bêbado</h1>
<div className={styles.gameContainer}>
<div className={styles.pointer}></div>
<div className={styles.centerPin}>
<button
className={styles.spinButton}
onClick={spin}
disabled={isSpinning}
>
Girar
</button>
</div>
<div
className={styles.wheelWrapper}
style={{
transform: `rotate(${rotation}deg)`,
background: `conic-gradient(${TAREFAS.map((t, i) => `${t.color} ${(360 / TAREFAS.length) * i}deg ${(360 / TAREFAS.length) * (i + 1)}deg`).join(', ')})`
}}
ref={wheelRef}
>
{TAREFAS.map((tarefa, i) => {
const angle = (360 / TAREFAS.length) * i + (360 / TAREFAS.length / 2)
return (
<div
key={tarefa.id}
className={styles.labelWrapper}
style={{ transform: `rotate(${angle}deg)` }}
>
<div className={styles.sectorLabel}>
{tarefa.id}
</div>
</div>
)
})}
</div>
</div>
<div className={styles.resultContainer} style={{ opacity: resultado ? 1 : 0.3 }}>
{resultado ? (
<>
<div className={styles.resultTitle}>Opção {resultado.id}:</div>
<div className={styles.resultText}>{resultado.text}</div>
</>
) : (
<div className={styles.resultText}>
{isSpinning ? 'Sorteando...' : 'Gire a roleta!'}
</div>
)}
</div>
<div className={styles.legendContainer}>
<h3 className={styles.legendTitle}>Legenda das Tarefas</h3>
<div className={styles.legendGrid}>
{TAREFAS.map(tarefa => (
<div key={tarefa.id} className={styles.legendItem}>
<span className={styles.legendNumber} style={{ backgroundColor: tarefa.color }}>
{tarefa.id}
</span>
<span className={styles.legendText}>{tarefa.text}</span>
</div>
))}
</div>
</div>
<div style={{ marginTop: '20px', fontSize: '0.8rem', color: 'rgba(239, 199, 184, 0.4)', textAlign: 'center' }}>
Respeite as leis e seus limites. <br /> Beber e dirigir é crime. Se beber, não dirija.
</div>
</div>
)
}

View File

@@ -0,0 +1,233 @@
.page {
padding: 20px;
min-height: calc(100vh - 80px);
display: flex;
flex-direction: column;
align-items: center;
gap: 30px;
color: #efc7b8;
background: linear-gradient(135deg, #1a1a1a 0%, #0d0d0d 100%);
}
.title {
font-size: 2.5rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 2px;
text-shadow: 0 0 15px rgba(239, 199, 184, 0.3);
margin-bottom: 20px;
text-align: center;
color: #efc7b8;
}
.gameContainer {
position: relative;
width: 400px;
height: 400px;
margin: 40px auto;
}
@media (max-width: 500px) {
.gameContainer {
width: 320px;
height: 320px;
}
}
.wheelWrapper {
width: 100%;
height: 100%;
border-radius: 50%;
border: 10px solid #2a2a2a;
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8), 0 0 10px rgba(239, 199, 184, 0.1);
position: relative;
overflow: hidden;
transition: transform 5s cubic-bezier(0.15, 0, 0.15, 1);
}
.labelWrapper {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: flex;
justify-content: center;
padding-top: 20px;
pointer-events: none;
}
.sectorLabel {
font-weight: 800;
font-size: 28px;
color: #fff;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
user-select: none;
}
/* Center Pin */
.centerPin {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60px;
height: 60px;
background: #2a2a2a;
border: 4px solid #efc7b8;
border-radius: 50%;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
}
.spinButton {
background: none;
border: none;
color: #efc7b8;
font-weight: 900;
cursor: pointer;
font-size: 1rem;
text-transform: uppercase;
transition: transform 0.2s;
}
.spinButton:hover {
transform: scale(1.1);
}
.spinButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Pointer Arrow */
.pointer {
position: absolute;
top: -25px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 40px;
background: #efc7b8;
clip-path: polygon(50% 100%, 0 0, 100% 0);
z-index: 20;
filter: drop-shadow(0 2px 5px rgba(0, 0, 0, 0.5));
}
.resultContainer {
margin-top: 40px;
text-align: center;
max-width: 500px;
padding: 30px;
background: rgba(42, 42, 42, 0.6);
border-radius: 20px;
border: 1px solid rgba(239, 199, 184, 0.2);
backdrop-filter: blur(10px);
min-height: 120px;
display: flex;
flex-direction: column;
justify-content: center;
animation: fadeIn 0.5s ease-out;
}
.resultTitle {
font-size: 0.9rem;
text-transform: uppercase;
color: rgba(239, 199, 184, 0.6);
margin-bottom: 10px;
letter-spacing: 3px;
}
.resultText {
font-size: 1.8rem;
font-weight: bold;
color: #efc7b8;
line-height: 1.2;
}
.backLink {
margin-top: 30px;
color: rgba(239, 199, 184, 0.5);
text-decoration: none;
font-size: 0.9rem;
transition: color 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.backLink:hover {
color: #efc7b8;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.legendContainer {
width: 100%;
max-width: 600px;
background: rgba(255, 255, 255, 0.05);
border-radius: 20px;
padding: 20px;
margin-top: 20px;
}
.legendTitle {
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 15px;
text-align: center;
color: rgba(239, 199, 184, 0.7);
}
.legendGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
@media (max-width: 480px) {
.legendGrid {
grid-template-columns: 1fr;
}
}
.legendItem {
display: flex;
align-items: center;
gap: 12px;
background: rgba(0, 0, 0, 0.2);
padding: 8px 12px;
border-radius: 10px;
}
.legendNumber {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 0.8rem;
color: #fff;
flex-shrink: 0;
}
.legendText {
font-size: 0.9rem;
color: #efc7b8;
}