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

0
README.md Normal file
View File

29
eslint.config.js Normal file
View File

@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

23
index.html Normal file
View File

@@ -0,0 +1,23 @@
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="https://files.menudino.com/cardapios/53448/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Raul Rock Bar & Café - Cardápio, Karaokê e Jogos de Bar" />
<title>Raul Rock Bar & Café</title>
<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=Inter:wght@400;500;600;700;800&family=Playfair+Display:wght@700;900&display=swap"
rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

2989
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "site_rrbec",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.13.1",
"sweetalert2": "^11.26.20"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"vite": "^7.3.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 KiB

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

42
src/App.css Normal file
View File

@@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

30
src/App.jsx Normal file
View File

@@ -0,0 +1,30 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import Layout from './components/Layout'
import BioPage from './pages/BioPage'
import CardapioPage from './pages/CardapioPage'
import KaraokePage from './pages/KaraokePage'
import JogosPage from './pages/JogosPage'
import TarefaDeBebado from './pages/TarefaDeBebado'
import EuNunca from './pages/EuNunca'
import QuemEMaisProvavel from './pages/QuemEMaisProvavel'
import DedoNoCopo from './pages/DedoNoCopo'
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<BioPage />} />
<Route path="cardapio" element={<CardapioPage />} />
<Route path="karaoke" element={<KaraokePage />} />
<Route path="jogos" element={<JogosPage />} />
<Route path="jogos/tarefa-de-bebado" element={<TarefaDeBebado />} />
<Route path="jogos/eu-nunca" element={<EuNunca />} />
<Route path="jogos/quem-e-mais-provavel" element={<QuemEMaisProvavel />} />
<Route path="jogos/dedo-no-copo" element={<DedoNoCopo />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Routes>
</BrowserRouter>
)
}

1
src/assets/react.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

124
src/components/Layout.jsx Normal file
View File

@@ -0,0 +1,124 @@
import { useState, useEffect } from 'react'
import { Outlet, NavLink, useLocation, useNavigate } from 'react-router-dom'
import styles from './Layout.module.css'
const navItems = [
{ path: '/', label: 'Bio', icon: 'fa-solid fa-user', end: true },
{ path: '/cardapio', label: 'Cardápio', icon: 'fa-solid fa-utensils', end: false },
{ path: '/karaoke', label: 'Karaokê', icon: 'fa-solid fa-microphone', end: false },
{ path: '/jogos', label: 'Jogos', icon: 'fa-solid fa-gamepad', end: false },
]
const pageTitles = {
'/': 'Bio do Instagram',
'/cardapio': 'Cardápio',
'/karaoke': 'Karaokê',
'/jogos': 'Jogos de Bar',
}
export default function Layout() {
const [sidebarOpen, setSidebarOpen] = useState(false)
const location = useLocation()
const navigate = useNavigate()
const title = pageTitles[location.pathname] ?? 'Raul Rock Bar'
// Fecha sidebar ao mudar de rota
useEffect(() => {
setSidebarOpen(false)
}, [location.pathname])
// Fecha sidebar ao clicar fora (escape)
useEffect(() => {
const handleKey = (e) => { if (e.key === 'Escape') setSidebarOpen(false) }
window.addEventListener('keydown', handleKey)
return () => window.removeEventListener('keydown', handleKey)
}, [])
return (
<div className={styles.app}>
{/* ── Header ─────────────────────────────── */}
<header className={styles.header}>
<div className={styles.headerInner}>
<div className={styles.headerLeft}>
<button
className={styles.menuBtn}
onClick={() => setSidebarOpen(v => !v)}
aria-label="Abrir menu"
>
<i className={`fa-solid ${sidebarOpen ? 'fa-xmark' : 'fa-bars'}`} />
</button>
<img
src="https://files.menudino.com/cardapios/53448/logo.png"
alt="Logo"
className={styles.headerLogo}
onClick={() => navigate('/')}
/>
<span className={styles.headerTitle}>{title}</span>
</div>
</div>
</header>
{/* ── Sidebar ─────────────────────────────── */}
<>
<div
className={`${styles.overlay} ${sidebarOpen ? styles.overlayVisible : ''}`}
onClick={() => setSidebarOpen(false)}
/>
<aside className={`${styles.sidebar} ${sidebarOpen ? styles.sidebarOpen : ''}`}>
<div className={styles.sidebarHeader}>
<img
src="https://files.menudino.com/cardapios/53448/logo.png"
alt="Logo"
className={styles.sidebarLogo}
/>
<p className={styles.sidebarBrand}>Raul Rock Bar<br /><span>& Café</span></p>
</div>
<nav className={styles.sidebarNav}>
<p className={styles.sidebarSection}>Menu</p>
{navItems.map(item => (
<NavLink
key={item.path}
to={item.path}
end={item.end}
className={({ isActive }) =>
`${styles.navLink} ${isActive ? styles.navLinkActive : ''}`
}
>
<i className={item.icon} />
{item.label}
</NavLink>
))}
</nav>
<div className={styles.sidebarFooter}>
<p>© 2025 Raul Rock Bar & Café</p>
</div>
</aside>
</>
{/* ── Conteúdo principal ──────────────────── */}
<main className={styles.main}>
<Outlet />
</main>
{/* ── Bottom Nav (mobile) ─────────────────── */}
<nav className={styles.bottomNav}>
{navItems.map(item => (
<NavLink
key={item.path}
to={item.path}
end={item.end}
className={({ isActive }) =>
`${styles.bottomNavItem} ${isActive ? styles.bottomNavActive : ''}`
}
>
<i className={item.icon} />
<span>{item.label}</span>
</NavLink>
))}
</nav>
</div>
)
}

