├── src ├── components │ ├── add-card-link │ │ ├── styles.module.scss │ │ └── index.tsx │ ├── GoBack │ │ ├── styles.module.scss │ │ └── index.tsx │ ├── Toast │ │ └── index.tsx │ ├── SomeLoading │ │ ├── styles.module.scss │ │ └── index.tsx │ ├── Avatar │ │ └── index.tsx │ ├── Maintenance │ │ ├── styles.module.scss │ │ └── index.tsx │ ├── pix-qrcode.tsx │ ├── show-cards-owner-button.tsx │ ├── login-button.tsx │ ├── form │ │ └── textarea.tsx │ ├── PrivateRoute │ │ └── index.tsx │ ├── cookies-modal │ │ └── index.tsx │ ├── warn-modal │ │ └── index.tsx │ ├── finish-match-button.tsx │ ├── add-card-modal │ │ └── index.tsx │ └── Card │ │ ├── index.tsx │ │ └── styles.module.scss ├── vite-env.d.ts ├── favicon.png ├── assets │ ├── avatar.png │ └── qrcode.png ├── utils │ ├── count-string.ts │ ├── title-string.ts │ ├── get-random-item.ts │ ├── get-first-string.ts │ ├── get-user-name.ts │ ├── get-error-message.ts │ ├── check-morning-time.ts │ └── fireworks.ts ├── constants │ ├── globals.ts │ └── matches.ts ├── lib │ ├── ServerMaintanceError.ts │ └── Repository.ts ├── styles │ ├── components │ │ ├── input.ts │ │ └── button.ts │ ├── tokens.ts │ └── theme.ts ├── hooks │ ├── useBoolean.ts │ ├── useIsAdmin.ts │ ├── useStorage.ts │ └── useProcessing.ts ├── main.tsx ├── pages │ ├── Home │ │ ├── tags.tsx │ │ ├── contribution.tsx │ │ ├── index.tsx │ │ ├── WarnAlert.tsx │ │ ├── join-match.tsx │ │ ├── header.tsx │ │ └── actions.tsx │ ├── Match │ │ ├── useDisableMatch.tsx │ │ ├── styles.module.scss │ │ ├── match-link-input.tsx │ │ ├── useFetchDeck.ts │ │ ├── UsersList.tsx │ │ ├── CardsToPlay.tsx │ │ ├── useSetupMatch.ts │ │ ├── CardsPlayedList.tsx │ │ ├── index.tsx │ │ └── SettingsDrawer.tsx │ ├── Matches │ │ ├── index.tsx │ │ ├── styles.module.scss │ │ ├── useLastMatches.ts │ │ └── LastMatches.tsx │ ├── Cards │ │ ├── useFetchCards.ts │ │ └── index.tsx │ └── NewCard │ │ └── index.tsx ├── services │ ├── general.ts │ ├── users.ts │ ├── core.ts │ ├── cards.ts │ ├── types.d.ts │ └── matches.ts ├── types │ └── ads.d.ts ├── favicon.svg ├── App.tsx ├── firebase │ └── config.ts └── contexts │ └── AuthContext.tsx ├── .eslintignore ├── public ├── ads.txt ├── cover.png ├── img │ └── qrcode.png ├── css │ ├── reset.css │ └── shared.css ├── atualizacoes.html ├── doacoes.html ├── tutorial.html ├── sobre.html └── faq.html ├── vercel.json ├── nixpacks.toml ├── .prettierrc ├── tsconfig.node.json ├── .editorconfig ├── .gitignore ├── vite.config.ts ├── .github └── workflows │ └── clear-matches.yml ├── tsconfig.json ├── scripts └── finish-matches.js ├── .eslintrc.json ├── package.json ├── index.html └── README.md /src/components/add-card-link/styles.module.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build 3 | vite.config.ts 4 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-8489649559992587, DIRECT, f08c47fec0942fa0 2 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/(.*)", "destination": "/" }] 3 | } 4 | -------------------------------------------------------------------------------- /nixpacks.toml: -------------------------------------------------------------------------------- 1 | providers = ["staticfile"] 2 | 3 | [phases.setup] 4 | nixPkgs = ['nginx'] 5 | -------------------------------------------------------------------------------- /public/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilvanoGPM/cards-against-humanity/HEAD/public/cover.png -------------------------------------------------------------------------------- /src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilvanoGPM/cards-against-humanity/HEAD/src/favicon.png -------------------------------------------------------------------------------- /public/img/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilvanoGPM/cards-against-humanity/HEAD/public/img/qrcode.png -------------------------------------------------------------------------------- /src/assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilvanoGPM/cards-against-humanity/HEAD/src/assets/avatar.png -------------------------------------------------------------------------------- /src/assets/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SilvanoGPM/cards-against-humanity/HEAD/src/assets/qrcode.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/count-string.ts: -------------------------------------------------------------------------------- 1 | export function countString(word: string, token: string): number { 2 | return word.split(token).length - 1; 3 | } 4 | -------------------------------------------------------------------------------- /src/constants/globals.ts: -------------------------------------------------------------------------------- 1 | export const CARD_TOKEN = '[?]'; 2 | 3 | export const IS_MAINTANCE = false; 4 | 5 | export const MAX_OF_CARDS_IN_DECK = 4; 6 | -------------------------------------------------------------------------------- /src/utils/title-string.ts: -------------------------------------------------------------------------------- 1 | export function titleString(string: string) { 2 | return string[0].toUpperCase() + string.slice(1).toLowerCase(); 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/get-random-item.ts: -------------------------------------------------------------------------------- 1 | export function getRandomItem(arr: T[]): T { 2 | const randomIndex = Math.floor(Math.random() * arr.length); 3 | return arr[randomIndex]; 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/get-first-string.ts: -------------------------------------------------------------------------------- 1 | export function getFirstString( 2 | str: string | undefined | null, 3 | delimiter = ' ' 4 | ): string { 5 | return str?.split(delimiter)[0] || ''; 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/ServerMaintanceError.ts: -------------------------------------------------------------------------------- 1 | export class ServerMaintanceError extends Error { 2 | constructor() { 3 | super('Estamos com nosso servidor em manutenção, volte mais tarde.'); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/components/input.ts: -------------------------------------------------------------------------------- 1 | import { ComponentStyleConfig } from '@chakra-ui/react'; 2 | 3 | export const Input: ComponentStyleConfig = { 4 | baseStyle: { 5 | borderColor: 'black !important', 6 | bg: 'red', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /src/components/GoBack/styles.module.scss: -------------------------------------------------------------------------------- 1 | .goBack { 2 | cursor: pointer; 3 | border-radius: 50%; 4 | border: 1px solid; 5 | padding: 0.5rem; 6 | 7 | &Button { 8 | border: none; 9 | background-color: transparent; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Toast/index.tsx: -------------------------------------------------------------------------------- 1 | import { Position, Toaster } from '@blueprintjs/core'; 2 | 3 | export const AppToaster = Toaster.create({ 4 | position: Position.TOP, 5 | }); 6 | 7 | export const BottomToaster = Toaster.create({ 8 | position: Position.BOTTOM, 9 | }); 10 | -------------------------------------------------------------------------------- /src/styles/tokens.ts: -------------------------------------------------------------------------------- 1 | export const thinScrollbar = { 2 | '&': { scrollbarWidth: 'thin' }, 3 | '&::-webkit-scrollbar ': { width: '9px' }, 4 | '&::-webkit-scrollbar-track': { bg: 'transparent' }, 5 | '&::-webkit-scrollbar-thumb': { 6 | bg: 'black', 7 | border: 'transparent', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/utils/get-user-name.ts: -------------------------------------------------------------------------------- 1 | import { User } from 'firebase/auth'; 2 | import { getFirstString } from './get-first-string'; 3 | import { titleString } from './title-string'; 4 | 5 | export function getUserName(user: User | UserType) { 6 | return titleString( 7 | getFirstString(user.displayName) || 8 | getFirstString(user.email, '@') || 9 | 'User' 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/get-error-message.ts: -------------------------------------------------------------------------------- 1 | export function getErrorMessage( 2 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 3 | error: any, 4 | defaultMessage = 'Aconteceu um erro' 5 | ) { 6 | if (error.code === 'resource-exhausted') { 7 | return 'Limite diário do servidor foi atingido, volte amanhã a partir das 9 horas.'; 8 | } 9 | 10 | return error?.message || defaultMessage; 11 | } 12 | -------------------------------------------------------------------------------- /public/css/reset.css: -------------------------------------------------------------------------------- 1 | /*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */html,body,p,ol,ul,li,dl,dt,dd,blockquote,figure,fieldset,legend,textarea,pre,iframe,hr,h1,h2,h3,h4,h5,h6{margin:0;padding:0}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal}ul{list-style:none}button,input,select{margin:0}html{box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}img,video{height:auto;max-width:100%}iframe{border:0}table{border-collapse:collapse;border-spacing:0}td,th{padding:0} 2 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { defineConfig } from 'vite'; 3 | 4 | const path = require('path'); 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | resolve: { 9 | alias: [ 10 | { find: '@', replacement: path.resolve(__dirname, 'src') }, 11 | { 12 | // this is required for the SCSS modules 13 | find: /^~(.*)$/, 14 | replacement: '$1', 15 | }, 16 | ], 17 | }, 18 | plugins: [react()], 19 | }); 20 | -------------------------------------------------------------------------------- /src/hooks/useBoolean.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | 3 | type UseBooleanReturn = [ 4 | boolean, 5 | () => void, 6 | () => void, 7 | (value: boolean) => void 8 | ]; 9 | 10 | export function useBoolean(initalValue: boolean): UseBooleanReturn { 11 | const [value, setValue] = useState(initalValue); 12 | 13 | const setTrueValue = useCallback(() => { 14 | setValue(true); 15 | }, []); 16 | 17 | const setFalseValue = useCallback(() => { 18 | setValue(false); 19 | }, []); 20 | 21 | return [value, setTrueValue, setFalseValue, setValue]; 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/Repository.ts: -------------------------------------------------------------------------------- 1 | export default class Repository { 2 | static save(key: string, value: Record | T): void { 3 | const valueString = JSON.stringify(value); 4 | 5 | try { 6 | localStorage.setItem(key, valueString); 7 | } catch (err) { 8 | console.error(err); 9 | } 10 | } 11 | 12 | static get(key: string): T | null | undefined { 13 | try { 14 | const value = localStorage.getItem(key); 15 | return value ? (JSON.parse(value) as T) : null; 16 | } catch (err) { 17 | console.error(err); 18 | } 19 | 20 | return null; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/useIsAdmin.ts: -------------------------------------------------------------------------------- 1 | import { isAdmin } from '@/services/users'; 2 | import { User } from 'firebase/auth'; 3 | import { useEffect, useState } from 'react'; 4 | 5 | export function useIsAdmin(user: User, shouldFetch: boolean) { 6 | const [userIsAdmin, setUserIsAdmin] = useState(false); 7 | 8 | useEffect(() => { 9 | async function verifyIsAdmin(): Promise { 10 | const userIsAdmin = await isAdmin(user.uid); 11 | 12 | setUserIsAdmin(userIsAdmin); 13 | } 14 | 15 | if (user?.uid && shouldFetch) { 16 | verifyIsAdmin(); 17 | } 18 | }, [user, shouldFetch]); 19 | 20 | return { userIsAdmin, setUserIsAdmin }; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/GoBack/index.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | 3 | import { Icon, IconButton, IconButtonProps } from '@chakra-ui/react'; 4 | 5 | import { FaArrowLeft } from 'react-icons/fa'; 6 | 7 | interface GoBackProps extends Omit { 8 | toHome?: boolean; 9 | } 10 | 11 | export function GoBack({ toHome = false, ...props }: GoBackProps): JSX.Element { 12 | const navigate = useNavigate(); 13 | 14 | return ( 15 | (toHome ? navigate('/') : navigate(-1))} 18 | icon={} 19 | {...props} 20 | /> 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/clear-matches.yml: -------------------------------------------------------------------------------- 1 | name: Finalizar partidas todos os dias 2 | 3 | on: 4 | schedule: 5 | - cron: '0 11 * * *' # 11 da manhã UTC (Github) / 9 da manhã Brasil (Eu) / 4 horas Pacific Time (Firebase) 6 | 7 | jobs: 8 | executar-script: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout do repositório 13 | uses: actions/checkout@v2 14 | 15 | - name: Configurar Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: '18' 19 | 20 | - name: Instalar dependências 21 | run: npm install 22 | 23 | - name: Executar Script 24 | run: node ./scripts/finish-matches.js 25 | -------------------------------------------------------------------------------- /src/utils/check-morning-time.ts: -------------------------------------------------------------------------------- 1 | export function checkMorningTime() { 2 | const now = new Date(); 3 | const morningStart = new Date(); 4 | const morningEnd = new Date(); 5 | 6 | morningStart.setHours(8, 0, 0, 0); 7 | morningEnd.setHours(20, 0, 0, 0); 8 | 9 | const randomDays: number[] = []; 10 | 11 | while (randomDays.length < 3) { 12 | const randomDay = Math.floor(Math.random() * 7); // Números de 0 a 6 (domingo a sábado) 13 | 14 | if (!randomDays.includes(randomDay)) { 15 | randomDays.push(randomDay); 16 | } 17 | } 18 | 19 | return ( 20 | randomDays.includes(now.getDay()) && 21 | now >= morningStart && 22 | now <= morningEnd 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { ChakraProvider } from '@chakra-ui/react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | 5 | import { App } from './App'; 6 | import { theme } from './styles/theme'; 7 | 8 | const root = createRoot(document.getElementById('root') as HTMLElement); 9 | 10 | root.render( 11 | 12 | 22 | 23 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /src/components/SomeLoading/styles.module.scss: -------------------------------------------------------------------------------- 1 | @import "~@blueprintjs/core/lib/scss/variables"; 2 | 3 | .container { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: center; 8 | text-align: center; 9 | position: absolute; 10 | top: 0; 11 | bottom: 0; 12 | left: 0; 13 | right: 0; 14 | z-index: 9999; 15 | background-color: $white; 16 | opacity: 0; 17 | height: 0; 18 | transition: 0.2s ease-in-out; 19 | 20 | &.show { 21 | height: 100vh; 22 | opacity: 1; 23 | } 24 | } 25 | 26 | .loading { 27 | margin-bottom: 1rem; 28 | animation: spin 1s infinite linear; 29 | } 30 | 31 | @keyframes spin { 32 | to { 33 | transform: rotate(360DEG); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/Home/tags.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Tag } from '@chakra-ui/react'; 2 | import { version } from '../../../package.json'; 3 | 4 | export function Tags() { 5 | return ( 6 | 14 |