commit e48b4809c046a1cfc21e20cbeea498c3b556301a
Author: Welton Moura <98863639+welton89@users.noreply.github.com>
Date: Fri Feb 27 13:19:26 2026 -0300
first upload
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e69de29
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..4fa125d
--- /dev/null
+++ b/eslint.config.js
@@ -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_]' }],
+ },
+ },
+])
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..efde272
--- /dev/null
+++ b/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+ {/* ── Header ─────────────────────────────── */}
+
+
+
+
setSidebarOpen(v => !v)}
+ aria-label="Abrir menu"
+ >
+
+
+
navigate('/')}
+ />
+
{title}
+
+
+
+
+ {/* ── Sidebar ─────────────────────────────── */}
+ <>
+
setSidebarOpen(false)}
+ />
+
+ >
+
+ {/* ── Conteúdo principal ──────────────────── */}
+
+
+
+
+ {/* ── Bottom Nav (mobile) ─────────────────── */}
+
+ {navItems.map(item => (
+
+ `${styles.bottomNavItem} ${isActive ? styles.bottomNavActive : ''}`
+ }
+ >
+
+ {item.label}
+
+ ))}
+
+
+ )
+}
diff --git a/src/components/Layout.module.css b/src/components/Layout.module.css
new file mode 100644
index 0000000..6ce6246
--- /dev/null
+++ b/src/components/Layout.module.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/data/menuData.js b/src/data/menuData.js
new file mode 100644
index 0000000..f534102
--- /dev/null
+++ b/src/data/menuData.js
@@ -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" },
+]
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..863b7ca
--- /dev/null
+++ b/src/index.css
@@ -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; }
diff --git a/src/main.jsx b/src/main.jsx
new file mode 100644
index 0000000..54b39dd
--- /dev/null
+++ b/src/main.jsx
@@ -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(
+
+
+ ,
+)
diff --git a/src/pages/BioPage.jsx b/src/pages/BioPage.jsx
new file mode 100644
index 0000000..290027c
--- /dev/null
+++ b/src/pages/BioPage.jsx
@@ -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 (
+
+ {/* Hero com imagem de fundo */}
+
+
+
Raul Rock Bar& Café
+
+ {/* Logo flutuante */}
+
+
+
+
+
+ {/* Conteúdo abaixo do hero */}
+
+ {/* Ícones sociais */}
+
+ {socialLinks.map(link => (
+
+
+
+ ))}
+
+
+ {/* Descrição */}
+
+ O melhor bar rock da cidade.
+ Cerveja gelada, boa música e muito Rock! 🤘
+
+
+ {/* Botões de ação */}
+
+ {actionButtons.map(btn => (
+ navigate(btn.path)}
+ className={styles.actionBtn}
+ >
+
+ {btn.label}
+
+
+ ))}
+
+
+
+ )
+}
diff --git a/src/pages/BioPage.module.css b/src/pages/BioPage.module.css
new file mode 100644
index 0000000..91e9558
--- /dev/null
+++ b/src/pages/BioPage.module.css
@@ -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);
+}
\ No newline at end of file
diff --git a/src/pages/CardapioPage.jsx b/src/pages/CardapioPage.jsx
new file mode 100644
index 0000000..6d4ea75
--- /dev/null
+++ b/src/pages/CardapioPage.jsx
@@ -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: `
${item.description}
`,
+ imageUrl: item.image,
+ showCloseButton: true,
+ showConfirmButton: false,
+ background: '#2a2a2a',
+ color: '#efc7b8',
+ imageAlt: item.name,
+ })
+ }, [])
+
+ return (
+
+ {/* Filtro de categorias horizontal (pills) */}
+
+ setSelectedCategory(null)}
+ className={`${styles.pill} ${selectedCategory === null ? styles.pillActive : ''}`}
+ >
+ Todas
+
+ {categories.map(cat => (
+ setSelectedCategory(cat)}
+ className={`${styles.pill} ${selectedCategory === cat ? styles.pillActive : ''}`}
+ >
+ {cat}
+
+ ))}
+
+
+ {/* Campo de busca */}
+
+
+ {
+ setSearch(e.target.value)
+ setSelectedCategory(null)
+ }}
+ className={styles.searchInput}
+ />
+ {search && (
+ setSearch('')} className={styles.clearBtn}>
+
+
+ )}
+
+
+ {/* Resultados */}
+
+ {Object.keys(grouped).length === 0 && (
+
Nenhum produto encontrado.
+ )}
+
+ {Object.entries(grouped).map(([cat, items]) => (
+
+ {cat}
+
+ {items.map(item => (
+
openDetails(item)}
+ >
+
+
+
+
+
{item.name}
+ {item.description && (
+
{item.description}
+ )}
+
+
+ R$ {item.price.toFixed(2).replace('.', ',')}
+
+
+
+
+ ))}
+
+
+ ))}
+
+
+ )
+}
diff --git a/src/pages/CardapioPage.module.css b/src/pages/CardapioPage.module.css
new file mode 100644
index 0000000..1cf52ef
--- /dev/null
+++ b/src/pages/CardapioPage.module.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/DedoNoCopo.jsx b/src/pages/DedoNoCopo.jsx
new file mode 100644
index 0000000..1a26ec6
--- /dev/null
+++ b/src/pages/DedoNoCopo.jsx
@@ -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 (
+
+
+
Voltar
+
+
+
+
Roleta de Toque
+
+
+
{
+ 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 => (
+
+ {(status === 'counting' || (status === 'result' && t.id === winnerId)) && (
+
+ )}
+
+ ))}
+
+ {status === 'waiting' && (
+
+ {touches.length < 2 ? (
+ touches.length === 0 ? "COLOQUEM OS DEDOS NA TELA" : "FALTA MAIS ALGUÉM..."
+ ) : (
+
+ SINCRONIZANDO... {autoStartTimer}s
+
+ )}
+
+ )}
+
+ {status === 'counting' && (
+
+ NÃO TIREM OS DEDOS!
+
+ )}
+
+ {status === 'result' && (
+
+
ELEITO! 🍺
+
Quem parou no círculo dourado, bebe!
+
JOGAR NOVAMENTE
+
+ )}
+
+
+ )
+}
diff --git a/src/pages/DedoNoCopo.module.css b/src/pages/DedoNoCopo.module.css
new file mode 100644
index 0000000..f514858
--- /dev/null
+++ b/src/pages/DedoNoCopo.module.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/EuNunca.jsx b/src/pages/EuNunca.jsx
new file mode 100644
index 0000000..6de5f5d
--- /dev/null
+++ b/src/pages/EuNunca.jsx
@@ -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 (
+
+
+
Voltar para Jogos
+
+
+
Eu Nunca
+
+
+ category !== 'light' && getNewPhrase('light')}
+ >
+ Light
+
+ category !== 'hard' && getNewPhrase('hard')}
+ >
+ Hard
+
+
+
+
+
getNewPhrase()}>
+
+
+
+
+ {currentPhrase}
+
+
PRÓXIMA
+
+
+
+
+ Como Jogar: Leia a frase em voz alta.
+ Todos que JÁ FIZERAM o que diz a frase, devem beber um gole! 🥃
+
+
+
+ Respeite seus limites. Jogo destinado a maiores de 18 anos.
+
+
+ )
+}
diff --git a/src/pages/EuNunca.module.css b/src/pages/EuNunca.module.css
new file mode 100644
index 0000000..99e3245
--- /dev/null
+++ b/src/pages/EuNunca.module.css
@@ -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;
+}
\ No newline at end of file
diff --git a/src/pages/JogosPage.jsx b/src/pages/JogosPage.jsx
new file mode 100644
index 0000000..ef6eaae
--- /dev/null
+++ b/src/pages/JogosPage.jsx
@@ -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: `
+
Objetivo: Ficar bêbado 🥴 e pagar mico 🤣.
+
Como Jogar:
+ 1 - Reúna os jogadores em volta da mesa.
+ 2 - Cada jogador sorteia apenas uma tarefa por rodada.
+ 3 - O jogo segue em sentido horário.
+ 4 - O jogador que iniciar o jogo clica em SORTEAR.
+ 5 - O jogador deverá cumprir a tarefa que aparecer.
+ 6 - Se não cumprir, os outros jogadores escolhem um prenda a ser paga.
+ 7 - Se a prenda não for paga, o jogador será eliminado.
+
Liberdade Total: Sintam-se à vontade para mudar as regras e usar a criatividade!
+ 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: `
+
Como Jogar:
+ 1 - Um jogador lê a frase da carta.
+ 2 - Todos os jogadores que
JÁ FIZERAM o que está escrito na carta devem beber um gole.
+ 3 - Quem nunca fez, não bebe.
+ 4 - Clique em PRÓXIMA para mudar a frase.
+ 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: `
+
Como Jogar:
+ 1 - O grupo lê a pergunta da tela.
+ 2 - Alguém conta "3... 2... 1...".
+ 3 - No "JÁ!", todos devem apontar para a pessoa que acham que mais provavelmente faria aquilo.
+ 4 - A pessoa mais votada bebe um gole (ou paga uma prenda)!
+ 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: `
+
Como Jogar:
+ 1 - Todos os jogadores colocam um dedo na tela.
+ 2 - Quando houver 2 ou mais dedos, uma contagem de
5 segundos iniciará automaticamente.
+ 3 - Se um novo amigo entrar na roda durante a contagem, o tempo reinicia para dar chance a todos!
+ 4 - Após os 5 segundos, o anel dourado começará a carregar. Não tire o dedo!
+ 5 - No final, apenas um círculo ficará iluminado. O "Eleito" paga a rodada! 🍺
+
Dica: Se alguém tirar o dedo antes da roleta terminar, o jogo volta para o início.
+ `,
+ },
+]
+
+export default function JogosPage() {
+ function showRules(jogo) {
+ Swal.fire({
+ title: 'Regras do Jogo',
+ html: `
${jogo.rules}
`,
+ showCloseButton: true,
+ showConfirmButton: false,
+ background: '#2a2a2a',
+ color: '#efc7b8',
+ })
+ }
+
+ return (
+
+
+ Jogos de Bar
+
+
+
+ {jogos.map(jogo => (
+
+
+
+
+
+
+
+
{jogo.title}
+
{jogo.description}
+
+
+
showRules(jogo)}
+ className={styles.rulesBtn}
+ >
+ Regras
+
+ {jogo.internal ? (
+
+
Jogar
+
+ ) : (
+
+ Jogar
+
+ )}
+
+
+
+ ))}
+
+
+ )
+}
diff --git a/src/pages/JogosPage.module.css b/src/pages/JogosPage.module.css
new file mode 100644
index 0000000..e3250b3
--- /dev/null
+++ b/src/pages/JogosPage.module.css
@@ -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);
+}
\ No newline at end of file
diff --git a/src/pages/KaraokePage.jsx b/src/pages/KaraokePage.jsx
new file mode 100644
index 0000000..daf096a
--- /dev/null
+++ b/src/pages/KaraokePage.jsx
@@ -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 (
+
+
+ {/* Header da fila */}
+
+
+ Fila do Karaokê
+
+
+
+
+
+
+ {/* Campo de busca */}
+
+
+ setSearch(e.target.value)}
+ className={styles.searchInput}
+ />
+
+
+ {/* Lista */}
+
+ {status === 'loading' && (
+
+ {[1, 2, 3].map(i => (
+
+ ))}
+
+ )}
+
+ {status === 'error' && (
+
+
+ Erro ao carregar a fila. Tente novamente.
+
+ )}
+
+ {status === 'ok' && filtered.length === 0 && (
+
+ {search ? 'Nenhum resultado encontrado.' : 'A fila está vazia no momento.'}
+
+ )}
+
+ {status === 'ok' && filtered.map((item, index) => (
+
+
+ {index === 0 ? (
+
+
+
+ ) : (
+ {index + 1}
+ )}
+
+
+
+
+ {item.nome}
+ – {item.musica}
+ {(String(item.primeiraVez).toUpperCase() === 'TRUE' || String(item.primeiraVez) === '1') && (
+ 1ª vez! 🎉
+ )}
+
+
+ {item.peopleRemaining}
+ · Estimado: {item.estimatedTime}
+
+
+
+ ))}
+
+
+
+ )
+}
diff --git a/src/pages/KaraokePage.module.css b/src/pages/KaraokePage.module.css
new file mode 100644
index 0000000..2a21dfa
--- /dev/null
+++ b/src/pages/KaraokePage.module.css
@@ -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);
+}
\ No newline at end of file
diff --git a/src/pages/QuemEMaisProvavel.jsx b/src/pages/QuemEMaisProvavel.jsx
new file mode 100644
index 0000000..35da399
--- /dev/null
+++ b/src/pages/QuemEMaisProvavel.jsx
@@ -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 (
+
+
+
Voltar para Jogos
+
+
+
Quem é mais provável?
+
+
+
+
+
+
+
+ {currentQuestion}
+
+
+
+ PRÓXIMA PERGUNTA
+
+
+
+
+ Como Jogar:
+ No 3... 2... 1... todos devem APONTAR para a pessoa que acham mais provável de fazer o que diz a frase!
+
+
+
+ Apenas para diversão. Brinquem com respeito!
+
+
+ )
+}
diff --git a/src/pages/QuemEMaisProvavel.module.css b/src/pages/QuemEMaisProvavel.module.css
new file mode 100644
index 0000000..0e1a825
--- /dev/null
+++ b/src/pages/QuemEMaisProvavel.module.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/TarefaDeBebado.jsx b/src/pages/TarefaDeBebado.jsx
new file mode 100644
index 0000000..8367d29
--- /dev/null
+++ b/src/pages/TarefaDeBebado.jsx
@@ -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 (
+
+
+
Voltar para Jogos
+
+
+
Tarefa de Bêbado
+
+
+
+
+
+ Girar
+
+
+
+
`${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 (
+
+ )
+ })}
+
+
+
+
+ {resultado ? (
+ <>
+
Opção {resultado.id}:
+
{resultado.text}
+ >
+ ) : (
+
+ {isSpinning ? 'Sorteando...' : 'Gire a roleta!'}
+
+ )}
+
+
+
+
Legenda das Tarefas
+
+ {TAREFAS.map(tarefa => (
+
+
+ {tarefa.id}
+
+ {tarefa.text}
+
+ ))}
+
+
+
+
+ Respeite as leis e seus limites. Beber e dirigir é crime. Se beber, não dirija.
+
+
+ )
+}
diff --git a/src/pages/TarefaDeBebado.module.css b/src/pages/TarefaDeBebado.module.css
new file mode 100644
index 0000000..5315825
--- /dev/null
+++ b/src/pages/TarefaDeBebado.module.css
@@ -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;
+}
\ No newline at end of file
diff --git a/src/services/karaokeService.js b/src/services/karaokeService.js
new file mode 100644
index 0000000..157cb39
--- /dev/null
+++ b/src/services/karaokeService.js
@@ -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,
+ }
+ })
+}
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..fbb0471
--- /dev/null
+++ b/vite.config.js
@@ -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/',
+})