View File

@@ -0,0 +1,272 @@
/* ── App wrapper ─────────────────────────────── */
.app {
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* ── Header ──────────────────────────────────── */
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--header-height);
background: var(--primary);
z-index: 100;
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
}
.headerInner {
height: 100%;
padding: 0 1rem;
display: flex;
align-items: center;
justify-content: space-between;
max-width: 1400px;
margin: 0 auto;
width: 100%;
}
.headerLeft {
display: flex;
align-items: center;
gap: 0.75rem;
}
.headerLogo {
width: 38px;
height: 38px;
border-radius: 50%;
object-fit: cover;
cursor: pointer;
transition: var(--transition);
}
.headerLogo:hover {
transform: scale(1.08);
}
.headerTitle {
font-size: 1.2rem;
font-weight: 700;
color: var(--secondary);
letter-spacing: 0.01em;
}
.menuBtn {
color: var(--secondary);
font-size: 1.3rem;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
transition: var(--transition);
}
.menuBtn:hover {
background: rgba(255, 255, 255, 0.1);
}
/* ── Overlay ─────────────────────────────────── */
.overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0);
z-index: 149;
pointer-events: none;
transition: background 0.3s ease;
}
.overlayVisible {
background: rgba(0, 0, 0, 0.6);
pointer-events: all;
}
/* ── Sidebar ─────────────────────────────────── */
.sidebar {
position: fixed;
top: 0;
left: 0;
width: var(--sidebar-width);
height: 100vh;
background: var(--sidebar-bg);
z-index: 150;
display: flex;
flex-direction: column;
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.4);
overflow-y: auto;
}
.sidebarOpen {
transform: translateX(0);
}
.sidebarHeader {
padding: 2rem 1.5rem 1.5rem;
display: flex;
align-items: center;
gap: 1rem;
border-bottom: 1px solid var(--card-border);
}
.sidebarLogo {
width: 52px;
height: 52px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.sidebarBrand {
font-size: 1rem;
font-weight: 700;
color: var(--text-light);
line-height: 1.3;
}
.sidebarBrand span {
color: var(--secondary);
font-weight: 400;
}
.sidebarNav {
flex: 1;
padding: 1.5rem 1rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.sidebarSection {
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--secondary);
opacity: 0.6;
padding: 0 0.5rem;
margin-bottom: 0.5rem;
}
.navLink {
display: flex;
align-items: center;
gap: 0.85rem;
padding: 0.75rem 1rem;
border-radius: var(--radius-md);
font-size: 1rem;
font-weight: 500;
color: rgba(245, 240, 238, 0.75);
transition: var(--transition);
}
.navLink i {
width: 20px;
text-align: center;
font-size: 1rem;
}
.navLink:hover {
background: rgba(255, 255, 255, 0.07);
color: var(--text-light);
}
.navLinkActive {
background: var(--primary) !important;
color: var(--secondary) !important;
font-weight: 600;
}
.sidebarFooter {
padding: 1rem 1.5rem;
border-top: 1px solid var(--card-border);
font-size: 0.75rem;
color: rgba(245, 240, 238, 0.35);
text-align: center;
}
/* ── Main content ────────────────────────────── */
.main {
flex: 1;
padding-top: var(--header-height);
padding-bottom: var(--bottom-nav-height);
}
/* ── Bottom Nav (mobile only) ─────────────────── */
.bottomNav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: var(--bottom-nav-height);
background: var(--sidebar-bg);
display: flex;
align-items: stretch;
border-top: 1px solid var(--card-border);
z-index: 90;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3);
}
.bottomNavItem {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
font-size: 0.65rem;
font-weight: 500;
color: rgba(245, 240, 238, 0.5);
transition: var(--transition);
}
.bottomNavItem i {
font-size: 1.15rem;
}
.bottomNavActive {
color: var(--secondary) !important;
}
.bottomNavItem:hover {
color: var(--text-light);
}
/* Desktop: sidebar fixa, esconder bottom nav e botão menu */
@media (min-width: 768px) {
.sidebar {
transform: translateX(0);
box-shadow: 2px 0 12px rgba(0, 0, 0, 0.2);
}
.overlay {
display: none;
}
.menuBtn {
display: none !important;
}
.header {
left: var(--sidebar-width);
}
.headerInner {
padding: 0 2rem;
}
.main {
padding-left: var(--sidebar-width);
padding-bottom: 0;
}
.bottomNav {
display: none;
}
}

