diff --git a/.env b/.env index 946946c..619fce7 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -VITE_SUPABASE_URL=https://*.supabase.co -VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.* +VITE_SUPABASE_URL=https://.supabase.co +VITE_SUPABASE_ANON_KEY= VITE_DEFAULT_API_URL=http://localhost:5174 diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..e2d19f7 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,18 @@ +on: + push: + branches: + - "main" + +permissions: + contents: read + pages: write + id-token: write + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - id: build-publish + uses: bitovi/github-actions-react-to-github-pages@v1.2.2 + with: + path: dist diff --git a/index.html b/index.html index e54cd2e..7e8a743 100644 --- a/index.html +++ b/index.html @@ -5,11 +5,11 @@ PHPinga - - + + + + + diff --git a/package.json b/package.json index f7de3fa..2939fda 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "version": "0.0.0", "type": "module", + "homepage": "https://phpcomrapadura.github.io/quiz", "scripts": { "dev": "vite", "build": "tsc && vite build", diff --git a/src/app/Domain/Game/Answer.ts b/src/app/Domain/Game/Answer.ts index 5c29902..f4b7c51 100644 --- a/src/app/Domain/Game/Answer.ts +++ b/src/app/Domain/Game/Answer.ts @@ -3,5 +3,4 @@ export default interface Answer { text: string correct: boolean createdAt: Date - updatedAt: Date } diff --git a/src/app/Infrastructure/InMemory/games.ts b/src/app/Infrastructure/InMemory/games.ts index 60b94c6..ee6b721 100644 --- a/src/app/Infrastructure/InMemory/games.ts +++ b/src/app/Infrastructure/InMemory/games.ts @@ -18,31 +18,26 @@ export default function (): Game[] { { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`int`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`is_interger`' }, { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: '`is_int`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`isInt`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`isInteger`' } ] @@ -56,25 +51,21 @@ export default function (): Game[] { { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: '`__construct()`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`contruct__()`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`__constructor()`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`construtor__`' } ] @@ -88,25 +79,21 @@ export default function (): Game[] { { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`mysql_free_result()`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`mysql_stmt_start()`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`mysql_fetch_array()`' }, { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: '`mysql_query()`' } ] @@ -120,25 +107,21 @@ export default function (): Game[] { { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`strcmp()`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`str_cmp()`' }, { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: '`strcasecmp()`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`stricmp()`' } ] @@ -152,26 +135,22 @@ export default function (): Game[] { { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'Não há diferença entre elas' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`str_ireplace` e `str_casereplace` não existem' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`str_ireplace` e `str_casereplace` são para substituir caracteres em strings com case insensitive' }, { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: '`str_ireplace` é para substituir caracteres em strings com case insensitive e `str_casereplace` não existe', } ] @@ -185,19 +164,16 @@ export default function (): Game[] { { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`false`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`null`' }, { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: '`true`' } ] @@ -211,31 +187,26 @@ export default function (): Game[] { { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'PHP' }, { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: 'Ele' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'ElePHPant' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'php' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'PHPAnt' } ] @@ -249,31 +220,26 @@ export default function (): Game[] { { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '5.3' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '5.4' }, { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: '5.5' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '5.6' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '7' } ] @@ -287,25 +253,21 @@ export default function (): Game[] { { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`__serialize()` e `__unserialize()`' }, { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: '`__sleep()` e `__wakeup()`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`__begin()` e `__end()`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`__construct()` e `__destruct()`' } ] @@ -319,25 +281,21 @@ export default function (): Game[] { { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: 'Impede que uma classe ou método seja sobrescrito' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'Encerra o script PHP' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'Indica o final do programa' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'Nada, pois keyword \'final\' não existe' } ] @@ -351,25 +309,21 @@ export default function (): Game[] { { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'Não pode' }, { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: 'Com PDO' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'Com ADODB' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'Com PHP Global Databases' } ] @@ -383,25 +337,21 @@ export default function (): Game[] { { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'Dá erro: String to Integer convertion error' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`\'Tem conteúdo\'`' }, { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: '`\'Vazio\'`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`\'0\'`' } ] @@ -415,25 +365,21 @@ export default function (): Game[] { { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'Dá erro: String to Integer convertion error' }, { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: '`\'Igual\'`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`\'Diferente\'`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`\'2015PHPinga\'`' } ] @@ -447,25 +393,21 @@ export default function (): Game[] { { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: 'Personal Home Page' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'Preprocessor for Huge Parallelization' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'Packaged Hints for Programming' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'Program for High Problems' } ] @@ -479,25 +421,21 @@ export default function (): Game[] { { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`(1 | 2) == 1`' }, { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: '`(1 ^ 3) == 2`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`(2 ^ 3) == 8`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`(2 & 3) == 6`' } ] @@ -511,25 +449,21 @@ export default function (): Game[] { { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`strtofirst()`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`firstcharupper()`' }, { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: '`ucwords()`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`firstcase()`' } ] @@ -543,25 +477,21 @@ export default function (): Game[] { { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`php --repl`' }, { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: '`php -a`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`php -i`' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: '`php -r`' } ] @@ -575,25 +505,21 @@ export default function (): Game[] { { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'Espaços após `?>` ocasionam erros de execução' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'Espaços após `?>` iniciam o envio de cabeçalhos da requisição' }, { correct: true, createdAt: new Date(), - updatedAt: new Date(), text: 'Espaços após `?>` iniciam o envio do corpo da requisição' }, { correct: false, createdAt: new Date(), - updatedAt: new Date(), text: 'Espaços após `?>` encerram o envio do corpo da requisição' } ] diff --git a/src/app/Infrastructure/Supabase/SupabaseGameRepository.ts b/src/app/Infrastructure/Supabase/SupabaseGameRepository.ts new file mode 100644 index 0000000..8ab40c6 --- /dev/null +++ b/src/app/Infrastructure/Supabase/SupabaseGameRepository.ts @@ -0,0 +1,59 @@ +import Game from '../../Domain/Game/Game.ts' +import GameRepository from '../../Domain/Game/GameRepository.ts' +import { SupabaseClient } from '@supabase/supabase-js' +import SupabaseClientFactory from './SupabaseClientFactory.ts' + +export default class SupabaseGameRepository implements GameRepository { + private supabase: SupabaseClient + + constructor () { + this.supabase = SupabaseClientFactory() + } + + async findById (id: number | string): Promise { + const { data, error } = await this.supabase + .from('games') + .select(`id, + description, + author, + created_at, + updated_at, + questions ( + id, + text, + created_at, + updated_at, + answers ( + id, + text, + correct, + created_at + ) + )` + ) + .eq('id', id) + .single() + if (!data) { + throw new Error(error?.message || 'Game not found') + } + return Promise.resolve({ + id: data.id, + description: data.description, + author: data.author, + createdAt: data.created_at, + updatedAt: data.updated_at, + questions: data.questions.map((question) => ({ + id: question.id, + text: question.text, + createdAt: question.created_at, + updatedAt: question.updated_at, + answers: question.answers.map((answer) => ({ + id: answer.id, + text: answer.text, + correct: answer.correct, + createdAt: answer.created_at + })) + })) + }) + } +} diff --git a/src/config/dependencies.ts b/src/config/dependencies.ts index b791f2b..69e462e 100644 --- a/src/config/dependencies.ts +++ b/src/config/dependencies.ts @@ -4,11 +4,16 @@ import { AuthService } from '../app/Application/AuthService.ts' // import SupabaseAuthRepository from '../app/Infrastructure/Supabase/SupabaseAuthRepository.ts' import JsonHttpAuthRepository from '../app/Infrastructure/Http/JsonHttpAuthRepository.ts' import InMemoryGameRepository from '../app/Infrastructure/InMemory/InMemoryGameRepository.ts' +import SupabaseGameRepository from '../app/Infrastructure/Supabase/SupabaseGameRepository.ts' export default function () { container.register('AuthService', { useClass: AuthService }) container.register('AuthRepository', { useClass: JsonHttpAuthRepository }) - container.register('GameRepository', { useClass: InMemoryGameRepository }) + if (process.env.NODE_ENV === 'test') { + container.register('GameRepository', { useClass: InMemoryGameRepository }) + return container + } + container.register('GameRepository', { useClass: SupabaseGameRepository }) return container } diff --git a/src/view/components/game/GameQuestion.tsx b/src/view/components/game/GameQuestion.tsx index 8090c30..25056d8 100644 --- a/src/view/components/game/GameQuestion.tsx +++ b/src/view/components/game/GameQuestion.tsx @@ -37,8 +37,8 @@ export function GameQuestion (props: GameQuestionProps) { const [options, setOptions] = useState([]) useEffect(() => { - if (options.length === 0) { - setOptions(shuffle(answers)) + if (options.length === 0 && answers.length > 0) { + setOptions(shuffle((answers))) } const tick = () => { @@ -72,7 +72,6 @@ export function GameQuestion (props: GameQuestionProps) { return (
- (null) - const [fetched, setFetched] = useState(false) + const [initialized, setInitialized] = useState(false) + const fetched = useRef(false) const gameRepository = container.resolve('GameRepository') const [questions, setQuestions] = useState([]) @@ -31,8 +32,9 @@ export function GamePage () { navigate(`/game/${gameId}/end`) return } - const current = Math.floor(Math.random() * questions.length) - setQuestion(questions[current]) + const randomIndex = Math.floor(Math.random() * questions.length) + const newQuestion = questions[randomIndex] + setQuestion(newQuestion) } const answerQuestion: GameQuestionAnswerQuestion = (status) => { @@ -42,7 +44,6 @@ export function GamePage () { console.log('Correct!') const current = questions.findIndex((q) => q === question) const newQuestions = questions.filter((_, index) => index !== current) - console.log('newQuestions', newQuestions) setQuestions(shuffle(newQuestions)) }, [AnswerStatus.WRONG]: () => console.log('Wrong!'), @@ -53,18 +54,27 @@ export function GamePage () { } useEffect(() => { - if (fetched) { + if (initialized) { return } const fetchGame = async () => { - const game = await gameRepository.findById(gameId) - setGame(game) - setFetched(true) - setQuestions(JSON.parse(JSON.stringify(game.questions))) + if (fetched.current) { + return + } + try { + fetched.current = true + const game = await gameRepository.findById(gameId) + setGame(game) + setInitialized(true) + setQuestions(JSON.parse(JSON.stringify(game.questions))) + } catch (e) { + console.error(e) + return navigate(`/game/${gameId}/not-found`) + } } fetchGame() - }, [fetched, gameId, gameRepository]) + }, [initialized, gameId, gameRepository]) return game ? ( @@ -91,7 +101,7 @@ export function GamePage () { : ( // TODO: add loading and error states - fetched ? + initialized ?
Error fetching game {gameId} ...
:
Fetching game {gameId} ...