23
src/data/menuData.js Normal file
View File

@@ -0,0 +1,23 @@
export const menuData = [
{ id: 83, name: "PETRA", description: "", price: 9.50, category: "Cervejas 600ml", image: "https://aloalobahia.com/images/p/petraorigem_alo_alo_bahia.jpg" },
{ id: 413, name: "DEVASSA 600", description: "", price: 10.00, category: "Cervejas 600ml", image: "https://ajufest.com.br/wp-content/uploads/2019/03/5b575eea775fa-5b5b31657d879-980x480.jpg" },
{ id: 249, name: "HEINEKEN L.N.", description: "", price: 10.99, category: "Cervejas L.N e Latas", image: "https://www.dg-media.com.br/cardapio/produto_359754.webp?v=1523439570" },
{ id: 10, name: "AGUA MINERAL", description: "", price: 3.00, category: "Refrigerantes e mais", image: "https://www.delgo.com.br/imagens/como-e-feito-o-envase-de-agua-mineral.jpg" },
{ id: 69, name: "ITAIPAVA 600", description: "", price: 7.99, category: "Cervejas 600ml", image: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS4MLuwyrzr4LBHPIJqlPa8Omu-ruMhtO7wNg&s" },
{ id: 244, name: "BUDWEISER L.N.", description: "", price: 9.99, category: "Cervejas L.N e Latas", image: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTQyL1mv7qGeG4ngZVX5m9BGXY6XBPIoljdgw&s" },
{ id: 263, name: "STELLA ARTOIS L.N", description: "", price: 10.99, category: "Cervejas L.N e Latas", image: "https://http2.mlstatic.com/D_NQ_NP_653548-MLB47709787115_092021-O.webp" },
{ id: 75, name: "ITAIPAVA 100% MALTE", description: "", price: 8.50, category: "Cervejas 600ml", image: "https://cervejaitaipava.com.br/wp-content/uploads/2023/10/100_malte_600ml.png" },
{ id: 421, name: "SAO BRAZ", description: "", price: 9.90, category: "Vinhos", image: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSVcuLv4ORpSoaFBM9_Nf3RMZstCwyEz0Wy7A&s" },
{ id: 32, name: "51 OURO", description: "", price: 3.00, category: "Cachaças", image: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS3U1q3VZz0ErvTz1zmS8VHbYyFfX0YUZKz2Q&s" },
{ id: 440, name: "BOHEMIA 600", description: "", price: 10.99, category: "Cervejas 600ml", image: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTg-P1AeoxFJxN8tuxZfPU6nnIzXbMgGuRxvA&s" },
{ id: 270, name: "BATATA, QUEIJO, CALABRESA E BACON", description: "", price: 19.90, category: "Petiscos", image: "https://cdn.outback.com.br/wp-data/wp-content/uploads/2018/10/Outback-Fries_305x342.png" },
{ id: 6, name: "AGUA C/ GAS", description: "", price: 3.99, category: "Refrigerantes e mais", image: "https://www.institucional.europa.com.br/blog/wp-content/uploads/2020/07/IMG_1822.jpg" },
{ id: 8, name: "AGUA DE COCO", description: "", price: 5.99, category: "Refrigerantes e mais", image: "https://altoastral.joaobidu.com.br/wp-content/uploads/2023/09/beneficios-agua-coco.jpg" },
{ id: 14, name: "COCA 600", description: "", price: 7.99, category: "Refrigerantes e mais", image: "https://cdn.awsli.com.br/600x450/98/98381/produto/3118862/a5edfa27ee.jpg" },
{ id: 82, name: "LOKAL", description: "", price: 6.99, category: "Cervejas 600ml", image: "https://cdn.awsli.com.br/2500x2500/2650/2650877/produto/23671156412292e883c.jpg" },
{ id: 268, name: "BATATA FRITA", description: "", price: 17.90, category: "Petiscos", image: "https://www.jetferr.com.br/blog/wp-content/uploads/2023/10/batatas-fritas-1.jpg" },
{ id: 420, name: "PEPSI ZERO", description: "", price: 6.00, category: "Refrigerantes e mais", image: "https://gkpb.com.br/wp-content/uploads/2023/01/pepsi-zero-acucar-nova1.jpg" },
{ id: 70, name: "BLACK PRINCESS", description: "", price: 14.00, category: "Cervejas 600ml", image: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRWofcVL1GUAFQ_d-pzbX0nB7LGanbVxoNdhw&s" },
{ id: 16, name: "COCA-COLA LATA", description: "", price: 5.99, category: "Refrigerantes e mais", image: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTkTp67TtOQCqWfwxFZpfO7ADyTpMZrKGk46Q&s" },
{ id: 415, name: "COCA-COLA LATA (GD)", description: "", price: 6.00, category: "Refrigerantes e mais", image: "https://thumbs.dreamstime.com/b/um-copo-e-uma-lata-de-coca-cola-156479264.jpg" },
]

113
src/index.css Normal file
View File

@@ -0,0 +1,113 @@
:root {
/* Paleta original */
--primary: #49291c;
--primary-hover: #3a1f14;
--secondary: #efc7b8;
--accent: #5b3b30;
--bg-dark: #514e4e;
--sidebar-bg: #2a2a2a;
--text-light: #f5f0ee;
/* Extras modernos */
--card-bg: rgba(255, 255, 255, 0.08);
--card-border: rgba(239, 199, 184, 0.15);
--glass-bg: rgba(42, 42, 42, 0.75);
--radius-md: 14px;
--radius-lg: 20px;
--shadow-sm: 0 2px 10px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 20px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5);
--transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
--header-height: 64px;
--sidebar-width: 260px;
--bottom-nav-height: 64px;
}
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: smooth;
font-size: 16px;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-dark);
color: var(--text-light);
min-height: 100vh;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
a {
text-decoration: none;
color: inherit;
}
button {
cursor: pointer;
font-family: inherit;
border: none;
background: none;
}
img {
max-width: 100%;
}
/* Scrollbar */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--sidebar-bg); }
::-webkit-scrollbar-thumb { background: var(--accent); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--primary); }
/* Animations */
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(24px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fallIn {
from { opacity: 0; transform: translateY(-40px) scale(0.9); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* Skeleton Loading */
.skeleton {
background: linear-gradient(90deg, var(--accent) 25%, var(--bg-dark) 50%, var(--accent) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: var(--radius-md);
}
/* Swal Overrides */
.swal2-popup {
background: var(--sidebar-bg) !important;
color: var(--secondary) !important;
border-radius: var(--radius-lg) !important;
border: 1px solid var(--card-border) !important;
}
.swal2-title { color: var(--text-light) !important; }
.swal2-close { color: var(--secondary) !important; }
.swal2-image {
border-radius: var(--radius-md) !important;
max-height: 220px !important;
object-fit: cover !important;
}
/* Utility */
.fade-in-up { animation: fadeInUp 0.45s ease both; }

10
src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

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;
}

View File

@@ -0,0 +1,40 @@
const API_URL =
'https://script.google.com/macros/s/AKfycbyuVF4ShwcVrEtkMXAumI8wx4ou9XsM43hIdcMhGrpmy24noQ0faO5eRdaQEpFat3ST/exec'
export async function fetchKaraokeData() {
const response = await fetch(API_URL)
if (!response.ok) {
throw new Error(`Erro na rede: ${response.status} ${response.statusText}`)
}
return response.json()
}
export function calculateAllEstimates(data) {
const durationPerPersonMs = (5 * 60 + 33) * 1000
let estimatedTime = new Date()
return data.map((item, index) => {
if (index > 0) {
estimatedTime = new Date(estimatedTime.getTime() + durationPerPersonMs)
}
const peopleBeforeInFullList = index
let peopleRemainingText = ''
if (peopleBeforeInFullList === 0) {
peopleRemainingText = 'Próximo a cantar'
} else if (peopleBeforeInFullList === 1) {
peopleRemainingText = 'Falta 1 pessoa para cantar'
} else {
peopleRemainingText = `Faltam ${peopleBeforeInFullList} pessoas para cantar`
}
const hours = String(estimatedTime.getHours()).padStart(2, '0')
const minutes = String(estimatedTime.getMinutes()).padStart(2, '0')
return {
...item,
estimatedTime: `${hours}:${minutes}`,
peopleRemaining: peopleRemainingText,
}
})
}

8
vite.config.js Normal file
View File

@@ -0,0 +1,8 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
base: '/rrbec/',
})