From 07ddb0ca7bb598ed1780681a3d2f505d01e9956e Mon Sep 17 00:00:00 2001 From: William Correa Date: Thu, 11 Apr 2024 13:23:24 -0300 Subject: [PATCH 01/17] feature: change dir strucure --- {src/config => config}/assets.ts | 0 {src/config => config}/dependencies.ts | 12 +++++----- {src/config => config}/env.ts | 0 {src/config => config}/i18n.ts | 0 {src/database => database}/static/games.json | 0 src/index.css => index.css | 0 index.html | 2 +- src/main.tsx => main.tsx | 2 +- src/{app => }/Application/AuthService.ts | 0 src/{app => }/Domain/Auth/Auth.ts | 0 src/{app => }/Domain/Auth/AuthRepository.ts | 0 src/{app => }/Domain/Contracts.ts | 0 src/{app => }/Domain/Game/Answer.ts | 0 src/{app => }/Domain/Game/AnswerStatus.ts | 0 src/{app => }/Domain/Game/Game.ts | 0 src/{app => }/Domain/Game/GameRepository.ts | 0 src/{app => }/Domain/Game/Question.ts | 0 src/{app => }/Domain/Util.ts | 0 .../Backend/HttpAuthRepository.ts | 0 .../Infrastructure/Http/Contracts.ts | 0 .../Infrastructure/Http/HttpClient.ts | 0 .../Infrastructure/Http/HttpClientFactory.ts | 0 .../Infrastructure/Http/JsonHttpClient.ts | 0 .../InMemory/InMemoryAuthRepository.ts | 0 .../InMemory/InMemoryGameRepository.ts | 0 .../InMemory/InMemoryRepository.ts | 0 .../Infrastructure/InMemory/games.ts | 0 .../Supabase/Mapper/GameMapper.ts | 0 .../Supabase/SupabaseAuthRepository.ts | 0 .../Supabase/SupabaseClientFactory.ts | 0 .../Supabase/SupabaseGameRepository.ts | 0 tsconfig.json | 5 ++-- {src => view}/App.css | 0 {src => view}/App.tsx | 24 +++++++++---------- .../components/app/AppContext.tsx | 0 .../components/auth/ProtectPage.tsx | 0 .../components/game/GameImage.tsx | 0 .../components/game/GameList.tsx | 2 +- .../components/game/GamePlaySession.tsx | 8 +++---- .../game/game-list/index.module.css | 0 .../GamePlaySessionInstruction.tsx | 2 +- .../GamePlaySessionQuestion.tsx | 6 ++--- .../GamePlaySessionQuestionCorrect.tsx | 0 .../GamePlaySessionQuestionTimeExpired.tsx | 0 .../GamePlaySessionQuestionUnanswered.tsx | 2 +- .../GamePlaySessionQuestionWrong.tsx | 0 .../game-play-session-question/index.tsx | 0 .../components/general/Alert.tsx | 0 .../components/general/Async.tsx | 0 .../components/general/Loading.tsx | 0 .../components/general/Markdown.tsx | 0 .../components/general/Match.tsx | 0 {src/view => view}/contracts.ts | 2 +- {src => view}/decorators.ts | 0 {src/view => view}/hooks/useApp.ts | 0 {src/view => view}/hooks/useRunOnce.ts | 0 {src/view => view}/layouts/PublicLayout.tsx | 0 {src/view => view}/pages/DashboardPage.tsx | 0 {src/view => view}/pages/HomePage.tsx | 0 {src/view => view}/pages/auth/SignInPage.tsx | 0 .../pages/auth/WaitOneTimePassword.tsx | 0 {src/view => view}/pages/game/GameEndPage.tsx | 0 .../view => view}/pages/game/GamePlayPage.tsx | 4 ++-- .../pages/game/GameWelcomePage.tsx | 4 ++-- {src/view => view}/providers/AppProvider.tsx | 0 .../providers/auth-manager-factory.ts | 2 +- src/vite-env.d.ts => vite-env.d.ts | 0 67 files changed, 39 insertions(+), 38 deletions(-) rename {src/config => config}/assets.ts (100%) rename {src/config => config}/dependencies.ts (73%) rename {src/config => config}/env.ts (100%) rename {src/config => config}/i18n.ts (100%) rename {src/database => database}/static/games.json (100%) rename src/index.css => index.css (100%) rename src/main.tsx => main.tsx (92%) rename src/{app => }/Application/AuthService.ts (100%) rename src/{app => }/Domain/Auth/Auth.ts (100%) rename src/{app => }/Domain/Auth/AuthRepository.ts (100%) rename src/{app => }/Domain/Contracts.ts (100%) rename src/{app => }/Domain/Game/Answer.ts (100%) rename src/{app => }/Domain/Game/AnswerStatus.ts (100%) rename src/{app => }/Domain/Game/Game.ts (100%) rename src/{app => }/Domain/Game/GameRepository.ts (100%) rename src/{app => }/Domain/Game/Question.ts (100%) rename src/{app => }/Domain/Util.ts (100%) rename src/{app => }/Infrastructure/Backend/HttpAuthRepository.ts (100%) rename src/{app => }/Infrastructure/Http/Contracts.ts (100%) rename src/{app => }/Infrastructure/Http/HttpClient.ts (100%) rename src/{app => }/Infrastructure/Http/HttpClientFactory.ts (100%) rename src/{app => }/Infrastructure/Http/JsonHttpClient.ts (100%) rename src/{app => }/Infrastructure/InMemory/InMemoryAuthRepository.ts (100%) rename src/{app => }/Infrastructure/InMemory/InMemoryGameRepository.ts (100%) rename src/{app => }/Infrastructure/InMemory/InMemoryRepository.ts (100%) rename src/{app => }/Infrastructure/InMemory/games.ts (100%) rename src/{app => }/Infrastructure/Supabase/Mapper/GameMapper.ts (100%) rename src/{app => }/Infrastructure/Supabase/SupabaseAuthRepository.ts (100%) rename src/{app => }/Infrastructure/Supabase/SupabaseClientFactory.ts (100%) rename src/{app => }/Infrastructure/Supabase/SupabaseGameRepository.ts (100%) rename {src => view}/App.css (100%) rename {src => view}/App.tsx (61%) rename {src/view => view}/components/app/AppContext.tsx (100%) rename {src/view => view}/components/auth/ProtectPage.tsx (100%) rename {src/view => view}/components/game/GameImage.tsx (100%) rename {src/view => view}/components/game/GameList.tsx (93%) rename {src/view => view}/components/game/GamePlaySession.tsx (94%) rename {src/view => view}/components/game/game-list/index.module.css (100%) rename {src/view => view}/components/game/game-play-session/GamePlaySessionInstruction.tsx (94%) rename {src/view => view}/components/game/game-play-session/GamePlaySessionQuestion.tsx (93%) rename {src/view => view}/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionCorrect.tsx (100%) rename {src/view => view}/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionTimeExpired.tsx (100%) rename {src/view => view}/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionUnanswered.tsx (97%) rename {src/view => view}/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionWrong.tsx (100%) rename {src/view => view}/components/game/game-play-session/game-play-session-question/index.tsx (100%) rename {src/view => view}/components/general/Alert.tsx (100%) rename {src/view => view}/components/general/Async.tsx (100%) rename {src/view => view}/components/general/Loading.tsx (100%) rename {src/view => view}/components/general/Markdown.tsx (100%) rename {src/view => view}/components/general/Match.tsx (100%) rename {src/view => view}/contracts.ts (79%) rename {src => view}/decorators.ts (100%) rename {src/view => view}/hooks/useApp.ts (100%) rename {src/view => view}/hooks/useRunOnce.ts (100%) rename {src/view => view}/layouts/PublicLayout.tsx (100%) rename {src/view => view}/pages/DashboardPage.tsx (100%) rename {src/view => view}/pages/HomePage.tsx (100%) rename {src/view => view}/pages/auth/SignInPage.tsx (100%) rename {src/view => view}/pages/auth/WaitOneTimePassword.tsx (100%) rename {src/view => view}/pages/game/GameEndPage.tsx (100%) rename {src/view => view}/pages/game/GamePlayPage.tsx (94%) rename {src/view => view}/pages/game/GameWelcomePage.tsx (92%) rename {src/view => view}/providers/AppProvider.tsx (100%) rename {src/view => view}/providers/auth-manager-factory.ts (92%) rename src/vite-env.d.ts => vite-env.d.ts (100%) diff --git a/src/config/assets.ts b/config/assets.ts similarity index 100% rename from src/config/assets.ts rename to config/assets.ts diff --git a/src/config/dependencies.ts b/config/dependencies.ts similarity index 73% rename from src/config/dependencies.ts rename to config/dependencies.ts index 705ccfd..09bacb5 100644 --- a/src/config/dependencies.ts +++ b/config/dependencies.ts @@ -1,11 +1,11 @@ import 'reflect-metadata' import { container } from 'tsyringe' -import { AuthService } from '../app/Application/AuthService.ts' -import SupabaseAuthRepository from '../app/Infrastructure/Supabase/SupabaseAuthRepository.ts' -import HttpAuthRepository from '../app/Infrastructure/Backend/HttpAuthRepository.ts' -import InMemoryGameRepository from '../app/Infrastructure/InMemory/InMemoryGameRepository.ts' -import SupabaseGameRepository from '../app/Infrastructure/Supabase/SupabaseGameRepository.ts' -import InMemoryAuthRepository from '../app/Infrastructure/InMemory/InMemoryAuthRepository.ts' +import { AuthService } from '../src/Application/AuthService.ts' +import SupabaseAuthRepository from '../src/Infrastructure/Supabase/SupabaseAuthRepository.ts' +import HttpAuthRepository from '../src/Infrastructure/Backend/HttpAuthRepository.ts' +import InMemoryGameRepository from '../src/Infrastructure/InMemory/InMemoryGameRepository.ts' +import SupabaseGameRepository from '../src/Infrastructure/Supabase/SupabaseGameRepository.ts' +import InMemoryAuthRepository from '../src/Infrastructure/InMemory/InMemoryAuthRepository.ts' import { mode } from './env.ts' diff --git a/src/config/env.ts b/config/env.ts similarity index 100% rename from src/config/env.ts rename to config/env.ts diff --git a/src/config/i18n.ts b/config/i18n.ts similarity index 100% rename from src/config/i18n.ts rename to config/i18n.ts diff --git a/src/database/static/games.json b/database/static/games.json similarity index 100% rename from src/database/static/games.json rename to database/static/games.json diff --git a/src/index.css b/index.css similarity index 100% rename from src/index.css rename to index.css diff --git a/index.html b/index.html index c509fde..24a171d 100644 --- a/index.html +++ b/index.html @@ -13,6 +13,6 @@
- + diff --git a/src/main.tsx b/main.tsx similarity index 92% rename from src/main.tsx rename to main.tsx index 281dc99..f5767b2 100644 --- a/src/main.tsx +++ b/main.tsx @@ -3,7 +3,7 @@ import ReactDOM from 'react-dom/client' import { BrowserRouter } from 'react-router-dom' import './config/i18n' -import App from './App.tsx' +import App from './view/App.tsx' import './index.css' import { base } from './config/assets.ts' diff --git a/src/app/Application/AuthService.ts b/src/Application/AuthService.ts similarity index 100% rename from src/app/Application/AuthService.ts rename to src/Application/AuthService.ts diff --git a/src/app/Domain/Auth/Auth.ts b/src/Domain/Auth/Auth.ts similarity index 100% rename from src/app/Domain/Auth/Auth.ts rename to src/Domain/Auth/Auth.ts diff --git a/src/app/Domain/Auth/AuthRepository.ts b/src/Domain/Auth/AuthRepository.ts similarity index 100% rename from src/app/Domain/Auth/AuthRepository.ts rename to src/Domain/Auth/AuthRepository.ts diff --git a/src/app/Domain/Contracts.ts b/src/Domain/Contracts.ts similarity index 100% rename from src/app/Domain/Contracts.ts rename to src/Domain/Contracts.ts diff --git a/src/app/Domain/Game/Answer.ts b/src/Domain/Game/Answer.ts similarity index 100% rename from src/app/Domain/Game/Answer.ts rename to src/Domain/Game/Answer.ts diff --git a/src/app/Domain/Game/AnswerStatus.ts b/src/Domain/Game/AnswerStatus.ts similarity index 100% rename from src/app/Domain/Game/AnswerStatus.ts rename to src/Domain/Game/AnswerStatus.ts diff --git a/src/app/Domain/Game/Game.ts b/src/Domain/Game/Game.ts similarity index 100% rename from src/app/Domain/Game/Game.ts rename to src/Domain/Game/Game.ts diff --git a/src/app/Domain/Game/GameRepository.ts b/src/Domain/Game/GameRepository.ts similarity index 100% rename from src/app/Domain/Game/GameRepository.ts rename to src/Domain/Game/GameRepository.ts diff --git a/src/app/Domain/Game/Question.ts b/src/Domain/Game/Question.ts similarity index 100% rename from src/app/Domain/Game/Question.ts rename to src/Domain/Game/Question.ts diff --git a/src/app/Domain/Util.ts b/src/Domain/Util.ts similarity index 100% rename from src/app/Domain/Util.ts rename to src/Domain/Util.ts diff --git a/src/app/Infrastructure/Backend/HttpAuthRepository.ts b/src/Infrastructure/Backend/HttpAuthRepository.ts similarity index 100% rename from src/app/Infrastructure/Backend/HttpAuthRepository.ts rename to src/Infrastructure/Backend/HttpAuthRepository.ts diff --git a/src/app/Infrastructure/Http/Contracts.ts b/src/Infrastructure/Http/Contracts.ts similarity index 100% rename from src/app/Infrastructure/Http/Contracts.ts rename to src/Infrastructure/Http/Contracts.ts diff --git a/src/app/Infrastructure/Http/HttpClient.ts b/src/Infrastructure/Http/HttpClient.ts similarity index 100% rename from src/app/Infrastructure/Http/HttpClient.ts rename to src/Infrastructure/Http/HttpClient.ts diff --git a/src/app/Infrastructure/Http/HttpClientFactory.ts b/src/Infrastructure/Http/HttpClientFactory.ts similarity index 100% rename from src/app/Infrastructure/Http/HttpClientFactory.ts rename to src/Infrastructure/Http/HttpClientFactory.ts diff --git a/src/app/Infrastructure/Http/JsonHttpClient.ts b/src/Infrastructure/Http/JsonHttpClient.ts similarity index 100% rename from src/app/Infrastructure/Http/JsonHttpClient.ts rename to src/Infrastructure/Http/JsonHttpClient.ts diff --git a/src/app/Infrastructure/InMemory/InMemoryAuthRepository.ts b/src/Infrastructure/InMemory/InMemoryAuthRepository.ts similarity index 100% rename from src/app/Infrastructure/InMemory/InMemoryAuthRepository.ts rename to src/Infrastructure/InMemory/InMemoryAuthRepository.ts diff --git a/src/app/Infrastructure/InMemory/InMemoryGameRepository.ts b/src/Infrastructure/InMemory/InMemoryGameRepository.ts similarity index 100% rename from src/app/Infrastructure/InMemory/InMemoryGameRepository.ts rename to src/Infrastructure/InMemory/InMemoryGameRepository.ts diff --git a/src/app/Infrastructure/InMemory/InMemoryRepository.ts b/src/Infrastructure/InMemory/InMemoryRepository.ts similarity index 100% rename from src/app/Infrastructure/InMemory/InMemoryRepository.ts rename to src/Infrastructure/InMemory/InMemoryRepository.ts diff --git a/src/app/Infrastructure/InMemory/games.ts b/src/Infrastructure/InMemory/games.ts similarity index 100% rename from src/app/Infrastructure/InMemory/games.ts rename to src/Infrastructure/InMemory/games.ts diff --git a/src/app/Infrastructure/Supabase/Mapper/GameMapper.ts b/src/Infrastructure/Supabase/Mapper/GameMapper.ts similarity index 100% rename from src/app/Infrastructure/Supabase/Mapper/GameMapper.ts rename to src/Infrastructure/Supabase/Mapper/GameMapper.ts diff --git a/src/app/Infrastructure/Supabase/SupabaseAuthRepository.ts b/src/Infrastructure/Supabase/SupabaseAuthRepository.ts similarity index 100% rename from src/app/Infrastructure/Supabase/SupabaseAuthRepository.ts rename to src/Infrastructure/Supabase/SupabaseAuthRepository.ts diff --git a/src/app/Infrastructure/Supabase/SupabaseClientFactory.ts b/src/Infrastructure/Supabase/SupabaseClientFactory.ts similarity index 100% rename from src/app/Infrastructure/Supabase/SupabaseClientFactory.ts rename to src/Infrastructure/Supabase/SupabaseClientFactory.ts diff --git a/src/app/Infrastructure/Supabase/SupabaseGameRepository.ts b/src/Infrastructure/Supabase/SupabaseGameRepository.ts similarity index 100% rename from src/app/Infrastructure/Supabase/SupabaseGameRepository.ts rename to src/Infrastructure/Supabase/SupabaseGameRepository.ts diff --git a/tsconfig.json b/tsconfig.json index d06a2e6..f9d394c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,8 +21,9 @@ "noFallthroughCasesInSwitch": true, "experimentalDecorators": true, - "emitDecoratorMetadata": true + "emitDecoratorMetadata": true, + "types": ["vite/client"] }, - "include": ["src"], + "include": ["src", "view", "config"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/src/App.css b/view/App.css similarity index 100% rename from src/App.css rename to view/App.css diff --git a/src/App.tsx b/view/App.tsx similarity index 61% rename from src/App.tsx rename to view/App.tsx index 7ba10a3..91d38a7 100644 --- a/src/App.tsx +++ b/view/App.tsx @@ -2,26 +2,26 @@ import 'reflect-metadata' import { Route, Routes } from 'react-router-dom' -import { AppProvider } from './view/providers/AppProvider' +import { AppProvider } from './providers/AppProvider.tsx' -import { PublicLayout } from './view/layouts/PublicLayout.tsx' +import { PublicLayout } from './layouts/PublicLayout.tsx' import './App.css' // components -import { ProtectPage } from './view/components/auth/ProtectPage' +import { ProtectPage } from './components/auth/ProtectPage.tsx' // pages -import { HomePage } from './view/pages/HomePage.tsx' +import { HomePage } from './pages/HomePage.tsx' // game -import { GameWelcomePage } from './view/pages/game/GameWelcomePage.tsx' -import { GamePlayPage } from './view/pages/game/GamePlayPage.tsx' -import { GameEndPage } from './view/pages/game/GameEndPage.tsx' +import { GameWelcomePage } from './pages/game/GameWelcomePage.tsx' +import { GamePlayPage } from './pages/game/GamePlayPage.tsx' +import { GameEndPage } from './pages/game/GameEndPage.tsx' // session -import { DashboardPage } from './view/pages/DashboardPage.tsx' -import { SignInPage } from './view/pages/auth/SignInPage.tsx' -import { WaitOneTimePassword } from './view/pages/auth/WaitOneTimePassword.tsx' -import { useRunOnce } from './view/hooks/useRunOnce.ts' +import { DashboardPage } from './pages/DashboardPage.tsx' +import { SignInPage } from './pages/auth/SignInPage.tsx' +import { WaitOneTimePassword } from './pages/auth/WaitOneTimePassword.tsx' +import { useRunOnce } from './hooks/useRunOnce.ts' -import { name } from './config/i18n.ts' +import { name } from '../config/i18n.ts' export default function App () { useRunOnce(() => document.title = name) diff --git a/src/view/components/app/AppContext.tsx b/view/components/app/AppContext.tsx similarity index 100% rename from src/view/components/app/AppContext.tsx rename to view/components/app/AppContext.tsx diff --git a/src/view/components/auth/ProtectPage.tsx b/view/components/auth/ProtectPage.tsx similarity index 100% rename from src/view/components/auth/ProtectPage.tsx rename to view/components/auth/ProtectPage.tsx diff --git a/src/view/components/game/GameImage.tsx b/view/components/game/GameImage.tsx similarity index 100% rename from src/view/components/game/GameImage.tsx rename to view/components/game/GameImage.tsx diff --git a/src/view/components/game/GameList.tsx b/view/components/game/GameList.tsx similarity index 93% rename from src/view/components/game/GameList.tsx rename to view/components/game/GameList.tsx index 8000891..e9f889d 100644 --- a/src/view/components/game/GameList.tsx +++ b/view/components/game/GameList.tsx @@ -1,4 +1,4 @@ -import Game from '../../../app/Domain/Game/Game.ts' +import Game from '../../../src/Domain/Game/Game.ts' import { Link } from 'react-router-dom' import { useTranslation } from 'react-i18next' diff --git a/src/view/components/game/GamePlaySession.tsx b/view/components/game/GamePlaySession.tsx similarity index 94% rename from src/view/components/game/GamePlaySession.tsx rename to view/components/game/GamePlaySession.tsx index 52e6fd4..eebd26b 100644 --- a/src/view/components/game/GamePlaySession.tsx +++ b/view/components/game/GamePlaySession.tsx @@ -1,9 +1,9 @@ import { useEffect, useState } from 'react' -import Question from '../../../app/Domain/Game/Question.ts' -import AnswerStatus from '../../../app/Domain/Game/AnswerStatus.ts' -import { shuffle } from '../../../app/Domain/Util.ts' -import Game from '../../../app/Domain/Game/Game.ts' +import Question from '../../../src/Domain/Game/Question.ts' +import AnswerStatus from '../../../src/Domain/Game/AnswerStatus.ts' +import { shuffle } from '../../../src/Domain/Util.ts' +import Game from '../../../src/Domain/Game/Game.ts' import { GamePlaySessionInstruction } from './game-play-session/GamePlaySessionInstruction.tsx' import { GamePlaySessionQuestion, GameQuestionAnswerQuestion } from './game-play-session/GamePlaySessionQuestion.tsx' diff --git a/src/view/components/game/game-list/index.module.css b/view/components/game/game-list/index.module.css similarity index 100% rename from src/view/components/game/game-list/index.module.css rename to view/components/game/game-list/index.module.css diff --git a/src/view/components/game/game-play-session/GamePlaySessionInstruction.tsx b/view/components/game/game-play-session/GamePlaySessionInstruction.tsx similarity index 94% rename from src/view/components/game/game-play-session/GamePlaySessionInstruction.tsx rename to view/components/game/game-play-session/GamePlaySessionInstruction.tsx index 00903fd..e785e52 100644 --- a/src/view/components/game/game-play-session/GamePlaySessionInstruction.tsx +++ b/view/components/game/game-play-session/GamePlaySessionInstruction.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next' -import Game from '../../../../app/Domain/Game/Game.ts' +import Game from '../../../../src/Domain/Game/Game.ts' import { AlertPrimary } from '../../general/Alert.tsx' export type GameInstructionProps = { diff --git a/src/view/components/game/game-play-session/GamePlaySessionQuestion.tsx b/view/components/game/game-play-session/GamePlaySessionQuestion.tsx similarity index 93% rename from src/view/components/game/game-play-session/GamePlaySessionQuestion.tsx rename to view/components/game/game-play-session/GamePlaySessionQuestion.tsx index 580e15a..087898a 100644 --- a/src/view/components/game/game-play-session/GamePlaySessionQuestion.tsx +++ b/view/components/game/game-play-session/GamePlaySessionQuestion.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react' -import Answer from '../../../../app/Domain/Game/Answer.ts' -import { shuffle } from '../../../../app/Domain/Util.ts' -import AnswerStatus from '../../../../app/Domain/Game/AnswerStatus.ts' +import Answer from '../../../../src/Domain/Game/Answer.ts' +import { shuffle } from '../../../../src/Domain/Util.ts' +import AnswerStatus from '../../../../src/Domain/Game/AnswerStatus.ts' import { Case, Match } from '../../general/Match.tsx' diff --git a/src/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionCorrect.tsx b/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionCorrect.tsx similarity index 100% rename from src/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionCorrect.tsx rename to view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionCorrect.tsx diff --git a/src/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionTimeExpired.tsx b/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionTimeExpired.tsx similarity index 100% rename from src/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionTimeExpired.tsx rename to view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionTimeExpired.tsx diff --git a/src/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionUnanswered.tsx b/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionUnanswered.tsx similarity index 97% rename from src/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionUnanswered.tsx rename to view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionUnanswered.tsx index 3c32897..665734e 100644 --- a/src/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionUnanswered.tsx +++ b/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionUnanswered.tsx @@ -1,7 +1,7 @@ import { useTranslation } from 'react-i18next' import { Markdown } from '../../../general/Markdown.tsx' -import Answer from '../../../../../app/Domain/Game/Answer.ts' +import Answer from '../../../../../src/Domain/Game/Answer.ts' export type GameQuestionOptionsProps = { text: string diff --git a/src/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionWrong.tsx b/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionWrong.tsx similarity index 100% rename from src/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionWrong.tsx rename to view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionWrong.tsx diff --git a/src/view/components/game/game-play-session/game-play-session-question/index.tsx b/view/components/game/game-play-session/game-play-session-question/index.tsx similarity index 100% rename from src/view/components/game/game-play-session/game-play-session-question/index.tsx rename to view/components/game/game-play-session/game-play-session-question/index.tsx diff --git a/src/view/components/general/Alert.tsx b/view/components/general/Alert.tsx similarity index 100% rename from src/view/components/general/Alert.tsx rename to view/components/general/Alert.tsx diff --git a/src/view/components/general/Async.tsx b/view/components/general/Async.tsx similarity index 100% rename from src/view/components/general/Async.tsx rename to view/components/general/Async.tsx diff --git a/src/view/components/general/Loading.tsx b/view/components/general/Loading.tsx similarity index 100% rename from src/view/components/general/Loading.tsx rename to view/components/general/Loading.tsx diff --git a/src/view/components/general/Markdown.tsx b/view/components/general/Markdown.tsx similarity index 100% rename from src/view/components/general/Markdown.tsx rename to view/components/general/Markdown.tsx diff --git a/src/view/components/general/Match.tsx b/view/components/general/Match.tsx similarity index 100% rename from src/view/components/general/Match.tsx rename to view/components/general/Match.tsx diff --git a/src/view/contracts.ts b/view/contracts.ts similarity index 79% rename from src/view/contracts.ts rename to view/contracts.ts index 2227df9..61d9fa7 100644 --- a/src/view/contracts.ts +++ b/view/contracts.ts @@ -1,5 +1,5 @@ import DependencyContainer from 'tsyringe/dist/typings/types/dependency-container' -import { AuthContract, Session } from '../app/Domain/Auth/Auth.ts' +import { AuthContract, Session } from '../src/Domain/Auth/Auth.ts' export type { AuthContract, Session } diff --git a/src/decorators.ts b/view/decorators.ts similarity index 100% rename from src/decorators.ts rename to view/decorators.ts diff --git a/src/view/hooks/useApp.ts b/view/hooks/useApp.ts similarity index 100% rename from src/view/hooks/useApp.ts rename to view/hooks/useApp.ts diff --git a/src/view/hooks/useRunOnce.ts b/view/hooks/useRunOnce.ts similarity index 100% rename from src/view/hooks/useRunOnce.ts rename to view/hooks/useRunOnce.ts diff --git a/src/view/layouts/PublicLayout.tsx b/view/layouts/PublicLayout.tsx similarity index 100% rename from src/view/layouts/PublicLayout.tsx rename to view/layouts/PublicLayout.tsx diff --git a/src/view/pages/DashboardPage.tsx b/view/pages/DashboardPage.tsx similarity index 100% rename from src/view/pages/DashboardPage.tsx rename to view/pages/DashboardPage.tsx diff --git a/src/view/pages/HomePage.tsx b/view/pages/HomePage.tsx similarity index 100% rename from src/view/pages/HomePage.tsx rename to view/pages/HomePage.tsx diff --git a/src/view/pages/auth/SignInPage.tsx b/view/pages/auth/SignInPage.tsx similarity index 100% rename from src/view/pages/auth/SignInPage.tsx rename to view/pages/auth/SignInPage.tsx diff --git a/src/view/pages/auth/WaitOneTimePassword.tsx b/view/pages/auth/WaitOneTimePassword.tsx similarity index 100% rename from src/view/pages/auth/WaitOneTimePassword.tsx rename to view/pages/auth/WaitOneTimePassword.tsx diff --git a/src/view/pages/game/GameEndPage.tsx b/view/pages/game/GameEndPage.tsx similarity index 100% rename from src/view/pages/game/GameEndPage.tsx rename to view/pages/game/GameEndPage.tsx diff --git a/src/view/pages/game/GamePlayPage.tsx b/view/pages/game/GamePlayPage.tsx similarity index 94% rename from src/view/pages/game/GamePlayPage.tsx rename to view/pages/game/GamePlayPage.tsx index 60c2ee5..b9dbdd0 100644 --- a/src/view/pages/game/GamePlayPage.tsx +++ b/view/pages/game/GamePlayPage.tsx @@ -2,8 +2,8 @@ import { useNavigate, useParams } from 'react-router-dom' import { useState } from 'react' import { useTranslation } from 'react-i18next' -import GameRepository from '../../../app/Domain/Game/GameRepository.ts' -import Game from '../../../app/Domain/Game/Game.ts' +import GameRepository from '../../../src/Domain/Game/GameRepository.ts' +import Game from '../../../src/Domain/Game/Game.ts' import { useApp } from '../../hooks/useApp.ts' import { Async, AsyncStatus, On } from '../../components/general/Async.tsx' diff --git a/src/view/pages/game/GameWelcomePage.tsx b/view/pages/game/GameWelcomePage.tsx similarity index 92% rename from src/view/pages/game/GameWelcomePage.tsx rename to view/pages/game/GameWelcomePage.tsx index 391af54..954dcef 100644 --- a/src/view/pages/game/GameWelcomePage.tsx +++ b/view/pages/game/GameWelcomePage.tsx @@ -1,8 +1,8 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' -import GameRepository from '../../../app/Domain/Game/GameRepository.ts' -import Game from '../../../app/Domain/Game/Game.ts' +import GameRepository from '../../../src/Domain/Game/GameRepository.ts' +import Game from '../../../src/Domain/Game/Game.ts' import { useApp } from '../../hooks/useApp.ts' import { Async, AsyncStatus, On } from '../../components/general/Async.tsx' diff --git a/src/view/providers/AppProvider.tsx b/view/providers/AppProvider.tsx similarity index 100% rename from src/view/providers/AppProvider.tsx rename to view/providers/AppProvider.tsx diff --git a/src/view/providers/auth-manager-factory.ts b/view/providers/auth-manager-factory.ts similarity index 92% rename from src/view/providers/auth-manager-factory.ts rename to view/providers/auth-manager-factory.ts index 824c0ab..7a743f4 100644 --- a/src/view/providers/auth-manager-factory.ts +++ b/view/providers/auth-manager-factory.ts @@ -1,6 +1,6 @@ import { Session } from '../contracts.ts' import { container } from 'tsyringe' -import { AuthService } from '../../app/Application/AuthService.ts' +import { AuthService } from '../../src/Application/AuthService.ts' export function authManagerFactory (updateAuthSession: (session: Session) => void) { const authService = container.resolve('AuthService') diff --git a/src/vite-env.d.ts b/vite-env.d.ts similarity index 100% rename from src/vite-env.d.ts rename to vite-env.d.ts From 4251c0411f9d18d193051d1b6d078f2a92db91dc Mon Sep 17 00:00:00 2001 From: William Correa Date: Thu, 11 Apr 2024 19:29:57 -0300 Subject: [PATCH 02/17] feature: prepare dependencies to get infra from user session --- config/dependencies.ts | 30 +++++----- config/env.ts | 13 ++++- src/Domain/Auth/Auth.ts | 3 + src/Domain/Contracts.ts | 13 +++++ .../{ => Driver}/Http/Contracts.ts | 2 +- .../{ => Driver}/Http/HttpClient.ts | 4 +- .../{ => Driver}/Http/HttpClientFactory.ts | 2 +- .../{ => Driver}/Http/JsonHttpClient.ts | 4 +- .../Memory}/InMemoryRepository.ts | 0 .../Driver/Supabase/SupabaseClientFactory.ts | 20 +++++++ .../Driver/Supabase/SupabaseRepository.ts | 11 ++++ .../{Backend => Http}/HttpAuthRepository.ts | 4 +- .../InMemoryAuthRepository.ts | 2 +- .../InMemoryGameRepository.ts | 2 +- .../{InMemory => Memory}/games.ts | 0 .../Supabase/SupabaseAuthRepository.ts | 19 +++--- .../Supabase/SupabaseClientFactory.ts | 14 ----- .../Supabase/SupabaseGameRepository.ts | 23 ++++---- view/components/general/Async.tsx | 8 +-- view/pages/auth/SignInPage.tsx | 4 +- view/store.ts | 58 +++++++++++++++++++ view/stores/session.ts | 17 ++++++ 22 files changed, 186 insertions(+), 67 deletions(-) rename src/Infrastructure/{ => Driver}/Http/Contracts.ts (89%) rename src/Infrastructure/{ => Driver}/Http/HttpClient.ts (94%) rename src/Infrastructure/{ => Driver}/Http/HttpClientFactory.ts (95%) rename src/Infrastructure/{ => Driver}/Http/JsonHttpClient.ts (85%) rename src/Infrastructure/{InMemory => Driver/Memory}/InMemoryRepository.ts (100%) create mode 100644 src/Infrastructure/Driver/Supabase/SupabaseClientFactory.ts create mode 100644 src/Infrastructure/Driver/Supabase/SupabaseRepository.ts rename src/Infrastructure/{Backend => Http}/HttpAuthRepository.ts (93%) rename src/Infrastructure/{InMemory => Memory}/InMemoryAuthRepository.ts (90%) rename src/Infrastructure/{InMemory => Memory}/InMemoryGameRepository.ts (90%) rename src/Infrastructure/{InMemory => Memory}/games.ts (100%) delete mode 100644 src/Infrastructure/Supabase/SupabaseClientFactory.ts create mode 100644 view/store.ts create mode 100644 view/stores/session.ts diff --git a/config/dependencies.ts b/config/dependencies.ts index 09bacb5..58e0ffa 100644 --- a/config/dependencies.ts +++ b/config/dependencies.ts @@ -1,33 +1,37 @@ import 'reflect-metadata' import { container } from 'tsyringe' + +import { DriverResolver, Data, DriverType } from '../src/Domain/Contracts.ts' + import { AuthService } from '../src/Application/AuthService.ts' import SupabaseAuthRepository from '../src/Infrastructure/Supabase/SupabaseAuthRepository.ts' -import HttpAuthRepository from '../src/Infrastructure/Backend/HttpAuthRepository.ts' -import InMemoryGameRepository from '../src/Infrastructure/InMemory/InMemoryGameRepository.ts' +import HttpAuthRepository from '../src/Infrastructure/Http/HttpAuthRepository.ts' +import InMemoryGameRepository from '../src/Infrastructure/Memory/InMemoryGameRepository.ts' import SupabaseGameRepository from '../src/Infrastructure/Supabase/SupabaseGameRepository.ts' -import InMemoryAuthRepository from '../src/Infrastructure/InMemory/InMemoryAuthRepository.ts' +import InMemoryAuthRepository from '../src/Infrastructure/Memory/InMemoryAuthRepository.ts' -import { mode } from './env.ts' +import { loadedDriver } from './env.ts' -const binds: Record unknown>> = { - http: { +const binds: DriverResolver = { + [DriverType.http]: { AuthRepository: () => HttpAuthRepository.build(), - // GameRepository: () => HttpGameRepository.build(), + // GameRepository: (config: Data) => HttpGameRepository.build(), }, - memory: { + [DriverType.memory]: { AuthRepository: () => new InMemoryAuthRepository(), GameRepository: () => new InMemoryGameRepository(), }, - supabase: { - AuthRepository: () => SupabaseAuthRepository.build(), - GameRepository: () => SupabaseGameRepository.build(), + [DriverType.supabase]: { + AuthRepository: (config: Data) => SupabaseAuthRepository.build(config), + GameRepository: (config: Data) => SupabaseGameRepository.build(config), }, } const factory = (token: string): [string, { useFactory: () => unknown }] => { - const bind = binds[mode()] + const driver = loadedDriver() + const bind = binds[driver.type] const useFactory = bind[token] - return [token, { useFactory }] + return [token, { useFactory: () => useFactory(driver.config) }] } export default function () { diff --git a/config/env.ts b/config/env.ts index 85d4481..68c02e4 100644 --- a/config/env.ts +++ b/config/env.ts @@ -1 +1,12 @@ -export const mode = (): string => localStorage.getItem('mode') || import.meta.env.VITE_BACKEND_MODE || 'supabase' +import { sessionStore } from '../view/stores/session.ts' +import { Driver, DriverType } from '../src/Domain/Contracts.ts' + +const fallback: Driver = { + type: DriverType.supabase, + config: { + url: import.meta.env.VITE_SUPABASE_URL, + anonymousKey: import.meta.env.VITE_SUPABASE_ANON_KEY, + } +} + +export const loadedDriver = (): Driver => sessionStore.state?.driver || fallback diff --git a/src/Domain/Auth/Auth.ts b/src/Domain/Auth/Auth.ts index 97ca10d..461d765 100644 --- a/src/Domain/Auth/Auth.ts +++ b/src/Domain/Auth/Auth.ts @@ -1,3 +1,5 @@ +import { Driver } from '../Contracts.ts' + export type Session = { username: string credential?: { @@ -7,6 +9,7 @@ export type Session = { type: string } abilities?: string[] + driver?: Driver } | null export interface AuthContract { diff --git a/src/Domain/Contracts.ts b/src/Domain/Contracts.ts index ced6fbd..8601654 100644 --- a/src/Domain/Contracts.ts +++ b/src/Domain/Contracts.ts @@ -11,3 +11,16 @@ export interface Content { } export type Data = { [property: string]: Data | unknown } + +export enum DriverType { + http = 'http', + memory = 'memory', + supabase = 'supabase', +} + +export type Driver = { + type: DriverType + config: Data +} + +export type DriverResolver = Record unknown>> diff --git a/src/Infrastructure/Http/Contracts.ts b/src/Infrastructure/Driver/Http/Contracts.ts similarity index 89% rename from src/Infrastructure/Http/Contracts.ts rename to src/Infrastructure/Driver/Http/Contracts.ts index 3cfc673..20de48c 100644 --- a/src/Infrastructure/Http/Contracts.ts +++ b/src/Infrastructure/Driver/Http/Contracts.ts @@ -1,4 +1,4 @@ -import type { Content, Data } from '../../Domain/Contracts.ts' +import type { Content, Data } from '../../../Domain/Contracts.ts' export interface HttpRequester { request: (method: string, path: string, data?: Data) => Promise diff --git a/src/Infrastructure/Http/HttpClient.ts b/src/Infrastructure/Driver/Http/HttpClient.ts similarity index 94% rename from src/Infrastructure/Http/HttpClient.ts rename to src/Infrastructure/Driver/Http/HttpClient.ts index e1d6916..065fcd8 100644 --- a/src/Infrastructure/Http/HttpClient.ts +++ b/src/Infrastructure/Driver/Http/HttpClient.ts @@ -1,5 +1,5 @@ -import type { Content, Data } from '../../Domain/Contracts.ts' -import { Status } from '../../Domain/Contracts.ts' +import type { Content, Data } from '../../../Domain/Contracts.ts' +import { Status } from '../../../Domain/Contracts.ts' import { HttpClientContract, HttpRequester } from './Contracts.ts' export default class HttpClient implements HttpClientContract { diff --git a/src/Infrastructure/Http/HttpClientFactory.ts b/src/Infrastructure/Driver/Http/HttpClientFactory.ts similarity index 95% rename from src/Infrastructure/Http/HttpClientFactory.ts rename to src/Infrastructure/Driver/Http/HttpClientFactory.ts index 2ce8870..d6c708b 100644 --- a/src/Infrastructure/Http/HttpClientFactory.ts +++ b/src/Infrastructure/Driver/Http/HttpClientFactory.ts @@ -1,5 +1,5 @@ import JsonHttpClient from './JsonHttpClient.ts' -import { Content, Data } from '../../Domain/Contracts.ts' +import { Content, Data } from '../../../Domain/Contracts.ts' import { HttpClientContract, HttpClientDriverContract } from './Contracts.ts' const clients: Record = {} diff --git a/src/Infrastructure/Http/JsonHttpClient.ts b/src/Infrastructure/Driver/Http/JsonHttpClient.ts similarity index 85% rename from src/Infrastructure/Http/JsonHttpClient.ts rename to src/Infrastructure/Driver/Http/JsonHttpClient.ts index 35077ca..e617010 100644 --- a/src/Infrastructure/Http/JsonHttpClient.ts +++ b/src/Infrastructure/Driver/Http/JsonHttpClient.ts @@ -1,5 +1,5 @@ -import type { Content, Data } from '../../Domain/Contracts.ts' -import { Status } from '../../Domain/Contracts.ts' +import type { Content, Data } from '../../../Domain/Contracts.ts' +import { Status } from '../../../Domain/Contracts.ts' import HttpClient from './HttpClient.ts' export default class JsonHttpClient extends HttpClient { diff --git a/src/Infrastructure/InMemory/InMemoryRepository.ts b/src/Infrastructure/Driver/Memory/InMemoryRepository.ts similarity index 100% rename from src/Infrastructure/InMemory/InMemoryRepository.ts rename to src/Infrastructure/Driver/Memory/InMemoryRepository.ts diff --git a/src/Infrastructure/Driver/Supabase/SupabaseClientFactory.ts b/src/Infrastructure/Driver/Supabase/SupabaseClientFactory.ts new file mode 100644 index 0000000..eb6cb1b --- /dev/null +++ b/src/Infrastructure/Driver/Supabase/SupabaseClientFactory.ts @@ -0,0 +1,20 @@ +import { createClient, SupabaseClient } from '@supabase/supabase-js' + +import { Data } from '../../../Domain/Contracts.ts' + +let client: SupabaseClient + +export default class SupabaseClientFactory { + make (config: Data): SupabaseClient { + try { + if (!client) { + client = createClient(config.url as string, config.anonymousKey as string) + } + return client + } catch (error) { + const message = 'Error creating Supabase client' + console.error(message, ': ', error) + throw new Error(message) + } + } +} diff --git a/src/Infrastructure/Driver/Supabase/SupabaseRepository.ts b/src/Infrastructure/Driver/Supabase/SupabaseRepository.ts new file mode 100644 index 0000000..cf4bc66 --- /dev/null +++ b/src/Infrastructure/Driver/Supabase/SupabaseRepository.ts @@ -0,0 +1,11 @@ +import { SupabaseClient } from '@supabase/supabase-js' +import SupabaseClientFactory from './SupabaseClientFactory.ts' +import { Data } from '../../../Domain/Contracts.ts' + +export default class SupabaseRepository { + protected readonly driver: SupabaseClient + + constructor (config: Data) { + this.driver = (new SupabaseClientFactory()).make(config) + } +} diff --git a/src/Infrastructure/Backend/HttpAuthRepository.ts b/src/Infrastructure/Http/HttpAuthRepository.ts similarity index 93% rename from src/Infrastructure/Backend/HttpAuthRepository.ts rename to src/Infrastructure/Http/HttpAuthRepository.ts index f345b53..68a38cb 100644 --- a/src/Infrastructure/Backend/HttpAuthRepository.ts +++ b/src/Infrastructure/Http/HttpAuthRepository.ts @@ -1,7 +1,7 @@ import AuthRepository from '../../Domain/Auth/AuthRepository.ts' import { Data, Status } from '../../Domain/Contracts.ts' -import { HttpClientDriverContract } from '../Http/Contracts.ts' -import HttpClientFactory from '../Http/HttpClientFactory.ts' +import { HttpClientDriverContract } from '../Driver/Http/Contracts.ts' +import HttpClientFactory from '../Driver/Http/HttpClientFactory.ts' import { Session } from '../../Domain/Auth/Auth.ts' export default class HttpAuthRepository implements AuthRepository { diff --git a/src/Infrastructure/InMemory/InMemoryAuthRepository.ts b/src/Infrastructure/Memory/InMemoryAuthRepository.ts similarity index 90% rename from src/Infrastructure/InMemory/InMemoryAuthRepository.ts rename to src/Infrastructure/Memory/InMemoryAuthRepository.ts index 3bc39f1..cab8345 100644 --- a/src/Infrastructure/InMemory/InMemoryAuthRepository.ts +++ b/src/Infrastructure/Memory/InMemoryAuthRepository.ts @@ -1,6 +1,6 @@ import AuthRepository from '../../Domain/Auth/AuthRepository.ts' import { Session } from '../../Domain/Auth/Auth.ts' -import InMemoryRepository from './InMemoryRepository.ts' +import InMemoryRepository from '../Driver/Memory/InMemoryRepository.ts' export default class InMemoryAuthRepository extends InMemoryRepository implements AuthRepository { restore (): Promise { diff --git a/src/Infrastructure/InMemory/InMemoryGameRepository.ts b/src/Infrastructure/Memory/InMemoryGameRepository.ts similarity index 90% rename from src/Infrastructure/InMemory/InMemoryGameRepository.ts rename to src/Infrastructure/Memory/InMemoryGameRepository.ts index d0f2fba..b75a233 100644 --- a/src/Infrastructure/InMemory/InMemoryGameRepository.ts +++ b/src/Infrastructure/Memory/InMemoryGameRepository.ts @@ -1,7 +1,7 @@ import GameRepository from '../../Domain/Game/GameRepository.ts' import Game from '../../Domain/Game/Game.ts' import games from './games.ts' -import InMemoryRepository from './InMemoryRepository.ts' +import InMemoryRepository from '../Driver/Memory/InMemoryRepository.ts' export default class InMemoryGameRepository extends InMemoryRepository implements GameRepository { private games: Game[] diff --git a/src/Infrastructure/InMemory/games.ts b/src/Infrastructure/Memory/games.ts similarity index 100% rename from src/Infrastructure/InMemory/games.ts rename to src/Infrastructure/Memory/games.ts diff --git a/src/Infrastructure/Supabase/SupabaseAuthRepository.ts b/src/Infrastructure/Supabase/SupabaseAuthRepository.ts index 4c8a4f5..91c50a6 100644 --- a/src/Infrastructure/Supabase/SupabaseAuthRepository.ts +++ b/src/Infrastructure/Supabase/SupabaseAuthRepository.ts @@ -1,17 +1,14 @@ -import AuthRepository from '../../Domain/Auth/AuthRepository.ts' -import SupabaseClientFactory from './SupabaseClientFactory.ts' -import { AuthOtpResponse, SupabaseClient } from '@supabase/supabase-js' -import { Session } from '../../Domain/Auth/Auth.ts' +import { AuthOtpResponse } from '@supabase/supabase-js' -export default class SupabaseAuthRepository implements AuthRepository { - private driver: SupabaseClient +import SupabaseRepository from '../Driver/Supabase/SupabaseRepository.ts' - constructor (supabaseClientFactory: SupabaseClientFactory) { - this.driver = supabaseClientFactory.make() - } +import AuthRepository from '../../Domain/Auth/AuthRepository.ts' +import { Session } from '../../Domain/Auth/Auth.ts' +import { Data } from '../../Domain/Contracts.ts' - static build () { - return new this(new SupabaseClientFactory()) +export default class SupabaseAuthRepository extends SupabaseRepository implements AuthRepository { + static build (config: Data) { + return new this(config) } async signInWithOtp (email: string): Promise { diff --git a/src/Infrastructure/Supabase/SupabaseClientFactory.ts b/src/Infrastructure/Supabase/SupabaseClientFactory.ts deleted file mode 100644 index ff7629d..0000000 --- a/src/Infrastructure/Supabase/SupabaseClientFactory.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createClient, SupabaseClient } from '@supabase/supabase-js' - -let client: SupabaseClient - -export default class SupabaseClientFactory { - make (): SupabaseClient { - const url = import.meta.env.VITE_SUPABASE_URL - const anonymousKey = import.meta.env.VITE_SUPABASE_ANON_KEY - if (!client) { - client = createClient(url, anonymousKey) - } - return client - } -} diff --git a/src/Infrastructure/Supabase/SupabaseGameRepository.ts b/src/Infrastructure/Supabase/SupabaseGameRepository.ts index ee2f614..558f716 100644 --- a/src/Infrastructure/Supabase/SupabaseGameRepository.ts +++ b/src/Infrastructure/Supabase/SupabaseGameRepository.ts @@ -1,27 +1,26 @@ -import { SupabaseClient } from '@supabase/supabase-js' +import SupabaseRepository from '../Driver/Supabase/SupabaseRepository.ts' -import Game from '../../Domain/Game/Game.ts' import GameRepository from '../../Domain/Game/GameRepository.ts' -import SupabaseClientFactory from './SupabaseClientFactory.ts' -import GameMapper from './Mapper/GameMapper.ts' +import { Data } from '../../Domain/Contracts.ts' +import Game from '../../Domain/Game/Game.ts' -export default class SupabaseGameRepository implements GameRepository { - private supabase: SupabaseClient +import GameMapper from './Mapper/GameMapper.ts' +export default class SupabaseGameRepository extends SupabaseRepository implements GameRepository { constructor ( private mapper: GameMapper, - supabaseClientFactory: SupabaseClientFactory + config: Data ) { - this.supabase = supabaseClientFactory.make() + super(config) } - static build () { - return new this(new GameMapper(), new SupabaseClientFactory()) + static build (config: Data) { + return new this(new GameMapper(), config) } async paginate (page: number, limit: number): Promise { const from = (page - 1) * limit - const { data, error } = await this.supabase + const { data, error } = await this.driver .from('games') .select(`id, description, @@ -49,7 +48,7 @@ export default class SupabaseGameRepository implements GameRepository { } async findById (id: number | string): Promise { - const { data, error } = await this.supabase + const { data, error } = await this.driver .from('games') .select(`id, description, diff --git a/view/components/general/Async.tsx b/view/components/general/Async.tsx index 168ad68..5260612 100644 --- a/view/components/general/Async.tsx +++ b/view/components/general/Async.tsx @@ -1,12 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ReactNode, useEffect, useRef, useState } from 'react' -export type HydrateElementProps = { +export type AsyncElementProps = { children: ReactNode | ReactNode[] status: AsyncStatus } -export type HydrateProps = { +export type AsyncProps = { using: () => Promise onResolve: (data: any) => void onReject?: (data: any) => void @@ -20,11 +20,11 @@ export enum AsyncStatus { Rejected = 'Rejected' } -export function On ({ children }: HydrateElementProps) { +export function On ({ children }: AsyncElementProps) { return children } -export function Async ({ using, onResolve, onReject, children }: HydrateProps) { +export function Async ({ using, onResolve, onReject, children }: AsyncProps) { const fetched = useRef(false) const [status, setStatus] = useState(AsyncStatus.Pending) diff --git a/view/pages/auth/SignInPage.tsx b/view/pages/auth/SignInPage.tsx index 3db224c..77b1871 100644 --- a/view/pages/auth/SignInPage.tsx +++ b/view/pages/auth/SignInPage.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react' import { useNavigate } from 'react-router-dom' import { useApp } from '../../hooks/useApp.ts' import { AlertDanger } from '../../components/general/Alert.tsx' -import { mode } from '../../../config/env.ts' +import { loadedDriver, Driver } from '../../../config/env.ts' import { Loading } from '../../components/general/Loading.tsx' export function SignInPage () { @@ -17,7 +17,7 @@ export function SignInPage () { return } - const type = mode() === 'supabase' ? 'otp' : 'password' + const type = loadedDriver() === Mode.supabase ? 'otp' : 'password' const handleSubmit = async (event: React.FormEvent) => { event.preventDefault() diff --git a/view/store.ts b/view/store.ts new file mode 100644 index 0000000..5bf4ba6 --- /dev/null +++ b/view/store.ts @@ -0,0 +1,58 @@ +export interface StoreState { + [key: string | symbol]: unknown +} + +export interface Store { + state: T + subscribe: (property: string, callback: (data: unknown) => void) => number + unsubscribe: (property: string, index: number) => void +} + +export function createStore (initial: StoreState): Store { + type EventMap = { + [key: string]: Array<(data: unknown) => void> + }; + + const events: EventMap = {} + + const publish = (eventName: string, data: unknown) => { + const subscribers = events[eventName] + if (subscribers) { + subscribers.forEach((callback) => { + callback(data) + }) + } + } + + const state = new Proxy(initial, { + get (target, property) { + return target[property] + }, + set (target, property, value) { + if (target[property] === value) { + return true + } + target[property] = value + publish(property as string, value) + return true + } + }) + + const subscribe = function (property: string, callback: (data: unknown) => void) { + if (!events[property]) { + events[property] = [] + } + return events[property].push(callback) + } + const unsubscribe = function (property: string, index: number) { + if (!events[property]) { + return + } + events[property].splice(index - 1, 1) + } + return { + state: state as T, + subscribe, + unsubscribe + } +} diff --git a/view/stores/session.ts b/view/stores/session.ts new file mode 100644 index 0000000..83e732d --- /dev/null +++ b/view/stores/session.ts @@ -0,0 +1,17 @@ +import { createStore, Store } from '../store.ts' +import { Session } from '../../src/Domain/Auth/Auth.ts' + +export const sessionStore: Store = createStore({ + username: '', + credential: null, + abilities: [], + infra: localStorage.getItem('mode') || import.meta.env.VITE_BACKEND_MODE || 'supabase', +}) + +sessionStore.subscribe('mode', (mode: unknown) => { + if (mode) { + window.sessionStorage.setItem('mode', mode as string) + return + } + window.sessionStorage.removeItem('mode') +}) From 98c163d82f55ad9a5d872e9dba4c7d5368b4cce5 Mon Sep 17 00:00:00 2001 From: William Correa Date: Thu, 11 Apr 2024 19:38:07 -0300 Subject: [PATCH 03/17] feature: prepare dependencies to get infra from user session --- .env | 2 -- view/stores/session.ts | 13 ++++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.env b/.env index 0ba2be7..88f9be0 100644 --- a/.env +++ b/.env @@ -2,8 +2,6 @@ VITE_BASE_PATH=/quiz VITE_IN_MEMORY_TIMEOUT=200 VITE_GAME_QUESTION_TIMEOUT=30 -VITE_BACKEND_MODE=memory - VITE_SUPABASE_URL=https://.supabase.co VITE_SUPABASE_ANON_KEY= diff --git a/view/stores/session.ts b/view/stores/session.ts index 83e732d..314f485 100644 --- a/view/stores/session.ts +++ b/view/stores/session.ts @@ -1,17 +1,20 @@ import { createStore, Store } from '../store.ts' import { Session } from '../../src/Domain/Auth/Auth.ts' +import { Driver } from '../../src/Domain/Contracts.ts' export const sessionStore: Store = createStore({ username: '', credential: null, abilities: [], - infra: localStorage.getItem('mode') || import.meta.env.VITE_BACKEND_MODE || 'supabase', + driver: window.sessionStorage.getItem('driver') ? + JSON.parse(window.sessionStorage.getItem('driver') as string) : + undefined, }) -sessionStore.subscribe('mode', (mode: unknown) => { - if (mode) { - window.sessionStorage.setItem('mode', mode as string) +sessionStore.subscribe('driver', (driver: unknown) => { + if (driver) { + window.sessionStorage.setItem('driver', JSON.stringify(driver as Driver)) return } - window.sessionStorage.removeItem('mode') + window.sessionStorage.removeItem('driver') }) From c36b1261de60e4fb9743322226064e454ad2b902 Mon Sep 17 00:00:00 2001 From: William Correa Date: Thu, 11 Apr 2024 19:53:07 -0300 Subject: [PATCH 04/17] feature: prepare dependencies to get infra from user session --- .env | 2 ++ config/dependencies.ts | 26 +++++++++++++++++--------- config/env.ts | 4 ++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/.env b/.env index 88f9be0..f7a2a80 100644 --- a/.env +++ b/.env @@ -2,6 +2,8 @@ VITE_BASE_PATH=/quiz VITE_IN_MEMORY_TIMEOUT=200 VITE_GAME_QUESTION_TIMEOUT=30 +VITE_DEVELOPMENT_MODE=false + VITE_SUPABASE_URL=https://.supabase.co VITE_SUPABASE_ANON_KEY= diff --git a/config/dependencies.ts b/config/dependencies.ts index 58e0ffa..6e5dba1 100644 --- a/config/dependencies.ts +++ b/config/dependencies.ts @@ -1,7 +1,7 @@ import 'reflect-metadata' import { container } from 'tsyringe' -import { DriverResolver, Data, DriverType } from '../src/Domain/Contracts.ts' +import { Data, Driver, DriverResolver, DriverType } from '../src/Domain/Contracts.ts' import { AuthService } from '../src/Application/AuthService.ts' import SupabaseAuthRepository from '../src/Infrastructure/Supabase/SupabaseAuthRepository.ts' @@ -10,33 +10,41 @@ import InMemoryGameRepository from '../src/Infrastructure/Memory/InMemoryGameRep import SupabaseGameRepository from '../src/Infrastructure/Supabase/SupabaseGameRepository.ts' import InMemoryAuthRepository from '../src/Infrastructure/Memory/InMemoryAuthRepository.ts' -import { loadedDriver } from './env.ts' +import { driverDefault, loadedDriver } from './env.ts' const binds: DriverResolver = { - [DriverType.http]: { - AuthRepository: () => HttpAuthRepository.build(), - // GameRepository: (config: Data) => HttpGameRepository.build(), - }, [DriverType.memory]: { AuthRepository: () => new InMemoryAuthRepository(), GameRepository: () => new InMemoryGameRepository(), }, + [DriverType.http]: { + AuthRepository: () => HttpAuthRepository.build(), + // GameRepository: (config: Data) => HttpGameRepository.build(), + }, [DriverType.supabase]: { AuthRepository: (config: Data) => SupabaseAuthRepository.build(config), GameRepository: (config: Data) => SupabaseGameRepository.build(config), }, } -const factory = (token: string): [string, { useFactory: () => unknown }] => { - const driver = loadedDriver() +const factory = (token: string, driver?: Driver): [string, { useFactory: () => unknown }] => { + if (!driver) { + driver = loadedDriver() + } const bind = binds[driver.type] const useFactory = bind[token] return [token, { useFactory: () => useFactory(driver.config) }] } export default function () { + // structure stuff container.register('AuthService', { useClass: AuthService }) - container.register(...factory('AuthRepository')) + if (import.meta.env.VITE_DEVELOPMENT_MODE === 'true') { + container.register(...factory('AuthRepository')) + } + container.register(...factory('AuthRepository', driverDefault)) + + // game stuff container.register(...factory('GameRepository')) return container diff --git a/config/env.ts b/config/env.ts index 68c02e4..7a03ee7 100644 --- a/config/env.ts +++ b/config/env.ts @@ -1,7 +1,7 @@ import { sessionStore } from '../view/stores/session.ts' import { Driver, DriverType } from '../src/Domain/Contracts.ts' -const fallback: Driver = { +export const driverDefault: Driver = { type: DriverType.supabase, config: { url: import.meta.env.VITE_SUPABASE_URL, @@ -9,4 +9,4 @@ const fallback: Driver = { } } -export const loadedDriver = (): Driver => sessionStore.state?.driver || fallback +export const loadedDriver = (): Driver => sessionStore.state?.driver || driverDefault From 383dd464ac2d0854eb24062ae3fdf7b9533f457e Mon Sep 17 00:00:00 2001 From: William Correa Date: Sat, 13 Apr 2024 11:33:49 -0300 Subject: [PATCH 05/17] feature: load driver from user session --- config/dependencies.ts | 31 ++++-- config/env.ts | 16 +-- src/Application/{ => Auth}/AuthService.ts | 9 +- src/Application/Auth/index.ts | 14 +++ src/Domain/Auth/Auth.ts | 20 ++-- src/Domain/Auth/AuthRepository.ts | 4 +- .../Driver/Supabase/SupabaseClientFactory.ts | 2 +- src/Infrastructure/Http/HttpAuthRepository.ts | 22 +++- .../Memory/InMemoryAuthRepository.ts | 48 ++++++-- .../Supabase/SupabaseAuthRepository.ts | 28 +++-- view/components/general/Async.tsx | 8 +- view/layouts/PublicLayout.tsx | 104 +++++++++--------- view/pages/auth/SignInPage.tsx | 9 +- view/providers/AppProvider.tsx | 24 ++-- view/providers/auth-manager-factory.ts | 11 +- view/stores/session.ts | 35 ++++-- 16 files changed, 239 insertions(+), 146 deletions(-) rename src/Application/{ => Auth}/AuthService.ts (62%) create mode 100644 src/Application/Auth/index.ts diff --git a/config/dependencies.ts b/config/dependencies.ts index 6e5dba1..b78eb4f 100644 --- a/config/dependencies.ts +++ b/config/dependencies.ts @@ -3,14 +3,14 @@ import { container } from 'tsyringe' import { Data, Driver, DriverResolver, DriverType } from '../src/Domain/Contracts.ts' -import { AuthService } from '../src/Application/AuthService.ts' +import { AuthService } from '../src/Application/Auth/AuthService.ts' import SupabaseAuthRepository from '../src/Infrastructure/Supabase/SupabaseAuthRepository.ts' import HttpAuthRepository from '../src/Infrastructure/Http/HttpAuthRepository.ts' import InMemoryGameRepository from '../src/Infrastructure/Memory/InMemoryGameRepository.ts' import SupabaseGameRepository from '../src/Infrastructure/Supabase/SupabaseGameRepository.ts' import InMemoryAuthRepository from '../src/Infrastructure/Memory/InMemoryAuthRepository.ts' -import { driverDefault, loadedDriver } from './env.ts' +import { getInheritDriver, getSessionDriver, isDevelopmentMode } from './env.ts' const binds: DriverResolver = { [DriverType.memory]: { @@ -28,21 +28,30 @@ const binds: DriverResolver = { } const factory = (token: string, driver?: Driver): [string, { useFactory: () => unknown }] => { - if (!driver) { - driver = loadedDriver() + const useFactory = () => { + if (!driver) { + driver = getSessionDriver() + } + const bind = binds[driver.type] + const maker = bind[token] + return maker(driver.config) } - const bind = binds[driver.type] - const useFactory = bind[token] - return [token, { useFactory: () => useFactory(driver.config) }] + return [token, { useFactory }] } export default function () { - // structure stuff + // [begin] structure stuff container.register('AuthService', { useClass: AuthService }) - if (import.meta.env.VITE_DEVELOPMENT_MODE === 'true') { - container.register(...factory('AuthRepository')) + + let authDriver: Driver = getInheritDriver() + if (isDevelopmentMode()) { + authDriver = { + type: DriverType.memory, + config: {} + } } - container.register(...factory('AuthRepository', driverDefault)) + container.register(...factory('AuthRepository', authDriver)) + // [end] structure stuff // game stuff container.register(...factory('GameRepository')) diff --git a/config/env.ts b/config/env.ts index 7a03ee7..18b9b17 100644 --- a/config/env.ts +++ b/config/env.ts @@ -1,12 +1,8 @@ -import { sessionStore } from '../view/stores/session.ts' -import { Driver, DriverType } from '../src/Domain/Contracts.ts' +import { getInitialSession, sessionStore } from '../view/stores/session.ts' +import { Driver } from '../src/Domain/Contracts.ts' -export const driverDefault: Driver = { - type: DriverType.supabase, - config: { - url: import.meta.env.VITE_SUPABASE_URL, - anonymousKey: import.meta.env.VITE_SUPABASE_ANON_KEY, - } -} +export const getInheritDriver = (): Driver => getInitialSession().driver -export const loadedDriver = (): Driver => sessionStore.state?.driver || driverDefault +export const getSessionDriver = (): Driver => sessionStore.state.driver + +export const isDevelopmentMode = (): boolean => import.meta.env.VITE_DEVELOPMENT_MODE === 'true' diff --git a/src/Application/AuthService.ts b/src/Application/Auth/AuthService.ts similarity index 62% rename from src/Application/AuthService.ts rename to src/Application/Auth/AuthService.ts index 5c49965..8192477 100644 --- a/src/Application/AuthService.ts +++ b/src/Application/Auth/AuthService.ts @@ -1,6 +1,7 @@ import { inject, injectable } from 'tsyringe' -import type AuthRepository from '../Domain/Auth/AuthRepository.ts' -import { Session } from '../../view/contracts.ts' +import type AuthRepository from '../../Domain/Auth/AuthRepository.ts' +import { Session } from '../../../view/contracts.ts' +import { Credential } from '../../Domain/Auth/Auth.ts' @injectable() export class AuthService { @@ -17,7 +18,7 @@ export class AuthService { return this.authRepository.signOut() } - async restore (): Promise { - return this.authRepository.restore() + async restore (context: Credential): Promise { + return this.authRepository.restore(context) } } diff --git a/src/Application/Auth/index.ts b/src/Application/Auth/index.ts new file mode 100644 index 0000000..bf60989 --- /dev/null +++ b/src/Application/Auth/index.ts @@ -0,0 +1,14 @@ +import { Credential } from '../../Domain/Auth/Auth.ts' + +export const credentialParser = (data: unknown): Credential => { + if (!data || typeof data === 'object') { + return undefined + } + const credential = data as Record + return { + token: String(credential.token), + refresh: String(credential.refresh), + expiresAt: Number(credential.expiresAt), + type: String(credential.type), + } +} diff --git a/src/Domain/Auth/Auth.ts b/src/Domain/Auth/Auth.ts index 461d765..1533d43 100644 --- a/src/Domain/Auth/Auth.ts +++ b/src/Domain/Auth/Auth.ts @@ -1,16 +1,18 @@ import { Driver } from '../Contracts.ts' +export type Credential = { + token: string + refresh: string + expiresAt: number | string | undefined + type: string +} | undefined + export type Session = { username: string - credential?: { - token: string - refresh: string - expiresAt: number | string | undefined - type: string - } - abilities?: string[] - driver?: Driver -} | null + credential: Credential + abilities: string[] + driver: Driver +} export interface AuthContract { signIn (username: string, password: string | null): Promise diff --git a/src/Domain/Auth/AuthRepository.ts b/src/Domain/Auth/AuthRepository.ts index 764325d..2f1ac5c 100644 --- a/src/Domain/Auth/AuthRepository.ts +++ b/src/Domain/Auth/AuthRepository.ts @@ -1,4 +1,4 @@ -import { Session } from './Auth.ts' +import { Credential, Session } from './Auth.ts' export default interface AuthRepository { signInWithOtp (email: string): Promise @@ -7,5 +7,5 @@ export default interface AuthRepository { signOut (): Promise - restore (): Promise + restore (context: Credential): Promise } diff --git a/src/Infrastructure/Driver/Supabase/SupabaseClientFactory.ts b/src/Infrastructure/Driver/Supabase/SupabaseClientFactory.ts index eb6cb1b..6e36962 100644 --- a/src/Infrastructure/Driver/Supabase/SupabaseClientFactory.ts +++ b/src/Infrastructure/Driver/Supabase/SupabaseClientFactory.ts @@ -12,7 +12,7 @@ export default class SupabaseClientFactory { } return client } catch (error) { - const message = 'Error creating Supabase client' + const message = 'Error making a new Supabase client on SupabaseClientFactory' console.error(message, ': ', error) throw new Error(message) } diff --git a/src/Infrastructure/Http/HttpAuthRepository.ts b/src/Infrastructure/Http/HttpAuthRepository.ts index 68a38cb..2c00cd1 100644 --- a/src/Infrastructure/Http/HttpAuthRepository.ts +++ b/src/Infrastructure/Http/HttpAuthRepository.ts @@ -1,5 +1,5 @@ import AuthRepository from '../../Domain/Auth/AuthRepository.ts' -import { Data, Status } from '../../Domain/Contracts.ts' +import { Data, DriverType, Status } from '../../Domain/Contracts.ts' import { HttpClientDriverContract } from '../Driver/Http/Contracts.ts' import HttpClientFactory from '../Driver/Http/HttpClientFactory.ts' import { Session } from '../../Domain/Auth/Auth.ts' @@ -24,7 +24,12 @@ export default class HttpAuthRepository implements AuthRepository { const user = data.user as Data return { username: user?.username as string, - abilities: [] + credential: undefined, + abilities: [], + driver: { + type: DriverType.http, + config: {} + } } } @@ -44,7 +49,11 @@ export default class HttpAuthRepository implements AuthRepository { expiresAt: credential?.expiresAt as string, type: credential?.type as string, }, - abilities: [] + abilities: [], + driver: { + type: DriverType.http, + config: {} + } } } @@ -62,7 +71,12 @@ export default class HttpAuthRepository implements AuthRepository { const user = data.user as Data return { username: user?.username as string, - abilities: [] + credential: undefined, + abilities: [], + driver: { + type: DriverType.http, + config: {} + } } } } diff --git a/src/Infrastructure/Memory/InMemoryAuthRepository.ts b/src/Infrastructure/Memory/InMemoryAuthRepository.ts index cab8345..7f34021 100644 --- a/src/Infrastructure/Memory/InMemoryAuthRepository.ts +++ b/src/Infrastructure/Memory/InMemoryAuthRepository.ts @@ -1,31 +1,59 @@ import AuthRepository from '../../Domain/Auth/AuthRepository.ts' -import { Session } from '../../Domain/Auth/Auth.ts' +import { Credential, Session } from '../../Domain/Auth/Auth.ts' import InMemoryRepository from '../Driver/Memory/InMemoryRepository.ts' +import { DriverType } from '../../Domain/Contracts.ts' +import { credentialParser } from '../../Application/Auth' export default class InMemoryAuthRepository extends InMemoryRepository implements AuthRepository { - restore (): Promise { - return this.promisify({ - username: 'memory', - abilities: [] - }) + restore (context: Credential): Promise { + const session: Session = { + username: '', + abilities: [], + credential: undefined, + driver: { + type: DriverType.memory, + config: {} + } + } + if (context) { + session.username = 'user@memory' + session.credential = credentialParser(context) + } + return this.promisify(session) } signIn (username: string, password: string): Promise { return this.promisify({ - username: username + ':' + password, - abilities: [] + username: username, + abilities: [], + credential: { + token: 'string', + refresh: 'string', + expiresAt: 'number | string | undefined', + type: 'string', + }, + driver: { + type: DriverType.memory, + config: { + password + } + } }) } signInWithOtp (username: string): Promise { return this.promisify({ username: username, - abilities: [] + abilities: [], + credential: undefined, + driver: { + type: DriverType.memory, + config: {} + } }) } signOut (): Promise { return Promise.resolve(true) } - } diff --git a/src/Infrastructure/Supabase/SupabaseAuthRepository.ts b/src/Infrastructure/Supabase/SupabaseAuthRepository.ts index 91c50a6..cef6674 100644 --- a/src/Infrastructure/Supabase/SupabaseAuthRepository.ts +++ b/src/Infrastructure/Supabase/SupabaseAuthRepository.ts @@ -5,6 +5,7 @@ import SupabaseRepository from '../Driver/Supabase/SupabaseRepository.ts' import AuthRepository from '../../Domain/Auth/AuthRepository.ts' import { Session } from '../../Domain/Auth/Auth.ts' import { Data } from '../../Domain/Contracts.ts' +import { getInheritDriver } from '../../../config/env.ts' export default class SupabaseAuthRepository extends SupabaseRepository implements AuthRepository { static build (config: Data) { @@ -19,7 +20,9 @@ export default class SupabaseAuthRepository extends SupabaseRepository implement } return { username: email, - abilities: [] + abilities: [], + credential: undefined, + driver: getInheritDriver() } } @@ -40,7 +43,8 @@ export default class SupabaseAuthRepository extends SupabaseRepository implement expiresAt: session?.expires_at, type: session?.token_type }, - abilities: [] + abilities: [], + driver: getInheritDriver() } } @@ -54,18 +58,22 @@ export default class SupabaseAuthRepository extends SupabaseRepository implement throw new Error(error.message) } const { session } = data - if (!session?.user?.email) { - throw new Error('Invalid session found') - } - return { - username: session?.user?.email, - credential: { + let username = '' + let credential + if (session?.user?.email) { + username = session?.user?.email + credential = { token: session?.access_token, refresh: session?.refresh_token, expiresAt: session?.expires_at, type: session?.token_type - }, - abilities: [] + } + } + return { + username: username, + credential: credential, + abilities: [], + driver: getInheritDriver() } } } diff --git a/view/components/general/Async.tsx b/view/components/general/Async.tsx index 5260612..f55e409 100644 --- a/view/components/general/Async.tsx +++ b/view/components/general/Async.tsx @@ -8,7 +8,7 @@ export type AsyncElementProps = { export type AsyncProps = { using: () => Promise - onResolve: (data: any) => void + onResolve?: (data: any) => void onReject?: (data: any) => void children: ReactNode | ReactNode[] } @@ -36,14 +36,12 @@ export function Async ({ using, onResolve, onReject, children }: AsyncProps) { fetched.current = true try { const data = await using() - onResolve(data) + onResolve && onResolve(data) setStatus(AsyncStatus.Resolved) } catch (e) { console.error(e) setStatus(AsyncStatus.Rejected) - if (onReject) { - onReject(e) - } + onReject && onReject(e) } } fetchData() diff --git a/view/layouts/PublicLayout.tsx b/view/layouts/PublicLayout.tsx index a70a9c0..0c52f86 100644 --- a/view/layouts/PublicLayout.tsx +++ b/view/layouts/PublicLayout.tsx @@ -1,7 +1,7 @@ import { Link, Outlet, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useApp } from '../hooks/useApp.ts' -import { useRunOnce } from '../hooks/useRunOnce.ts' +import { Async, AsyncStatus, On } from '../components/general/Async.tsx' export function PublicLayout () { const navigate = useNavigate() @@ -12,64 +12,64 @@ export function PublicLayout () { const { session, auth } = useApp() - useRunOnce({ - key: 'restore-session', - effect: () => auth.restore(), - }) - return ( -
- -
-
- -
-
+
    +
  • + + {t('play')} + +
  • +
+ { + session.credential ? + <> + {session.username} + + : + + } +
+ + +
+
+ +
+
-
-
- {t('copyright')} +
+
+ {t('copyright')} +
+
-
- + + ) } diff --git a/view/pages/auth/SignInPage.tsx b/view/pages/auth/SignInPage.tsx index 77b1871..a80bac9 100644 --- a/view/pages/auth/SignInPage.tsx +++ b/view/pages/auth/SignInPage.tsx @@ -2,8 +2,9 @@ import React, { useState } from 'react' import { useNavigate } from 'react-router-dom' import { useApp } from '../../hooks/useApp.ts' import { AlertDanger } from '../../components/general/Alert.tsx' -import { loadedDriver, Driver } from '../../../config/env.ts' import { Loading } from '../../components/general/Loading.tsx' +import { DriverType } from '../../../src/Domain/Contracts.ts' +import { isDevelopmentMode } from '../../../config/env.ts' export function SignInPage () { const { auth, session } = useApp() @@ -12,12 +13,12 @@ export function SignInPage () { const [error, setError] = useState('') const [loading, setLoading] = useState(false) - if (session) { + if (session.credential) { navigate('/dashboard') return } - const type = loadedDriver() === Mode.supabase ? 'otp' : 'password' + const type = session.driver.type === DriverType.supabase ? 'otp' : 'password' const handleSubmit = async (event: React.FormEvent) => { event.preventDefault() @@ -60,6 +61,7 @@ export function SignInPage () { name="username" aria-describedby="username_help" placeholder="Informe seu usuário" + defaultValue={isDevelopmentMode() ? 'root@phpcomrapadura.org' : ''} /> } diff --git a/view/providers/AppProvider.tsx b/view/providers/AppProvider.tsx index 2b846ba..724b66a 100644 --- a/view/providers/AppProvider.tsx +++ b/view/providers/AppProvider.tsx @@ -1,22 +1,28 @@ -import React, { useState } from 'react' +import { ReactNode, useState } from 'react' -import { AppContext } from '../components/app/AppContext' import { AppContextContract, Session } from '../contracts.ts' -import { authManagerFactory } from './auth-manager-factory.ts' + import dependencies from '../../config/dependencies.ts' -export function AppProvider ({ children }: { children: React.ReactNode }) { +import { getInitialSession, sessionStore } from '../stores/session.ts' +import { AppContext } from '../components/app/AppContext' +import { authManagerFactory } from './auth-manager-factory.ts' + +export function AppProvider ({ children }: { children: ReactNode }) { const container = dependencies() - const [session, setSession] = useState(null) + const [session, setSession] = useState(getInitialSession()) const updateAuthSession = (session: Session) => { - if (session?.credential) { - setSession(session) - } + setSession(session) + sessionStore.state.username = session.username + sessionStore.state.driver = session.driver + sessionStore.state.credential = session.credential + sessionStore.state.abilities = session.abilities } - const auth = authManagerFactory(updateAuthSession) + const context = sessionStore.state.credential + const auth = authManagerFactory(updateAuthSession, context) const value: AppContextContract = { container, session, auth } return {children} diff --git a/view/providers/auth-manager-factory.ts b/view/providers/auth-manager-factory.ts index 7a743f4..6536d9f 100644 --- a/view/providers/auth-manager-factory.ts +++ b/view/providers/auth-manager-factory.ts @@ -1,8 +1,10 @@ import { Session } from '../contracts.ts' import { container } from 'tsyringe' -import { AuthService } from '../../src/Application/AuthService.ts' +import { AuthService } from '../../src/Application/Auth/AuthService.ts' +import { getInitialSession } from '../stores/session.ts' +import { Credential } from '../../src/Domain/Auth/Auth.ts' -export function authManagerFactory (updateAuthSession: (session: Session) => void) { +export function authManagerFactory (updateAuthSession: (session: Session) => void, context: Credential) { const authService = container.resolve('AuthService') return { async signIn (username: string, password: string): Promise { @@ -15,12 +17,11 @@ export function authManagerFactory (updateAuthSession: (session: Session) => voi if (!done) { return false } - updateAuthSession(null) + updateAuthSession(getInitialSession()) return true }, async restore (): Promise { - const session = await authService.restore() - console.log(session) + const session = await authService.restore(context) updateAuthSession(session) return session } diff --git a/view/stores/session.ts b/view/stores/session.ts index 314f485..8398c36 100644 --- a/view/stores/session.ts +++ b/view/stores/session.ts @@ -1,20 +1,33 @@ import { createStore, Store } from '../store.ts' -import { Session } from '../../src/Domain/Auth/Auth.ts' -import { Driver } from '../../src/Domain/Contracts.ts' -export const sessionStore: Store = createStore({ - username: '', - credential: null, +import { Credential, Session } from '../../src/Domain/Auth/Auth.ts' +import { Driver, DriverType } from '../../src/Domain/Contracts.ts' +import { credentialParser } from '../../src/Application/Auth' + +const loadCredential = (): Credential => { + const credential = window.sessionStorage.getItem('credential') + return credential ? credentialParser(JSON.parse(credential)) : undefined +} + +export const getInitialSession = (): Session => ({ + username: 'guest', + credential: loadCredential(), abilities: [], - driver: window.sessionStorage.getItem('driver') ? - JSON.parse(window.sessionStorage.getItem('driver') as string) : - undefined, + driver: { + type: DriverType.supabase, + config: { + url: import.meta.env.VITE_SUPABASE_URL, + anonymousKey: import.meta.env.VITE_SUPABASE_ANON_KEY, + } + } }) -sessionStore.subscribe('driver', (driver: unknown) => { +export const sessionStore: Store = createStore(getInitialSession()) + +sessionStore.subscribe('credential', (driver: unknown) => { if (driver) { - window.sessionStorage.setItem('driver', JSON.stringify(driver as Driver)) + window.sessionStorage.setItem('credential', JSON.stringify(driver as Driver)) return } - window.sessionStorage.removeItem('driver') + window.sessionStorage.removeItem('credential') }) From a31dbcbc7916393e4e768bef1a0c4147bbb47c51 Mon Sep 17 00:00:00 2001 From: William Correa Date: Sat, 13 Apr 2024 13:32:23 -0300 Subject: [PATCH 06/17] feature: configure global loading --- config/i18n.ts | 3 +- src/Application/Auth/index.ts | 2 +- view/App.css | 7 + view/App.tsx | 17 +- .../{ProtectPage.tsx => AccreditedPage.tsx} | 8 +- view/components/game/GameImage.tsx | 6 +- view/components/game/GamePlaySession.tsx | 2 +- .../GamePlaySessionQuestion.tsx | 2 +- view/components/general/Conditional.tsx | 33 ++++ view/components/general/Match.tsx | 21 --- view/layouts/DashboardLayout.tsx | 82 ++++++++++ view/layouts/LayoutLoading.tsx | 32 ++++ view/layouts/PublicLayout.tsx | 110 +++++++------ ...shboardPage.tsx => DashboardIndexPage.tsx} | 3 +- view/pages/auth/SignInPage.tsx | 145 +++++++++--------- view/providers/auth-manager-factory.ts | 2 +- view/store.ts | 59 ++++--- view/stores/loading.ts | 3 + view/stores/session.ts | 14 +- 19 files changed, 350 insertions(+), 201 deletions(-) rename view/components/auth/{ProtectPage.tsx => AccreditedPage.tsx} (67%) create mode 100644 view/components/general/Conditional.tsx delete mode 100644 view/components/general/Match.tsx create mode 100644 view/layouts/DashboardLayout.tsx create mode 100644 view/layouts/LayoutLoading.tsx rename view/pages/{DashboardPage.tsx => DashboardIndexPage.tsx} (82%) create mode 100644 view/stores/loading.ts diff --git a/config/i18n.ts b/config/i18n.ts index bc34936..b4a0407 100644 --- a/config/i18n.ts +++ b/config/i18n.ts @@ -15,7 +15,8 @@ const resources = { play: 'Jogar', signIn: 'Entrar', myAccount: 'Minha Conta', - copyright: '© PHP com Rapadura' + copyright: '© PHP com Rapadura', + pending: 'Carregando ...', } }, pages: { diff --git a/src/Application/Auth/index.ts b/src/Application/Auth/index.ts index bf60989..719c19c 100644 --- a/src/Application/Auth/index.ts +++ b/src/Application/Auth/index.ts @@ -1,7 +1,7 @@ import { Credential } from '../../Domain/Auth/Auth.ts' export const credentialParser = (data: unknown): Credential => { - if (!data || typeof data === 'object') { + if (!data || typeof data !== 'object') { return undefined } const credential = data as Record diff --git a/view/App.css b/view/App.css index d7929d2..10776b2 100644 --- a/view/App.css +++ b/view/App.css @@ -57,3 +57,10 @@ .PublicLayout .GamePlaySessionQuestionUnanswered > .card > .card-body > .form-check p { line-height: 165%; } + +.LayoutLoading { + position: fixed; + top: 0; + z-index: 9999; + background-color: rgba(39, 39, 39, 0.7); +} diff --git a/view/App.tsx b/view/App.tsx index 91d38a7..ca818ca 100644 --- a/view/App.tsx +++ b/view/App.tsx @@ -8,7 +8,7 @@ import { PublicLayout } from './layouts/PublicLayout.tsx' import './App.css' // components -import { ProtectPage } from './components/auth/ProtectPage.tsx' +import { AccreditedPage } from './components/auth/AccreditedPage.tsx' // pages import { HomePage } from './pages/HomePage.tsx' // game @@ -16,12 +16,13 @@ import { GameWelcomePage } from './pages/game/GameWelcomePage.tsx' import { GamePlayPage } from './pages/game/GamePlayPage.tsx' import { GameEndPage } from './pages/game/GameEndPage.tsx' // session -import { DashboardPage } from './pages/DashboardPage.tsx' +import { DashboardLayout } from './layouts/DashboardLayout.tsx' import { SignInPage } from './pages/auth/SignInPage.tsx' import { WaitOneTimePassword } from './pages/auth/WaitOneTimePassword.tsx' import { useRunOnce } from './hooks/useRunOnce.ts' import { name } from '../config/i18n.ts' +import { DashboardIndexPage } from './pages/DashboardIndexPage.tsx' export default function App () { useRunOnce(() => document.title = name) @@ -58,13 +59,19 @@ export default function App () { element={} /> + } + element={} > } - /> + element={} + > + } + /> + diff --git a/view/components/auth/ProtectPage.tsx b/view/components/auth/AccreditedPage.tsx similarity index 67% rename from view/components/auth/ProtectPage.tsx rename to view/components/auth/AccreditedPage.tsx index 1dfa911..ca96988 100644 --- a/view/components/auth/ProtectPage.tsx +++ b/view/components/auth/AccreditedPage.tsx @@ -2,12 +2,14 @@ import { Navigate, Outlet, useLocation } from 'react-router-dom' import { useApp } from '../../hooks/useApp.ts' import { ReactNode } from 'react' -export function ProtectPage ({ children }: { children?: ReactNode | ReactNode[] }) { - const app = useApp() +export function AccreditedPage ({ children }: { children?: ReactNode | ReactNode[] }) { + const { session } = useApp() const from = useLocation() - if (app.session) { + + if (session.credential) { return children ? children : } + return +
+
+
child.props.value === condition) + } + const child = children as any + return child.props.value === condition +} diff --git a/view/components/general/Match.tsx b/view/components/general/Match.tsx deleted file mode 100644 index 55ad0a6..0000000 --- a/view/components/general/Match.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { ReactNode } from 'react' - -export type CaseProps = { - value: unknown - children: ReactNode | ReactNode[] -} - -export function Case (props: CaseProps) { - return props.children -} - -export type SwitchProps = { - condition: unknown - children: ReactNode[] -} - -export function Match (props: SwitchProps) { - const { condition, children } = props - return children.find((child: any) => child.props.value === condition) -} diff --git a/view/layouts/DashboardLayout.tsx b/view/layouts/DashboardLayout.tsx new file mode 100644 index 0000000..560a4e2 --- /dev/null +++ b/view/layouts/DashboardLayout.tsx @@ -0,0 +1,82 @@ +import { useTranslation } from 'react-i18next' +import { Link, Outlet, useNavigate } from 'react-router-dom' +import { useApp } from '../hooks/useApp.ts' +import { Async, AsyncStatus, On } from '../components/general/Async.tsx' + +export function DashboardLayout () { + const { t } = useTranslation( + 'default', + { keyPrefix: 'layouts.dashboard' } + ) + const navigate = useNavigate() + + const { session, auth } = useApp() + + const signOut = async () => { + const done = await auth.signOut() + if (done) { + navigate('/') + } + } + + return ( + auth.restore()}> + + +
+ + +
+
+ +
+
+ +
+
+ {t('copyright')} +
+
+
+
+
+ ) +} diff --git a/view/layouts/LayoutLoading.tsx b/view/layouts/LayoutLoading.tsx new file mode 100644 index 0000000..e222af0 --- /dev/null +++ b/view/layouts/LayoutLoading.tsx @@ -0,0 +1,32 @@ +import { Loading } from '../components/general/Loading.tsx' +import { If } from '../components/general/Conditional.tsx' +import { loadingStore } from '../stores/loading.ts' +import { useState } from 'react' +import { useBeforeUnload } from 'react-router-dom' + +export function LayoutLoading ({ label, initial = true }: { label: string, initial?: boolean }) { + const [loading, setLoading] = useState(initial) + + const id = loadingStore.subscribe('loading', (value: unknown) => { + if (value !== loading) { + setLoading(!!value) + } + }) + + useBeforeUnload(() => loadingStore.unsubscribe('loading', id)) + + return ( + +
loadingStore.state.loading = false} + > +
+
+ +
+
+
+
+ ) +} diff --git a/view/layouts/PublicLayout.tsx b/view/layouts/PublicLayout.tsx index 0c52f86..80317b9 100644 --- a/view/layouts/PublicLayout.tsx +++ b/view/layouts/PublicLayout.tsx @@ -2,6 +2,8 @@ import { Link, Outlet, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useApp } from '../hooks/useApp.ts' import { Async, AsyncStatus, On } from '../components/general/Async.tsx' +import { loadingStore } from '../stores/loading.ts' +import { LayoutLoading } from './LayoutLoading.tsx' export function PublicLayout () { const navigate = useNavigate() @@ -13,63 +15,69 @@ export function PublicLayout () { const { session, auth } = useApp() return ( - auth.restore()}> - + <> + + auth.restore()} + onResolve={() => loadingStore.state.loading = false} + > + -
- -
-
- -
-
+
+
+ +
+
-
-
- {t('copyright')} -
-
-
-
-
+
+
+ {t('copyright')} +
+
+
+ + + ) } diff --git a/view/pages/DashboardPage.tsx b/view/pages/DashboardIndexPage.tsx similarity index 82% rename from view/pages/DashboardPage.tsx rename to view/pages/DashboardIndexPage.tsx index 65c4817..ba0e02a 100644 --- a/view/pages/DashboardPage.tsx +++ b/view/pages/DashboardIndexPage.tsx @@ -1,11 +1,10 @@ import { useTranslation } from 'react-i18next' -export function DashboardPage () { +export function DashboardIndexPage () { const { t } = useTranslation( 'default', { keyPrefix: 'pages.dashboard' } ) - return (

{t('soon')} diff --git a/view/pages/auth/SignInPage.tsx b/view/pages/auth/SignInPage.tsx index a80bac9..2899955 100644 --- a/view/pages/auth/SignInPage.tsx +++ b/view/pages/auth/SignInPage.tsx @@ -1,29 +1,24 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { useApp } from '../../hooks/useApp.ts' import { AlertDanger } from '../../components/general/Alert.tsx' import { Loading } from '../../components/general/Loading.tsx' import { DriverType } from '../../../src/Domain/Contracts.ts' import { isDevelopmentMode } from '../../../config/env.ts' +import { loadingStore } from '../../stores/loading.ts' export function SignInPage () { const { auth, session } = useApp() const navigate = useNavigate() const [error, setError] = useState('') - const [loading, setLoading] = useState(false) - - if (session.credential) { - navigate('/dashboard') - return - } const type = session.driver.type === DriverType.supabase ? 'otp' : 'password' const handleSubmit = async (event: React.FormEvent) => { event.preventDefault() setError('') - setLoading(true) + loadingStore.state.loading = true const form = new FormData(event.currentTarget) const username = form.get('username') as string const password = form.get('password') as string @@ -35,84 +30,90 @@ export function SignInPage () { } navigate('/auth/otp') } catch (error) { - setError('Usuário e/ou senha inválidos') - return + setError('Usuário e/ou senha inválidos') + return } finally { - setLoading(false) + loadingStore.state.loading = false } } - return <> -

Entrar

+ useEffect(() => { + if (session.credential) { + navigate('/dashboard') + return + } + }, [session, navigate]) -
-
-
- - - - Utilize seu nome de usuário ou email - -
+ return session.credential ? + : +
+

Entrar

- { - type === 'password' && +
+
+ + Utilize seu nome de usuário ou email +
- } -
- -
- { - loading && ( - - ) - } - {error && ( - - {error} - - )} - -
+ { + type === 'password' && +
+ + +
+ } - +
+ +
+ { + error && ( + + {error} + + ) + } + +
+ +
} diff --git a/view/providers/auth-manager-factory.ts b/view/providers/auth-manager-factory.ts index 6536d9f..5979bb5 100644 --- a/view/providers/auth-manager-factory.ts +++ b/view/providers/auth-manager-factory.ts @@ -17,7 +17,7 @@ export function authManagerFactory (updateAuthSession: (session: Session) => voi if (!done) { return false } - updateAuthSession(getInitialSession()) + updateAuthSession(getInitialSession(false)) return true }, async restore (): Promise { diff --git a/view/store.ts b/view/store.ts index 5bf4ba6..ef37bff 100644 --- a/view/store.ts +++ b/view/store.ts @@ -2,57 +2,52 @@ export interface StoreState { [key: string | symbol]: unknown } +export type StoreCallback = (current?: unknown, previous?: unknown) => void + export interface Store { state: T - subscribe: (property: string, callback: (data: unknown) => void) => number - unsubscribe: (property: string, index: number) => void + subscribe: (property: keyof T, callback: StoreCallback) => number + unsubscribe: (property: keyof T, index: number) => void } export function createStore (initial: StoreState): Store { - type EventMap = { - [key: string]: Array<(data: unknown) => void> - }; + type Key = keyof T + type EventMap = Record> - const events: EventMap = {} + const EVENTS: EventMap = Object.create({}) - const publish = (eventName: string, data: unknown) => { - const subscribers = events[eventName] + const publish = (eventName: Key, current: unknown, previous: unknown) => { + const subscribers = EVENTS[eventName] if (subscribers) { - subscribers.forEach((callback) => { - callback(data) - }) + subscribers.forEach((callback: StoreCallback) => callback(current, previous)) } } const state = new Proxy(initial, { - get (target, property) { + get (target: StoreState, property) { return target[property] }, - set (target, property, value) { - if (target[property] === value) { - return true - } - target[property] = value - publish(property as string, value) + set (target: StoreState, property, current) { + const previous = target[property] + target[property] = current + publish(property as Key, current, previous) return true } }) - const subscribe = function (property: string, callback: (data: unknown) => void) { - if (!events[property]) { - events[property] = [] - } - return events[property].push(callback) - } - const unsubscribe = function (property: string, index: number) { - if (!events[property]) { - return - } - events[property].splice(index - 1, 1) - } return { state: state as T, - subscribe, - unsubscribe + subscribe: function (property: Key, callback: StoreCallback) { + if (!EVENTS[property]) { + EVENTS[property] = [] + } + return EVENTS[property].push(callback) + }, + unsubscribe: function (property: Key, id: number) { + if (!EVENTS[property]) { + return + } + EVENTS[property].splice(id - 1, 1) + } } } diff --git a/view/stores/loading.ts b/view/stores/loading.ts new file mode 100644 index 0000000..c7cfd84 --- /dev/null +++ b/view/stores/loading.ts @@ -0,0 +1,3 @@ +import { createStore, Store } from '../store.ts' + +export const loadingStore: Store<{ loading: boolean }> = createStore({ loading: false }) diff --git a/view/stores/session.ts b/view/stores/session.ts index 8398c36..e02e30f 100644 --- a/view/stores/session.ts +++ b/view/stores/session.ts @@ -1,7 +1,7 @@ import { createStore, Store } from '../store.ts' import { Credential, Session } from '../../src/Domain/Auth/Auth.ts' -import { Driver, DriverType } from '../../src/Domain/Contracts.ts' +import { DriverType } from '../../src/Domain/Contracts.ts' import { credentialParser } from '../../src/Application/Auth' const loadCredential = (): Credential => { @@ -9,9 +9,9 @@ const loadCredential = (): Credential => { return credential ? credentialParser(JSON.parse(credential)) : undefined } -export const getInitialSession = (): Session => ({ - username: 'guest', - credential: loadCredential(), +export const getInitialSession = (load: boolean = true): Session => ({ + username: '', + credential: load ? loadCredential() : undefined, abilities: [], driver: { type: DriverType.supabase, @@ -24,9 +24,9 @@ export const getInitialSession = (): Session => ({ export const sessionStore: Store = createStore(getInitialSession()) -sessionStore.subscribe('credential', (driver: unknown) => { - if (driver) { - window.sessionStorage.setItem('credential', JSON.stringify(driver as Driver)) +sessionStore.subscribe('credential', (credential: unknown) => { + if (credential) { + window.sessionStorage.setItem('credential', JSON.stringify(credential as Credential)) return } window.sessionStorage.removeItem('credential') From 97c108291204a8b65dc82e0d889a6cc55c249b96 Mon Sep 17 00:00:00 2001 From: William Correa Date: Sat, 13 Apr 2024 13:32:28 -0300 Subject: [PATCH 07/17] feature: configure global loading --- index.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/index.css b/index.css index 2d4631d..582e0ca 100644 --- a/index.css +++ b/index.css @@ -23,3 +23,11 @@ input[type="text"]:focus, input[type="password"]:focus { input[type="text"]::placeholder, input[type="password"]::placeholder { color: #c5c5c5; } + +.full-width { + width: 100vw; +} + +.full-height { + height: 100vh; +} From 2c4f9108ea2353fcae60835a04054559c505aa2a Mon Sep 17 00:00:00 2001 From: William Correa Date: Sat, 13 Apr 2024 15:00:52 -0300 Subject: [PATCH 08/17] feature: configure global loading --- config/i18n.ts | 7 +++ index.css | 15 ++--- view/App.css | 10 +++- view/App.tsx | 44 +++++++++----- view/components/auth/AccreditedPage.tsx | 18 ------ view/components/auth/CredentialChecker.tsx | 25 ++++++++ view/components/general/Conditional.tsx | 28 ++++++++- view/layouts/DashboardLayout.tsx | 57 ++++++++++++++++--- view/layouts/LayoutLoading.tsx | 2 +- .../pages/dashboard/DahboardMyAccountPage.tsx | 13 +++++ .../DashboardGamesPage.tsx} | 4 +- .../pages/dashboard/DashboardSettingsPage.tsx | 53 +++++++++++++++++ view/pages/{ => public}/auth/SignInPage.tsx | 26 +++------ .../{ => public}/auth/WaitOneTimePassword.tsx | 0 view/pages/{ => public}/game/GameEndPage.tsx | 2 +- view/pages/{ => public}/game/GamePlayPage.tsx | 14 ++--- .../{ => public}/game/GameWelcomePage.tsx | 14 ++--- 17 files changed, 242 insertions(+), 90 deletions(-) delete mode 100644 view/components/auth/AccreditedPage.tsx create mode 100644 view/components/auth/CredentialChecker.tsx create mode 100644 view/pages/dashboard/DahboardMyAccountPage.tsx rename view/pages/{DashboardIndexPage.tsx => dashboard/DashboardGamesPage.tsx} (68%) create mode 100644 view/pages/dashboard/DashboardSettingsPage.tsx rename view/pages/{ => public}/auth/SignInPage.tsx (83%) rename view/pages/{ => public}/auth/WaitOneTimePassword.tsx (100%) rename view/pages/{ => public}/game/GameEndPage.tsx (95%) rename view/pages/{ => public}/game/GamePlayPage.tsx (76%) rename view/pages/{ => public}/game/GameWelcomePage.tsx (71%) diff --git a/config/i18n.ts b/config/i18n.ts index b4a0407..bae3247 100644 --- a/config/i18n.ts +++ b/config/i18n.ts @@ -17,6 +17,13 @@ const resources = { myAccount: 'Minha Conta', copyright: '© PHP com Rapadura', pending: 'Carregando ...', + }, + dashboard: { + brand: name, + play: 'Jogar', + signOut: 'Sair', + myAccount: 'Minha Conta', + pending: 'Carregando ...', } }, pages: { diff --git a/index.css b/index.css index 582e0ca..9ca5e92 100644 --- a/index.css +++ b/index.css @@ -9,25 +9,26 @@ body { position: relative; } -input[type="text"], input[type="password"] { +input[type="text"].form-control, input[type="password"].form-control { background-color: #ffffff; color: #5c5c5c; } -input[type="text"]:focus, input[type="password"]:focus { +input[type="text"].form-control:focus, input[type="password"].form-control:focus { color: #5c5c5c; background-color: #ffffff; border: 1px solid #a8a8a8; } -input[type="text"]::placeholder, input[type="password"]::placeholder { +input[type="text"].form-control::placeholder, input[type="password"].form-control::placeholder { color: #c5c5c5; } -.full-width { - width: 100vw; +select.form-select { + background-color: #ffffff; + color: #5c5c5c; } -.full-height { - height: 100vh; +select.form-select { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23555' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); } diff --git a/view/App.css b/view/App.css index 10776b2..1995450 100644 --- a/view/App.css +++ b/view/App.css @@ -1,8 +1,8 @@ -.PublicLayout { +.PublicLayout, .DashboardLayout { padding-top: 82px; } -.PublicLayout nav { +.PublicLayout nav, .DashboardLayout nav { height: 82px; } @@ -10,6 +10,10 @@ padding: 30px 10px; } +.DashboardLayout main { + padding: 20px 10px; +} + .PublicLayout main > .container { min-height: calc(100vh - 198px); transition: height 0.3s; @@ -61,6 +65,8 @@ .LayoutLoading { position: fixed; top: 0; + width: 100vw; + height: 100vh; z-index: 9999; background-color: rgba(39, 39, 39, 0.7); } diff --git a/view/App.tsx b/view/App.tsx index ca818ca..18afb21 100644 --- a/view/App.tsx +++ b/view/App.tsx @@ -8,21 +8,23 @@ import { PublicLayout } from './layouts/PublicLayout.tsx' import './App.css' // components -import { AccreditedPage } from './components/auth/AccreditedPage.tsx' +import { CredentialChecker } from './components/auth/CredentialChecker.tsx' // pages import { HomePage } from './pages/HomePage.tsx' // game -import { GameWelcomePage } from './pages/game/GameWelcomePage.tsx' -import { GamePlayPage } from './pages/game/GamePlayPage.tsx' -import { GameEndPage } from './pages/game/GameEndPage.tsx' +import { GameWelcomePage } from './pages/public/game/GameWelcomePage.tsx' +import { GamePlayPage } from './pages/public/game/GamePlayPage.tsx' +import { GameEndPage } from './pages/public/game/GameEndPage.tsx' // session import { DashboardLayout } from './layouts/DashboardLayout.tsx' -import { SignInPage } from './pages/auth/SignInPage.tsx' -import { WaitOneTimePassword } from './pages/auth/WaitOneTimePassword.tsx' +import { SignInPage } from './pages/public/auth/SignInPage.tsx' +import { WaitOneTimePassword } from './pages/public/auth/WaitOneTimePassword.tsx' import { useRunOnce } from './hooks/useRunOnce.ts' import { name } from '../config/i18n.ts' -import { DashboardIndexPage } from './pages/DashboardIndexPage.tsx' +import { DashboardGamesPage } from './pages/dashboard/DashboardGamesPage.tsx' +import { DashboardSettingsPage } from './pages/dashboard/DashboardSettingsPage.tsx' +import { DashboardMyAccountPage } from './pages/dashboard/DahboardMyAccountPage.tsx' export default function App () { useRunOnce(() => document.title = name) @@ -50,26 +52,38 @@ export default function App () { path="/games/:id/end" element={} /> - } - /> + }> + } + /> + } /> - } - > + }> } > } + element={} + /> + } + /> + } + /> + } /> diff --git a/view/components/auth/AccreditedPage.tsx b/view/components/auth/AccreditedPage.tsx deleted file mode 100644 index ca96988..0000000 --- a/view/components/auth/AccreditedPage.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Navigate, Outlet, useLocation } from 'react-router-dom' -import { useApp } from '../../hooks/useApp.ts' -import { ReactNode } from 'react' - -export function AccreditedPage ({ children }: { children?: ReactNode | ReactNode[] }) { - const { session } = useApp() - const from = useLocation() - - if (session.credential) { - return children ? children : - } - - return -} diff --git a/view/components/auth/CredentialChecker.tsx b/view/components/auth/CredentialChecker.tsx new file mode 100644 index 0000000..c8f081e --- /dev/null +++ b/view/components/auth/CredentialChecker.tsx @@ -0,0 +1,25 @@ +import { Navigate, Outlet, useLocation } from 'react-router-dom' +import { useApp } from '../../hooks/useApp.ts' +import { ReactNode } from 'react' + +export type CredentialRoutesProps = { + children?: ReactNode | ReactNode[] + withCredential: boolean +} + +export function CredentialChecker ({ children, withCredential }: CredentialRoutesProps) { + const { session } = useApp() + const from = useLocation() + + const can = withCredential ? !!session.credential : !session.credential + if (can) { + return children ? children : + } + + const route = withCredential ? '/auth/sign-in' : '/dashboard' + return +} diff --git a/view/components/general/Conditional.tsx b/view/components/general/Conditional.tsx index 46d26b5..c314eed 100644 --- a/view/components/general/Conditional.tsx +++ b/view/components/general/Conditional.tsx @@ -13,6 +13,7 @@ export function Case (props: CaseProps) { export type ConditionalProps = { condition: unknown children: ReactNode | ReactNode[] + defaultValue?: unknown } export function If (props: ConditionalProps) { @@ -20,14 +21,35 @@ export function If (props: ConditionalProps) { return condition ? children : undefined } +const compare = (child: any, condition: unknown, defaultValue: unknown) => { + return child.props.value === condition || child.props.value === defaultValue +} + export function Match (props: ConditionalProps) { - const { condition, children } = props + const { condition, children, defaultValue = undefined } = props if (!children) { return undefined } if (Array.isArray(children)) { - return children.find((child: any) => child.props.value === condition) + return children.filter((child: any) => compare(child, condition, defaultValue)) } const child = children as any - return child.props.value === condition + return compare(child, condition, defaultValue) ? child : undefined +} + +export function Switch (props: ConditionalProps) { + const { condition, children, defaultValue = undefined } = props + if (!children) { + return undefined + } + if (!Array.isArray(children)) { + throw new Error('Conditional/Switch: At least two Case components are required') + } + const duplicates = children + .filter((e1: any, i1) => children + .some((e2: any, i2) => e2.props.value === e1.props.value && i2 !== i1)) + if (duplicates.length > 0) { + throw new Error('Conditional/Switch: Duplicate Case values are not allowed') + } + return children.find((child: any) => compare(child, condition, defaultValue)) } diff --git a/view/layouts/DashboardLayout.tsx b/view/layouts/DashboardLayout.tsx index 560a4e2..56b9ca2 100644 --- a/view/layouts/DashboardLayout.tsx +++ b/view/layouts/DashboardLayout.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next' -import { Link, Outlet, useNavigate } from 'react-router-dom' +import { Link, NavLink, Outlet, useNavigate } from 'react-router-dom' import { useApp } from '../hooks/useApp.ts' import { Async, AsyncStatus, On } from '../components/general/Async.tsx' @@ -36,7 +36,7 @@ export function DashboardLayout () {
  • {t('play')} @@ -66,15 +66,54 @@ export function DashboardLayout () {
    - +
    +
      +
    • + + Meus Jogos + +
    • +
    • + + Minha Conta + +
    • +
    • + + Configurações + +
    • +
    +
    + +
    +
    - -
    -
    - {t('copyright')} -
    -
diff --git a/view/layouts/LayoutLoading.tsx b/view/layouts/LayoutLoading.tsx index e222af0..cd9c9c1 100644 --- a/view/layouts/LayoutLoading.tsx +++ b/view/layouts/LayoutLoading.tsx @@ -18,7 +18,7 @@ export function LayoutLoading ({ label, initial = true }: { label: string, initi return (
loadingStore.state.loading = false} >
diff --git a/view/pages/dashboard/DahboardMyAccountPage.tsx b/view/pages/dashboard/DahboardMyAccountPage.tsx new file mode 100644 index 0000000..63feb95 --- /dev/null +++ b/view/pages/dashboard/DahboardMyAccountPage.tsx @@ -0,0 +1,13 @@ +import { useTranslation } from 'react-i18next' + +export function DashboardMyAccountPage () { + const { t } = useTranslation( + 'default', + { keyPrefix: 'pages.dashboard' } + ) + return ( +

+ DashboardMyAccountPage: {t('soon')} +

+ ) +} diff --git a/view/pages/DashboardIndexPage.tsx b/view/pages/dashboard/DashboardGamesPage.tsx similarity index 68% rename from view/pages/DashboardIndexPage.tsx rename to view/pages/dashboard/DashboardGamesPage.tsx index ba0e02a..4d38b82 100644 --- a/view/pages/DashboardIndexPage.tsx +++ b/view/pages/dashboard/DashboardGamesPage.tsx @@ -1,13 +1,13 @@ import { useTranslation } from 'react-i18next' -export function DashboardIndexPage () { +export function DashboardGamesPage () { const { t } = useTranslation( 'default', { keyPrefix: 'pages.dashboard' } ) return (

- {t('soon')} + DashboardGamesPage: {t('soon')}

) } diff --git a/view/pages/dashboard/DashboardSettingsPage.tsx b/view/pages/dashboard/DashboardSettingsPage.tsx new file mode 100644 index 0000000..3ada2a2 --- /dev/null +++ b/view/pages/dashboard/DashboardSettingsPage.tsx @@ -0,0 +1,53 @@ +import { useTranslation } from 'react-i18next' +import { useId, useState } from 'react' +import { DriverType } from '../../../src/Domain/Contracts.ts' +import { Case, Switch } from '../../components/general/Conditional.tsx' +import { useApp } from '../../hooks/useApp.ts' + +export function DashboardSettingsPage () { + const { t } = useTranslation( + 'default', + { keyPrefix: 'pages.dashboard' } + ) + const driverTypeId = useId() + const { session } = useApp() + + const [driverType, setDriverType] = useState(session.driver.type) + + return ( +
+
+ + +
+
+ + + HTTP + + + Memory + + + Supabase + + +
+
+ ) +} diff --git a/view/pages/auth/SignInPage.tsx b/view/pages/public/auth/SignInPage.tsx similarity index 83% rename from view/pages/auth/SignInPage.tsx rename to view/pages/public/auth/SignInPage.tsx index 2899955..433f1b8 100644 --- a/view/pages/auth/SignInPage.tsx +++ b/view/pages/public/auth/SignInPage.tsx @@ -1,11 +1,10 @@ -import React, { useEffect, useState } from 'react' +import React, { useState } from 'react' import { useNavigate } from 'react-router-dom' -import { useApp } from '../../hooks/useApp.ts' -import { AlertDanger } from '../../components/general/Alert.tsx' -import { Loading } from '../../components/general/Loading.tsx' -import { DriverType } from '../../../src/Domain/Contracts.ts' -import { isDevelopmentMode } from '../../../config/env.ts' -import { loadingStore } from '../../stores/loading.ts' +import { useApp } from '../../../hooks/useApp.ts' +import { AlertDanger } from '../../../components/general/Alert.tsx' +import { DriverType } from '../../../../src/Domain/Contracts.ts' +import { isDevelopmentMode } from '../../../../config/env.ts' +import { loadingStore } from '../../../stores/loading.ts' export function SignInPage () { const { auth, session } = useApp() @@ -37,18 +36,9 @@ export function SignInPage () { } } - useEffect(() => { - if (session.credential) { - navigate('/dashboard') - return - } - }, [session, navigate]) - - return session.credential ? - : + return (

Entrar

-
@@ -114,6 +104,6 @@ export function SignInPage () { }
-
+ ) } diff --git a/view/pages/auth/WaitOneTimePassword.tsx b/view/pages/public/auth/WaitOneTimePassword.tsx similarity index 100% rename from view/pages/auth/WaitOneTimePassword.tsx rename to view/pages/public/auth/WaitOneTimePassword.tsx diff --git a/view/pages/game/GameEndPage.tsx b/view/pages/public/game/GameEndPage.tsx similarity index 95% rename from view/pages/game/GameEndPage.tsx rename to view/pages/public/game/GameEndPage.tsx index ec96677..5b3cdaf 100644 --- a/view/pages/game/GameEndPage.tsx +++ b/view/pages/public/game/GameEndPage.tsx @@ -1,6 +1,6 @@ import { useNavigate, useParams } from 'react-router-dom' import { useTranslation } from 'react-i18next' -import { Done } from '../../components/game/GameImage.tsx' +import { Done } from '../../../components/game/GameImage.tsx' import { useEffect, useState } from 'react' export function GameEndPage () { diff --git a/view/pages/game/GamePlayPage.tsx b/view/pages/public/game/GamePlayPage.tsx similarity index 76% rename from view/pages/game/GamePlayPage.tsx rename to view/pages/public/game/GamePlayPage.tsx index b9dbdd0..4fb3c4f 100644 --- a/view/pages/game/GamePlayPage.tsx +++ b/view/pages/public/game/GamePlayPage.tsx @@ -2,14 +2,14 @@ import { useNavigate, useParams } from 'react-router-dom' import { useState } from 'react' import { useTranslation } from 'react-i18next' -import GameRepository from '../../../src/Domain/Game/GameRepository.ts' -import Game from '../../../src/Domain/Game/Game.ts' +import GameRepository from '../../../../src/Domain/Game/GameRepository.ts' +import Game from '../../../../src/Domain/Game/Game.ts' -import { useApp } from '../../hooks/useApp.ts' -import { Async, AsyncStatus, On } from '../../components/general/Async.tsx' -import { Loading } from '../../components/general/Loading.tsx' -import { AlertWarning } from '../../components/general/Alert.tsx' -import { GamePlaySession } from '../../components/game/GamePlaySession.tsx' +import { useApp } from '../../../hooks/useApp.ts' +import { Async, AsyncStatus, On } from '../../../components/general/Async.tsx' +import { Loading } from '../../../components/general/Loading.tsx' +import { AlertWarning } from '../../../components/general/Alert.tsx' +import { GamePlaySession } from '../../../components/game/GamePlaySession.tsx' export function GamePlayPage () { const params = useParams() diff --git a/view/pages/game/GameWelcomePage.tsx b/view/pages/public/game/GameWelcomePage.tsx similarity index 71% rename from view/pages/game/GameWelcomePage.tsx rename to view/pages/public/game/GameWelcomePage.tsx index 954dcef..08eee7f 100644 --- a/view/pages/game/GameWelcomePage.tsx +++ b/view/pages/public/game/GameWelcomePage.tsx @@ -1,15 +1,15 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' -import GameRepository from '../../../src/Domain/Game/GameRepository.ts' -import Game from '../../../src/Domain/Game/Game.ts' +import GameRepository from '../../../../src/Domain/Game/GameRepository.ts' +import Game from '../../../../src/Domain/Game/Game.ts' -import { useApp } from '../../hooks/useApp.ts' -import { Async, AsyncStatus, On } from '../../components/general/Async.tsx' -import { Loading } from '../../components/general/Loading.tsx' -import { AlertWarning } from '../../components/general/Alert.tsx' +import { useApp } from '../../../hooks/useApp.ts' +import { Async, AsyncStatus, On } from '../../../components/general/Async.tsx' +import { Loading } from '../../../components/general/Loading.tsx' +import { AlertWarning } from '../../../components/general/Alert.tsx' -import { GameList } from '../../components/game/GameList.tsx' +import { GameList } from '../../../components/game/GameList.tsx' export function GameWelcomePage () { const { t } = useTranslation( From 9028b362e1d51e5d66d7e4bf0539cfe71711a5d1 Mon Sep 17 00:00:00 2001 From: William Correa Date: Sat, 13 Apr 2024 15:52:35 -0300 Subject: [PATCH 09/17] feature: review components navigation --- config/i18n.ts | 6 + view/App.css | 2 +- view/App.tsx | 4 +- view/components/auth/CredentialChecker.tsx | 10 +- .../GamePlaySessionQuestion.tsx | 50 ++++--- view/components/general/Async.tsx | 53 +++---- view/layouts/DashboardLayout.tsx | 136 ++++++------------ view/layouts/LayoutLoading.tsx | 4 +- view/layouts/LayoutNavbar.tsx | 42 ++++++ view/layouts/PublicLayout.tsx | 63 +++----- .../layouts/dashboard/DashboardNavigation.tsx | 59 ++++++++ .../pages/dashboard/DashboardSettingsPage.tsx | 25 ++-- view/store.ts | 6 +- 13 files changed, 249 insertions(+), 211 deletions(-) create mode 100644 view/layouts/LayoutNavbar.tsx create mode 100644 view/layouts/dashboard/DashboardNavigation.tsx diff --git a/config/i18n.ts b/config/i18n.ts index bae3247..7e871f8 100644 --- a/config/i18n.ts +++ b/config/i18n.ts @@ -24,6 +24,12 @@ const resources = { signOut: 'Sair', myAccount: 'Minha Conta', pending: 'Carregando ...', + navigation: { + index: 'Meus Jogos', + games: 'Meus Jogos', + account: 'Minha Conta', + settings: 'Configurações' + } } }, pages: { diff --git a/view/App.css b/view/App.css index 1995450..fa970e5 100644 --- a/view/App.css +++ b/view/App.css @@ -68,5 +68,5 @@ width: 100vw; height: 100vh; z-index: 9999; - background-color: rgba(39, 39, 39, 0.7); + background-color: rgba(73, 73, 73, 0.7); } diff --git a/view/App.tsx b/view/App.tsx index 18afb21..fd69e26 100644 --- a/view/App.tsx +++ b/view/App.tsx @@ -52,7 +52,7 @@ export default function App () { path="/games/:id/end" element={} /> - }> + }> } @@ -64,7 +64,7 @@ export default function App () { /> - }> + }> } diff --git a/view/components/auth/CredentialChecker.tsx b/view/components/auth/CredentialChecker.tsx index c8f081e..132401d 100644 --- a/view/components/auth/CredentialChecker.tsx +++ b/view/components/auth/CredentialChecker.tsx @@ -2,21 +2,21 @@ import { Navigate, Outlet, useLocation } from 'react-router-dom' import { useApp } from '../../hooks/useApp.ts' import { ReactNode } from 'react' -export type CredentialRoutesProps = { +export type CredentialCheckerProps = { children?: ReactNode | ReactNode[] - withCredential: boolean + reverse?: boolean } -export function CredentialChecker ({ children, withCredential }: CredentialRoutesProps) { +export function CredentialChecker ({ children, reverse = false }: CredentialCheckerProps) { const { session } = useApp() const from = useLocation() - const can = withCredential ? !!session.credential : !session.credential + const can = reverse ? !session.credential : !!session.credential if (can) { return children ? children : } - const route = withCredential ? '/auth/sign-in' : '/dashboard' + const route: string = reverse ? '/dashboard' : '/auth/sign-in' return - - - - - - - - - - - - - - - + + + + + + + + + + + + + + ) } diff --git a/view/components/general/Async.tsx b/view/components/general/Async.tsx index f55e409..a8aa2fc 100644 --- a/view/components/general/Async.tsx +++ b/view/components/general/Async.tsx @@ -1,5 +1,5 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { ReactNode, useEffect, useRef, useState } from 'react' +import { ReactNode, useState } from 'react' +import { useRunOnce } from '../../hooks/useRunOnce.ts' export type AsyncElementProps = { children: ReactNode | ReactNode[] @@ -7,9 +7,10 @@ export type AsyncElementProps = { } export type AsyncProps = { - using: () => Promise - onResolve?: (data: any) => void - onReject?: (data: any) => void + using: () => Promise + onResolve?: (data: unknown) => void + onReject?: (data: unknown) => void + onFinally?: () => void children: ReactNode | ReactNode[] } @@ -17,39 +18,41 @@ export type AsyncProps = { export enum AsyncStatus { Pending = 'Pending', Resolved = 'Resolved', - Rejected = 'Rejected' + Rejected = 'Rejected', } export function On ({ children }: AsyncElementProps) { return children } -export function Async ({ using, onResolve, onReject, children }: AsyncProps) { - const fetched = useRef(false) +export function Async (props: AsyncProps) { + const { + using, + onResolve, + onReject, + onFinally, + children + } = props const [status, setStatus] = useState(AsyncStatus.Pending) - useEffect(() => { - if (fetched.current) { - return + useRunOnce(async () => { + try { + const data = await using() + onResolve && onResolve(data) + setStatus(AsyncStatus.Resolved) + } catch (error) { + console.error('Async detected an error: ', error) + setStatus(AsyncStatus.Rejected) + onReject && onReject(error) + } finally { + onFinally && onFinally() } - const fetchData = async () => { - fetched.current = true - try { - const data = await using() - onResolve && onResolve(data) - setStatus(AsyncStatus.Resolved) - } catch (e) { - console.error(e) - setStatus(AsyncStatus.Rejected) - onReject && onReject(e) - } - } - fetchData() - }, [fetched, using, onReject, onResolve]) + }) if (!children) { return null } + /* eslint-disable @typescript-eslint/no-explicit-any */ if (Array.isArray(children)) { return children.filter((child: any) => child.props.status === status) } diff --git a/view/layouts/DashboardLayout.tsx b/view/layouts/DashboardLayout.tsx index 56b9ca2..00431e0 100644 --- a/view/layouts/DashboardLayout.tsx +++ b/view/layouts/DashboardLayout.tsx @@ -1,7 +1,12 @@ import { useTranslation } from 'react-i18next' -import { Link, NavLink, Outlet, useNavigate } from 'react-router-dom' +import { useNavigate } from 'react-router-dom' import { useApp } from '../hooks/useApp.ts' import { Async, AsyncStatus, On } from '../components/general/Async.tsx' +import { Case } from '../components/general/Conditional.tsx' +import { loadingStore } from '../stores/loading.ts' +import { LayoutLoading } from './LayoutLoading.tsx' +import { LayoutNavbar } from './LayoutNavbar.tsx' +import { DashboardNavigation } from './dashboard/DashboardNavigation.tsx' export function DashboardLayout () { const { t } = useTranslation( @@ -20,102 +25,43 @@ export function DashboardLayout () { } return ( - auth.restore()}> - + <> + + auth.restore()} + onFinally={() => loadingStore.state.loading = false} + > + -
- - -
-
-
-
    + + + {session.username} +
-
- -
+ {t('signOut')} + + + + + + + +
+
+
-
-
-
-
-
+ +
+ + + ) } diff --git a/view/layouts/LayoutLoading.tsx b/view/layouts/LayoutLoading.tsx index cd9c9c1..70e4e8b 100644 --- a/view/layouts/LayoutLoading.tsx +++ b/view/layouts/LayoutLoading.tsx @@ -7,13 +7,13 @@ import { useBeforeUnload } from 'react-router-dom' export function LayoutLoading ({ label, initial = true }: { label: string, initial?: boolean }) { const [loading, setLoading] = useState(initial) - const id = loadingStore.subscribe('loading', (value: unknown) => { + const subscriptionId = loadingStore.subscribe('loading', (value: unknown) => { if (value !== loading) { setLoading(!!value) } }) - useBeforeUnload(() => loadingStore.unsubscribe('loading', id)) + useBeforeUnload(() => loadingStore.unsubscribe('loading', subscriptionId)) return ( diff --git a/view/layouts/LayoutNavbar.tsx b/view/layouts/LayoutNavbar.tsx new file mode 100644 index 0000000..c1101f5 --- /dev/null +++ b/view/layouts/LayoutNavbar.tsx @@ -0,0 +1,42 @@ +import { ReactElement } from 'react' +import { Link } from 'react-router-dom' +import { useTranslation } from 'react-i18next' + +export type LayoutNavbarProps = { + children: ReactElement[] + condition: unknown +} + +export function LayoutNavbar (props: LayoutNavbarProps) { + const { children, condition } = props + + const { t } = useTranslation( + 'default', + { keyPrefix: 'layouts.public' } + ) + + return ( + + ) +} diff --git a/view/layouts/PublicLayout.tsx b/view/layouts/PublicLayout.tsx index 80317b9..f8dce10 100644 --- a/view/layouts/PublicLayout.tsx +++ b/view/layouts/PublicLayout.tsx @@ -1,9 +1,11 @@ -import { Link, Outlet, useNavigate } from 'react-router-dom' +import { Outlet, useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useApp } from '../hooks/useApp.ts' import { Async, AsyncStatus, On } from '../components/general/Async.tsx' import { loadingStore } from '../stores/loading.ts' import { LayoutLoading } from './LayoutLoading.tsx' +import { Case } from '../components/general/Conditional.tsx' +import { LayoutNavbar } from './LayoutNavbar.tsx' export function PublicLayout () { const navigate = useNavigate() @@ -19,50 +21,31 @@ export function PublicLayout () { auth.restore()} - onResolve={() => loadingStore.state.loading = false} + onFinally={() => loadingStore.state.loading = false} >
- + + + {session.username} + + + + + +
diff --git a/view/layouts/dashboard/DashboardNavigation.tsx b/view/layouts/dashboard/DashboardNavigation.tsx new file mode 100644 index 0000000..a640343 --- /dev/null +++ b/view/layouts/dashboard/DashboardNavigation.tsx @@ -0,0 +1,59 @@ +import { NavLink, Outlet } from 'react-router-dom' +import { useTranslation } from 'react-i18next' + +export function DashboardNavigation () { + const { t } = useTranslation( + 'default', + { keyPrefix: 'layouts.dashboard' } + ) + + return ( +
+
    +
  • + + {t('navigation.index')} + +
  • +
  • + + {t('navigation.account')} + +
  • +
  • + + {t('navigation.settings')} + +
  • +
+
+ +
+
+ ) +} diff --git a/view/pages/dashboard/DashboardSettingsPage.tsx b/view/pages/dashboard/DashboardSettingsPage.tsx index 3ada2a2..5e12a38 100644 --- a/view/pages/dashboard/DashboardSettingsPage.tsx +++ b/view/pages/dashboard/DashboardSettingsPage.tsx @@ -35,19 +35,20 @@ export function DashboardSettingsPage () {
-
- - + + + +
HTTP - - - Memory - - - Supabase - - -
+
+ + + Memory + + + Supabase + + ) } diff --git a/view/store.ts b/view/store.ts index ef37bff..293b874 100644 --- a/view/store.ts +++ b/view/store.ts @@ -7,7 +7,7 @@ export type StoreCallback = (current?: unknown, previous?: unknown) => void export interface Store { state: T subscribe: (property: keyof T, callback: StoreCallback) => number - unsubscribe: (property: keyof T, index: number) => void + unsubscribe: (property: keyof T, subscriptionId: number) => void } export function createStore (initial: StoreState): Store { @@ -43,11 +43,11 @@ export function createStore (initial: StoreState): Store { } return EVENTS[property].push(callback) }, - unsubscribe: function (property: Key, id: number) { + unsubscribe: function (property: Key, subscriptionId: number) { if (!EVENTS[property]) { return } - EVENTS[property].splice(id - 1, 1) + EVENTS[property].splice(subscriptionId - 1, 1) } } } From 8829a91ac521e7642124af73aaac52d629b7a23b Mon Sep 17 00:00:00 2001 From: William Correa Date: Sat, 13 Apr 2024 16:05:50 -0300 Subject: [PATCH 10/17] feature: review components navigation --- view/layouts/DashboardLayout.tsx | 39 ++------ view/layouts/LayoutNavbar.tsx | 42 --------- view/layouts/PublicLayout.tsx | 32 ++----- view/layouts/{ => general}/LayoutLoading.tsx | 6 +- view/layouts/general/LayoutNavbar.tsx | 96 ++++++++++++++++++++ 5 files changed, 115 insertions(+), 100 deletions(-) delete mode 100644 view/layouts/LayoutNavbar.tsx rename view/layouts/{ => general}/LayoutLoading.tsx (82%) create mode 100644 view/layouts/general/LayoutNavbar.tsx diff --git a/view/layouts/DashboardLayout.tsx b/view/layouts/DashboardLayout.tsx index 00431e0..ec890aa 100644 --- a/view/layouts/DashboardLayout.tsx +++ b/view/layouts/DashboardLayout.tsx @@ -1,11 +1,9 @@ import { useTranslation } from 'react-i18next' -import { useNavigate } from 'react-router-dom' import { useApp } from '../hooks/useApp.ts' import { Async, AsyncStatus, On } from '../components/general/Async.tsx' -import { Case } from '../components/general/Conditional.tsx' import { loadingStore } from '../stores/loading.ts' -import { LayoutLoading } from './LayoutLoading.tsx' -import { LayoutNavbar } from './LayoutNavbar.tsx' +import { LayoutLoading } from './general/LayoutLoading.tsx' +import { LayoutNavbar } from './general/LayoutNavbar.tsx' import { DashboardNavigation } from './dashboard/DashboardNavigation.tsx' export function DashboardLayout () { @@ -13,17 +11,9 @@ export function DashboardLayout () { 'default', { keyPrefix: 'layouts.dashboard' } ) - const navigate = useNavigate() const { session, auth } = useApp() - const signOut = async () => { - const done = await auth.signOut() - if (done) { - navigate('/') - } - } - return ( <> @@ -34,25 +24,12 @@ export function DashboardLayout () {
- - - {session.username} - - - - - - + +
diff --git a/view/layouts/LayoutNavbar.tsx b/view/layouts/LayoutNavbar.tsx deleted file mode 100644 index c1101f5..0000000 --- a/view/layouts/LayoutNavbar.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { ReactElement } from 'react' -import { Link } from 'react-router-dom' -import { useTranslation } from 'react-i18next' - -export type LayoutNavbarProps = { - children: ReactElement[] - condition: unknown -} - -export function LayoutNavbar (props: LayoutNavbarProps) { - const { children, condition } = props - - const { t } = useTranslation( - 'default', - { keyPrefix: 'layouts.public' } - ) - - return ( - - ) -} diff --git a/view/layouts/PublicLayout.tsx b/view/layouts/PublicLayout.tsx index f8dce10..70150fb 100644 --- a/view/layouts/PublicLayout.tsx +++ b/view/layouts/PublicLayout.tsx @@ -1,14 +1,12 @@ -import { Outlet, useNavigate } from 'react-router-dom' +import { Outlet } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useApp } from '../hooks/useApp.ts' import { Async, AsyncStatus, On } from '../components/general/Async.tsx' import { loadingStore } from '../stores/loading.ts' -import { LayoutLoading } from './LayoutLoading.tsx' -import { Case } from '../components/general/Conditional.tsx' -import { LayoutNavbar } from './LayoutNavbar.tsx' +import { LayoutLoading } from './general/LayoutLoading.tsx' +import { LayoutNavbar } from './general/LayoutNavbar.tsx' export function PublicLayout () { - const navigate = useNavigate() const { t } = useTranslation( 'default', { keyPrefix: 'layouts.public' } @@ -27,25 +25,11 @@ export function PublicLayout () {
- - - {session.username} - - - - - - +
diff --git a/view/layouts/LayoutLoading.tsx b/view/layouts/general/LayoutLoading.tsx similarity index 82% rename from view/layouts/LayoutLoading.tsx rename to view/layouts/general/LayoutLoading.tsx index 70e4e8b..5032d2e 100644 --- a/view/layouts/LayoutLoading.tsx +++ b/view/layouts/general/LayoutLoading.tsx @@ -1,6 +1,6 @@ -import { Loading } from '../components/general/Loading.tsx' -import { If } from '../components/general/Conditional.tsx' -import { loadingStore } from '../stores/loading.ts' +import { Loading } from '../../components/general/Loading.tsx' +import { If } from '../../components/general/Conditional.tsx' +import { loadingStore } from '../../stores/loading.ts' import { useState } from 'react' import { useBeforeUnload } from 'react-router-dom' diff --git a/view/layouts/general/LayoutNavbar.tsx b/view/layouts/general/LayoutNavbar.tsx new file mode 100644 index 0000000..fefd11c --- /dev/null +++ b/view/layouts/general/LayoutNavbar.tsx @@ -0,0 +1,96 @@ +import { Link, useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { Case, Switch } from '../../components/general/Conditional.tsx' +import { AuthContract, Session } from '../../../src/Domain/Auth/Auth.ts' + +export type LayoutNavbarProps = { + session: Session + auth: AuthContract + layout: 'public' | 'dashboard' +} + +export function LayoutNavbar (props: LayoutNavbarProps) { + const { layout, session, auth } = props + const navigate = useNavigate() + + const { t } = useTranslation( + 'default', + { keyPrefix: 'layouts.public' } + ) + + const signOut = async () => { + const done = await auth.signOut() + if (done) { + navigate('/') + } + } + + return ( + + ) +} From 539072030bd5925557f0218cb6ea937a10768b4e Mon Sep 17 00:00:00 2001 From: William Correa Date: Sat, 13 Apr 2024 19:12:56 -0300 Subject: [PATCH 11/17] feature: improve form management --- config/dependencies.ts | 15 +- config/i18n.ts | 98 +---------- config/resources/ptBR/default.ts | 161 +++++++++++++++++ index.css | 11 ++ package-lock.json | 12 ++ package.json | 2 + src/Domain/Contracts.ts | 1 + view/components/form/FormSelect.tsx | 67 +++++++ view/components/form/FormText.tsx | 52 ++++++ view/components/form/index.tsx | 14 ++ view/hooks/useFormValue.ts | 33 ++++ .../pages/dashboard/DashboardSettingsPage.tsx | 164 ++++++++++++++---- view/store.ts | 5 +- 13 files changed, 496 insertions(+), 139 deletions(-) create mode 100644 config/resources/ptBR/default.ts create mode 100644 view/components/form/FormSelect.tsx create mode 100644 view/components/form/FormText.tsx create mode 100644 view/components/form/index.tsx create mode 100644 view/hooks/useFormValue.ts diff --git a/config/dependencies.ts b/config/dependencies.ts index b78eb4f..3e47e86 100644 --- a/config/dependencies.ts +++ b/config/dependencies.ts @@ -13,18 +13,21 @@ import InMemoryAuthRepository from '../src/Infrastructure/Memory/InMemoryAuthRep import { getInheritDriver, getSessionDriver, isDevelopmentMode } from './env.ts' const binds: DriverResolver = { - [DriverType.memory]: { - AuthRepository: () => new InMemoryAuthRepository(), + [DriverType.json]: { GameRepository: () => new InMemoryGameRepository(), }, [DriverType.http]: { AuthRepository: () => HttpAuthRepository.build(), - // GameRepository: (config: Data) => HttpGameRepository.build(), + GameRepository: () => new InMemoryGameRepository(), + }, + [DriverType.memory]: { + AuthRepository: () => new InMemoryAuthRepository(), + GameRepository: () => new InMemoryGameRepository(), }, [DriverType.supabase]: { AuthRepository: (config: Data) => SupabaseAuthRepository.build(config), GameRepository: (config: Data) => SupabaseGameRepository.build(config), - }, + } } const factory = (token: string, driver?: Driver): [string, { useFactory: () => unknown }] => { @@ -34,6 +37,10 @@ const factory = (token: string, driver?: Driver): [string, { useFactory: () => u } const bind = binds[driver.type] const maker = bind[token] + if (!maker) { + throw new Error( + `No factory found for '${token}' in '${driver.type}' driver type. Review your dependencies.ts file`) + } return maker(driver.config) } return [token, { useFactory }] diff --git a/config/i18n.ts b/config/i18n.ts index 7e871f8..e091b51 100644 --- a/config/i18n.ts +++ b/config/i18n.ts @@ -1,6 +1,8 @@ import i18n from 'i18next' import { initReactI18next } from 'react-i18next' +import ptBR from './resources/ptBR/default.ts' + // the translations // (tip move them in a JSON file and import them, // or even better, manage them separated from your code: https://react.i18next.com/guides/multiple-translation-files) @@ -8,101 +10,7 @@ export const name = '🎮 Super Quizz' const resources = { 'ptBR': { - default: { - layouts: { - public: { - brand: name, - play: 'Jogar', - signIn: 'Entrar', - myAccount: 'Minha Conta', - copyright: '© PHP com Rapadura', - pending: 'Carregando ...', - }, - dashboard: { - brand: name, - play: 'Jogar', - signOut: 'Sair', - myAccount: 'Minha Conta', - pending: 'Carregando ...', - navigation: { - index: 'Meus Jogos', - games: 'Meus Jogos', - account: 'Minha Conta', - settings: 'Configurações' - } - } - }, - pages: { - home: { - title: name, - description: 'O melhor jogo de perguntas e respostas para jogar com os amigos!\n' + - 'Reúna sua galera e divirta-se com moderação! 🍻', - contributing: 'Não se esqueça de contribuir com o projeto no link abaixo! 😉', - callToAction: 'Jogar agora »' - }, - game: { - play: { - pending: 'Carregando Jogo ...', - rejected: 'Não foi possível carregar o jogo', - error: 'Oh snap!', - instructions: { - title: 'O jogo já vai começar!', - selected: 'Jogo Selecionado: ', - description: 'Certifique-se de que todos estão prontos e clique em Começar!\n' + - 'Lembrando que este jogo tem um tempo limite de {{timeout}} segundos para responder ' + - 'cada pergunta e {{total}} perguntas ao todo', - greetings: 'Boa sorte!', - start: 'Começar' - }, - session: { - correct: { - title: 'Certa a resposta! Acerto Mizeravi!', - description: 'Você acertou! Escolha alguém para beber e passe a vez para a pessoa à sua esquerda.' - }, - wrong: { - title: 'Você errou! Bebe!', - description: 'Errou feio, errou Rude! Agora tem de beber e passar a vez para a pessoa à sua esquerda.' - }, - expired: { - title: 'Acabou o tempo!', - description: 'Você demorou demais para responder! Agora tem de beber e passar a vez para a pessoa à sua esquerda.' - }, - unanswered: { - timer: 'Tempo restante: {{time}} segundos', - }, - next: 'Próximo', - answer: 'Responder', - } - }, - welcome: { - title: 'Escolha um Jogo', - description: 'Depois de selecionar um jogo cada um na mesa deve responder a uma pergunta.\n' + - 'Caso acerte, pode escolher uma pessoa para beber, caso erre tem de beber.\n' + - 'Ao terminar, passa-se para o jogador da esquerda.', - pending: 'Carregando jogos ...', - rejected: 'Não foi possível carregar os jogos', - error: 'Oh snap!' - }, - end: { - title: 'O jogo acabou!', - description: 'Não está bêbado suficiente? Clique em "Começar de novo"!', - waiting: 'Recomeçar em {{timer}} segundos', - restart: 'Começar de novo' - } - }, - dashboard: { - soon: 'Em breve!' - } - }, - components: { - game: { - list: { - title: 'Jogos Disponíveis', - empty: 'Não há nenhum jogo disponível no momento' - } - } - } - } + default: ptBR(name) }, en: { translation: { diff --git a/config/resources/ptBR/default.ts b/config/resources/ptBR/default.ts new file mode 100644 index 0000000..33b1fc9 --- /dev/null +++ b/config/resources/ptBR/default.ts @@ -0,0 +1,161 @@ +export default function (name: string) { + return { + layouts: { + public: { + brand: name, + play: 'Jogar', + signIn: 'Entrar', + myAccount: 'Minha Conta', + copyright: '© PHP com Rapadura', + pending: 'Carregando ...', + }, + dashboard: { + brand: name, + play: 'Jogar', + signOut: 'Sair', + myAccount: 'Minha Conta', + pending: 'Carregando ...', + navigation: { + index: 'Meus Jogos', + games: 'Meus Jogos', + account: 'Minha Conta', + settings: 'Configurações' + } + } + }, + pages: { + home: { + title: name, + description: 'O melhor jogo de perguntas e respostas para jogar com os amigos!\n' + + 'Reúna sua galera e divirta-se com moderação! 🍻', + contributing: 'Não se esqueça de contribuir com o projeto no link abaixo! 😉', + callToAction: 'Jogar agora »' + }, + game: { + play: { + pending: 'Carregando Jogo ...', + rejected: 'Não foi possível carregar o jogo', + error: 'Oh snap!', + instructions: { + title: 'O jogo já vai começar!', + selected: 'Jogo Selecionado: ', + description: 'Certifique-se de que todos estão prontos e clique em Começar!\n' + + 'Lembrando que este jogo tem um tempo limite de {{timeout}} segundos para responder ' + + 'cada pergunta e {{total}} perguntas ao todo', + greetings: 'Boa sorte!', + start: 'Começar' + }, + session: { + correct: { + title: 'Certa a resposta! Acerto Mizeravi!', + description: 'Você acertou! Escolha alguém para beber e passe a vez para a pessoa à sua esquerda.' + }, + wrong: { + title: 'Você errou! Bebe!', + description: 'Errou feio, errou Rude! Agora tem de beber e passar a vez para a pessoa à sua esquerda.' + }, + expired: { + title: 'Acabou o tempo!', + description: 'Você demorou demais para responder! Agora tem de beber e passar a vez para a pessoa à sua esquerda.' + }, + unanswered: { + timer: 'Tempo restante: {{time}} segundos', + }, + next: 'Próximo', + answer: 'Responder', + } + }, + welcome: { + title: 'Escolha um Jogo', + description: 'Depois de selecionar um jogo cada um na mesa deve responder a uma pergunta.\n' + + 'Caso acerte, pode escolher uma pessoa para beber, caso erre tem de beber.\n' + + 'Ao terminar, passa-se para o jogador da esquerda.', + pending: 'Carregando jogos ...', + rejected: 'Não foi possível carregar os jogos', + error: 'Oh snap!' + }, + end: { + title: 'O jogo acabou!', + description: 'Não está bêbado suficiente? Clique em "Começar de novo"!', + waiting: 'Recomeçar em {{timer}} segundos', + restart: 'Começar de novo' + } + }, + dashboard: { + soon: 'Em breve!', + settings: { + title: 'Configurações', + description: 'Aqui você pode configurar as fontes de dados e outras preferências dos jogos. Esta ' + + 'configuração não afeta a experiência de outros jogadores.', + fields: { + type: { + label: 'Tipo de Driver', + drivers: { + memory: 'Nenhuma configuração adicional é necessária', + json: 'Usa um JSON estático fornecido por uma URL HTTP', + http: 'Utiliza uma API HTTP devidamente compartível com a aplicação', + supabase: 'Realiza a Conexão com o backend do Supabase' + }, + details: 'Configuração que define de onde os dados dos jogos serão carregados', + }, + config: { + label: 'Configuração', + drivers: { + memory: 'Usa os dados inclusos no aplicativo e os manipula em memória', + json: { + url: { + label: 'URL', + placeholder: 'Informe a URL do JSON que contém os jogos', + details: 'A URL do JSON é o endereço de um arquivo JSON que contém os dados dos jogos ' + + 'disponíveis para jogar. Geralmente é um arquivo hospedado em um servidor HTTP' + }, + }, + http: { + url: { + label: 'URL', + placeholder: 'Informe a URL base do backend HTTP', + details: 'A URL base do backend HTTP é o endereço da API HTTP que contém os dados dos jogos ' + + 'disponíveis para jogar. Geralmente é um endereço de um servidor HTTP' + }, + authorization: { + label: 'Cabeçalho de autorização', + placeholder: 'Informe o cabeçalho de autorização do backend HTTP', + details: 'O cabeçalho de autorização do backend HTTP é um token de acesso que permite ' + + 'consultar os dados protegidos. Geralmente chama-se Authorization e recebe um token JWT' + }, + }, + supabase: { + url: { + label: 'URL', + placeholder: 'Informe a URL do backend do Supabase', + details: 'A URL do backend do Supabase é o endereço da API HTTP do Supabase, geralmente ' + + 'assemelha-se a "https://.supabase.co"' + }, + anonKey: { + label: 'Chave Anônima', + placeholder: 'Informe a chave anônima do backend do Supabase', + details: 'A chave anônima do backend do Supabase é um token de acesso que permite ' + + 'consultar os dados sem autenticação. Geralmente é um token JWT' + } + } + }, + }, + language: { + title: 'Idioma', + description: 'Escolha o idioma do jogo' + } + }, + save: 'Salvar', + } + } + }, + components: { + game: { + list: { + title: 'Jogos Disponíveis', + empty: 'Não há nenhum jogo disponível no momento' + } + } + } + } +} diff --git a/index.css b/index.css index 9ca5e92..731db9f 100644 --- a/index.css +++ b/index.css @@ -32,3 +32,14 @@ select.form-select { select.form-select { background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23555' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); } + +.DashboardForm div.form-row { + margin-bottom: 1rem; +} + +.DashboardForm small.text-muted { + margin-top: 0.5rem; + display: block; + opacity: 0.7; + font-size: 0.8rem; +} diff --git a/package-lock.json b/package-lock.json index cd5fa4f..3e47386 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ "version": "0.0.0", "dependencies": { "@supabase/supabase-js": "^2.42.0", + "@types/lodash": "^4.17.0", "i18next": "^23.10.1", + "lodash": "^4.17.21", "markdown": "^0.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -1332,6 +1334,11 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==" + }, "node_modules/@types/node": { "version": "20.12.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.5.tgz", @@ -2680,6 +2687,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", diff --git a/package.json b/package.json index 0539fcd..6fbcc53 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ }, "dependencies": { "@supabase/supabase-js": "^2.42.0", + "@types/lodash": "^4.17.0", "i18next": "^23.10.1", + "lodash": "^4.17.21", "markdown": "^0.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/Domain/Contracts.ts b/src/Domain/Contracts.ts index 8601654..00a5e06 100644 --- a/src/Domain/Contracts.ts +++ b/src/Domain/Contracts.ts @@ -16,6 +16,7 @@ export enum DriverType { http = 'http', memory = 'memory', supabase = 'supabase', + json = 'json', } export type Driver = { diff --git a/view/components/form/FormSelect.tsx b/view/components/form/FormSelect.tsx new file mode 100644 index 0000000..20dfc7f --- /dev/null +++ b/view/components/form/FormSelect.tsx @@ -0,0 +1,67 @@ +import { ChangeEvent, useEffect, useId, useState } from 'react' +import { FormFieldProps } from './index.tsx' + +type OptionValue = string | number + +type Option = { + value: T + label: string +} + +export type FormSelectProps = FormFieldProps & { + options: Option[] +} +export function FormSelect (props: FormSelectProps) { + const { + id, + name, + label, + options, + description = '', + value = '', + update, + } = props + + // eslint-disable-next-line react-hooks/rules-of-hooks + const fieldId: string = id ?? useId() + + const [fieldValue, setFieldValue] = useState(value as T) + + const onChange = (event: ChangeEvent) => { + const value = event.target.value as T + setFieldValue(value) + update && update(name, value) + } + + useEffect(() => setFieldValue(value as T), [value]) + + return ( +
+ + + {description && ({description})} +
+ ) +} diff --git a/view/components/form/FormText.tsx b/view/components/form/FormText.tsx new file mode 100644 index 0000000..7db7a2f --- /dev/null +++ b/view/components/form/FormText.tsx @@ -0,0 +1,52 @@ +import { ChangeEvent, useEffect, useId, useState } from 'react' +import { FormFieldProps } from './index.tsx' + +export type FormTextProps = FormFieldProps & { + placeholder?: string +} + +export function FormText (props: FormTextProps) { + const { + id, + name, + label, + placeholder = '', + description = '', + value = '', + update, + } = props + + // eslint-disable-next-line react-hooks/rules-of-hooks + const fieldId: string = id ?? useId() + + const [fieldValue, setFieldValue] = useState(String(value)) + + const onChange = (event: ChangeEvent) => { + const value = String(event.target.value) + setFieldValue(value) + update && update(name, value) + } + + useEffect(() => setFieldValue(value as string), [value]) + + return ( +
+ + + {description && ({description})} +
+ ) +} diff --git a/view/components/form/index.tsx b/view/components/form/index.tsx new file mode 100644 index 0000000..8ebe996 --- /dev/null +++ b/view/components/form/index.tsx @@ -0,0 +1,14 @@ +export type FormValueUpdate = (fieldName: string, value: unknown) => void + +export type FormValueWatchCallback = (current: unknown, previous: unknown) => void + +export type FormValueWatch = (fieldName: string, callback: FormValueWatchCallback) => void + +export type FormFieldProps = { + id?: string + name: string + label: string + value?: unknown + description?: string + update?: FormValueUpdate +} diff --git a/view/hooks/useFormValue.ts b/view/hooks/useFormValue.ts new file mode 100644 index 0000000..1a404af --- /dev/null +++ b/view/hooks/useFormValue.ts @@ -0,0 +1,33 @@ +import { useState } from 'react' +import { get, set } from 'lodash' +import { FormValueUpdate, FormValueWatch, FormValueWatchCallback } from '../components/form' + +export function useFormValue (initial: T): [T, FormValueUpdate, FormValueWatch] { + const [data, setData] = useState(initial) + + const update: FormValueUpdate = (fieldName: string, current: unknown): void => { + const previous = get(data, fieldName) + const target = data as object + set(target, fieldName, current) + setData({ + ...data, + ...target + }) + triggerWatch(fieldName, current, previous) + } + + const watches = new Map() + + const watch = (field: string, callback: FormValueWatchCallback) => { + watches.set(field, [...(watches.get(field) ?? []), callback]) + } + + const triggerWatch = (fieldName: string, current: unknown, previous: unknown) => { + const callbacks = watches.get(fieldName) + if (callbacks) { + callbacks.forEach((callback) => callback(current, previous)) + } + } + + return [data, update, watch] as const +} diff --git a/view/pages/dashboard/DashboardSettingsPage.tsx b/view/pages/dashboard/DashboardSettingsPage.tsx index 5e12a38..3fd1388 100644 --- a/view/pages/dashboard/DashboardSettingsPage.tsx +++ b/view/pages/dashboard/DashboardSettingsPage.tsx @@ -1,54 +1,142 @@ +import { FormEvent } from 'react' import { useTranslation } from 'react-i18next' -import { useId, useState } from 'react' -import { DriverType } from '../../../src/Domain/Contracts.ts' -import { Case, Switch } from '../../components/general/Conditional.tsx' + +import { Driver, DriverType } from '../../../src/Domain/Contracts.ts' +import { isDevelopmentMode } from '../../../config/env.ts' + import { useApp } from '../../hooks/useApp.ts' +import { useFormValue } from '../../hooks/useFormValue.ts' +import { loadingStore } from '../../stores/loading.ts' +import { Case, If, Switch } from '../../components/general/Conditional.tsx' +import { FormSelect } from '../../components/form/FormSelect.tsx' +import { FormText } from '../../components/form/FormText.tsx' export function DashboardSettingsPage () { const { t } = useTranslation( 'default', - { keyPrefix: 'pages.dashboard' } + { keyPrefix: 'pages.dashboard.settings' } ) - const driverTypeId = useId() const { session } = useApp() + const [ + data, + update, + watch + ] = useFormValue(session.driver) + const config = { url: '', authorization: '', anonKey: '' } + watch('type', () => update('config', config)) - const [driverType, setDriverType] = useState(session.driver.type) + function save (event: FormEvent) { + event.preventDefault() + loadingStore.state.loading = true + window.setTimeout( + () => loadingStore.state.loading = false, + import.meta.env.VITE_IN_MEMORY_TIMEOUT + ) + } return ( -
-
- - + +

{t('title')}

+

{t('description')}

+ + +
+

{t('fields.config.label')}

+ + + + + + + + + + + + + +

{t('fields.config.drivers.memory')}

+
+ + + + + +
+
+ - - -
- HTTP -
-
- - Memory - - - Supabase - -
+ +
{JSON.stringify(data, null, 2)}
+
) } diff --git a/view/store.ts b/view/store.ts index 293b874..e00e3db 100644 --- a/view/store.ts +++ b/view/store.ts @@ -1,3 +1,4 @@ + export interface StoreState { [key: string | symbol]: unknown } @@ -16,8 +17,8 @@ export function createStore (initial: StoreState): Store { const EVENTS: EventMap = Object.create({}) - const publish = (eventName: Key, current: unknown, previous: unknown) => { - const subscribers = EVENTS[eventName] + const publish = (key: Key, current: unknown, previous: unknown) => { + const subscribers = EVENTS[key] if (subscribers) { subscribers.forEach((callback: StoreCallback) => callback(current, previous)) } From f492a990894e45235ee4252c2a906bb12a703253 Mon Sep 17 00:00:00 2001 From: William Correa Date: Sat, 13 Apr 2024 21:51:39 -0300 Subject: [PATCH 12/17] feature: improve form management --- view/components/form/FormSelect.tsx | 31 +++++---------- view/components/form/FormText.tsx | 37 +++++------------- .../components/form/hooks/useFormComponent.ts | 38 +++++++++++++++++++ .../form}/hooks/useFormValue.ts | 2 +- view/components/form/index.tsx | 3 ++ .../pages/dashboard/DashboardSettingsPage.tsx | 2 +- 6 files changed, 63 insertions(+), 50 deletions(-) create mode 100644 view/components/form/hooks/useFormComponent.ts rename view/{ => components/form}/hooks/useFormValue.ts (97%) diff --git a/view/components/form/FormSelect.tsx b/view/components/form/FormSelect.tsx index 20dfc7f..23589bb 100644 --- a/view/components/form/FormSelect.tsx +++ b/view/components/form/FormSelect.tsx @@ -1,5 +1,5 @@ -import { ChangeEvent, useEffect, useId, useState } from 'react' import { FormFieldProps } from './index.tsx' +import { useFormComponent } from './hooks/useFormComponent.ts' type OptionValue = string | number @@ -11,29 +11,18 @@ type Option = { export type FormSelectProps = FormFieldProps & { options: Option[] } + export function FormSelect (props: FormSelectProps) { const { - id, - name, + fieldId, + fieldName, + onChange, label, - options, - description = '', - value = '', - update, - } = props - - // eslint-disable-next-line react-hooks/rules-of-hooks - const fieldId: string = id ?? useId() - - const [fieldValue, setFieldValue] = useState(value as T) - - const onChange = (event: ChangeEvent) => { - const value = event.target.value as T - setFieldValue(value) - update && update(name, value) - } + description, + fieldValue, + } = useFormComponent(props) - useEffect(() => setFieldValue(value as T), [value]) + const { options } = props return (
@@ -46,7 +35,7 @@ export function FormSelect (props: FormSelectProps) { + {description && ({description})} +
+ ) +} diff --git a/view/components/form/FormPassword.tsx b/view/components/form/FormPassword.tsx new file mode 100644 index 0000000..0e7b009 --- /dev/null +++ b/view/components/form/FormPassword.tsx @@ -0,0 +1,6 @@ +import { FormFieldProps } from './index.tsx' +import { FormInput } from './FormInput.tsx' + +export function FormPassword (props: FormFieldProps) { + return FormInput({ ...props, type: 'password' }) +} diff --git a/view/components/form/FormText.tsx b/view/components/form/FormText.tsx index 663e69e..cbf1c6c 100644 --- a/view/components/form/FormText.tsx +++ b/view/components/form/FormText.tsx @@ -1,35 +1,6 @@ import { FormFieldProps } from './index.tsx' -import { useFormComponent } from './hooks/useFormComponent.ts' +import { FormInput } from './FormInput.tsx' export function FormText (props: FormFieldProps) { - const { - fieldId, - fieldName, - onChange, - label, - placeholder, - description, - fieldValue, - } = useFormComponent(props) - - return ( -
- - - {description && ({description})} -
- ) + return FormInput({ ...props, type: 'text' }) } diff --git a/view/hooks/useLoading.ts b/view/hooks/useLoading.ts new file mode 100644 index 0000000..a8d216d --- /dev/null +++ b/view/hooks/useLoading.ts @@ -0,0 +1,20 @@ +import { loadingStore } from '../stores/loading.ts' +import { useState } from 'react' +import { useBeforeUnload } from 'react-router-dom' + +export function useLoading (): { loading: boolean, start: () => void, stop: () => void } { + const [loading, setLoading] = useState(false) + + const subscriptionId = loadingStore.subscribe('loading', (value: unknown) => { + if (value !== loading) { + setLoading(!!value) + } + }) + useBeforeUnload(() => loadingStore.unsubscribe('loading', subscriptionId)) + + return { + loading, + start: () => loadingStore.state.loading = true, + stop: () => loadingStore.state.loading = false + } as const +} diff --git a/view/layouts/DashboardLayout.tsx b/view/layouts/DashboardLayout.tsx index 90335a6..1a3ef0b 100644 --- a/view/layouts/DashboardLayout.tsx +++ b/view/layouts/DashboardLayout.tsx @@ -1,14 +1,14 @@ import { useApp } from '../hooks/useApp.ts' import { Async, AsyncStatus, On } from '../components/general/Async.tsx' -import { loadingStore } from '../stores/loading.ts' import { LayoutLoading } from './general/LayoutLoading.tsx' import { LayoutNavbar } from './general/LayoutNavbar.tsx' import { DashboardNavigation } from './dashboard/DashboardNavigation.tsx' import { useI18n } from '../hooks/useI18n.ts' +import { useLoading } from '../hooks/useLoading.ts' export function DashboardLayout () { const $t = useI18n('layouts.dashboard') - + const { stop } = useLoading() const { session, auth } = useApp() return ( @@ -16,7 +16,7 @@ export function DashboardLayout () { auth.restore()} - onFinally={() => loadingStore.state.loading = false} + onFinally={() => stop()} > diff --git a/view/layouts/PublicLayout.tsx b/view/layouts/PublicLayout.tsx index 5a5acc4..73246b7 100644 --- a/view/layouts/PublicLayout.tsx +++ b/view/layouts/PublicLayout.tsx @@ -1,16 +1,16 @@ import { Outlet } from 'react-router-dom' +import { Async, AsyncStatus, On } from '../components/general/Async.tsx' import { useApp } from '../hooks/useApp.ts' import { useI18n } from '../hooks/useI18n.ts' -import { loadingStore } from '../stores/loading.ts' -import { Async, AsyncStatus, On } from '../components/general/Async.tsx' +import { useLoading } from '../hooks/useLoading.ts' import { LayoutLoading } from './general/LayoutLoading.tsx' import { LayoutNavbar } from './general/LayoutNavbar.tsx' export function PublicLayout () { const $t = useI18n('layouts.public') - + const { stop } = useLoading() const { session, auth } = useApp() return ( @@ -18,7 +18,7 @@ export function PublicLayout () { auth.restore()} - onFinally={() => loadingStore.state.loading = false} + onFinally={() => stop()} > diff --git a/view/layouts/general/LayoutLoading.tsx b/view/layouts/general/LayoutLoading.tsx index 5032d2e..5215396 100644 --- a/view/layouts/general/LayoutLoading.tsx +++ b/view/layouts/general/LayoutLoading.tsx @@ -12,7 +12,6 @@ export function LayoutLoading ({ label, initial = true }: { label: string, initi setLoading(!!value) } }) - useBeforeUnload(() => loadingStore.unsubscribe('loading', subscriptionId)) return ( diff --git a/view/pages/dashboard/DashboardSettingsPage.tsx b/view/pages/dashboard/DashboardSettingsPage.tsx index 9a99c7e..5302c49 100644 --- a/view/pages/dashboard/DashboardSettingsPage.tsx +++ b/view/pages/dashboard/DashboardSettingsPage.tsx @@ -1,19 +1,22 @@ -import { FormEvent } from 'react' +import { useState } from 'react' -import { Driver, DriverType } from '../../../src/Domain/Contracts.ts' import { isDevelopmentMode } from '../../../config/env.ts' +import { Driver, DriverType } from '../../../src/Domain/Contracts.ts' -import { useApp } from '../../hooks/useApp.ts' import { useFormValue } from '../../components/form/hooks/useFormValue.ts' -import { loadingStore } from '../../stores/loading.ts' import { Case, If, Switch } from '../../components/general/Conditional.tsx' +import { Form } from '../../components/form/Form.tsx' import { FormSelect } from '../../components/form/FormSelect.tsx' import { FormText } from '../../components/form/FormText.tsx' +import { useApp } from '../../hooks/useApp.ts' import { useI18n } from '../../hooks/useI18n.ts' +import { useLoading } from '../../hooks/useLoading.ts' export function DashboardSettingsPage () { const $t = useI18n('pages.dashboard.settings') const { session } = useApp() + const [error, setError] = useState('') + const { loading } = useLoading() const [ value, update, @@ -22,19 +25,28 @@ export function DashboardSettingsPage () { const config = { url: '', authorization: '', anonKey: '' } watch('type', () => update('config', config)) - function onSubmit (event: FormEvent) { - event.preventDefault() - loadingStore.state.loading = true - window.setTimeout( - () => loadingStore.state.loading = false, - import.meta.env.VITE_IN_MEMORY_TIMEOUT - ) + function action (data: Driver): Promise { + setError('') + console.log(data) + return new Promise((resolve) => { + window.setTimeout( + () => resolve(stop()), + import.meta.env.VITE_IN_MEMORY_TIMEOUT + ) + }) + } + + function onResolve (data: unknown) { + console.log(data) } return ( -
+ value={value} + action={action} + onResolve={onResolve} + onReject={() => setError($t('error'))} + error={error} >

{$t('title')}

{$t('description')}

@@ -124,16 +136,19 @@ export function DashboardSettingsPage () {

- +
+ +
- -
{JSON.stringify(value, null, 2)}
-
- - ) + +
{JSON.stringify(value, null, 2)}
+
+ +) } diff --git a/view/pages/public/auth/SignInPage.tsx b/view/pages/public/auth/SignInPage.tsx index 433f1b8..fd85e9d 100644 --- a/view/pages/public/auth/SignInPage.tsx +++ b/view/pages/public/auth/SignInPage.tsx @@ -1,108 +1,101 @@ -import React, { useState } from 'react' +import { useState } from 'react' import { useNavigate } from 'react-router-dom' -import { useApp } from '../../../hooks/useApp.ts' -import { AlertDanger } from '../../../components/general/Alert.tsx' -import { DriverType } from '../../../../src/Domain/Contracts.ts' + import { isDevelopmentMode } from '../../../../config/env.ts' -import { loadingStore } from '../../../stores/loading.ts' +import { Session } from '../../../../src/Domain/Auth/Auth.ts' +import { DriverType } from '../../../../src/Domain/Contracts.ts' + +import { FormText } from '../../../components/form/FormText.tsx' +import { useFormValue } from '../../../components/form/hooks/useFormValue.ts' +import { If } from '../../../components/general/Conditional.tsx' +import { useApp } from '../../../hooks/useApp.ts' +import { useI18n } from '../../../hooks/useI18n.ts' +import { useLoading } from '../../../hooks/useLoading.ts' +import { FormPassword } from '../../../components/form/FormPassword.tsx' +import { Form } from '../../../components/form/Form.tsx' + +type SignInData = { + username: string + password: string | null +} export function SignInPage () { + const $t = useI18n('pages.auth.signIn') const { auth, session } = useApp() const navigate = useNavigate() - const [error, setError] = useState('') + const { loading } = useLoading() const type = session.driver.type === DriverType.supabase ? 'otp' : 'password' + const initial = { + username: isDevelopmentMode() ? 'arretado@phpcomrapadura.org' : '', + password: isDevelopmentMode() ? '***************' : '' + } + const [ + value, + update + ] = useFormValue(initial) - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault() + function action (data: SignInData): Promise { setError('') - loadingStore.state.loading = true - const form = new FormData(event.currentTarget) - const username = form.get('username') as string - const password = form.get('password') as string - try { - const signed = await auth.signIn(username, type === 'password' ? password : null) - if (signed?.credential) { - navigate('/dashboard') - return - } - navigate('/auth/otp') - } catch (error) { - setError('Usuário e/ou senha inválidos') + if (type === 'password') { + return auth.signIn(data.username, data.password) + } + return auth.signIn(data.username) + } + + function onResolve (data: Session) { + if (data?.credential) { + navigate('/dashboard') return - } finally { - loadingStore.state.loading = false } + navigate('/auth/otp') } return ( -
-

Entrar

-
-
-
- - - - Utilize seu nome de usuário ou email - -
+
+

{$t('title')}

+
+ + value={value} + action={action} + onResolve={onResolve} + onReject={() => setError($t('error'))} + error={error} + > + - { - type === 'password' && -
- - -
- } - -
+ + + +
+
- { - error && ( - - {error} - - ) - } - +
) From 2c75d74002593e37d587a4269fe1d39f98e790ea Mon Sep 17 00:00:00 2001 From: William Correa Date: Sun, 14 Apr 2024 00:35:27 -0300 Subject: [PATCH 15/17] feature: improve loader and other UX --- index.css | 7 ++++++ view/components/form/Form.tsx | 11 +++++----- view/hooks/useLoading.ts | 6 ++--- view/layouts/DashboardLayout.tsx | 4 ++-- view/layouts/PublicLayout.tsx | 4 ++-- view/layouts/general/LayoutLoading.tsx | 22 +++++++++++++++++-- .../pages/dashboard/DashboardSettingsPage.tsx | 12 ++++------ 7 files changed, 44 insertions(+), 22 deletions(-) diff --git a/index.css b/index.css index ce982ce..43c2847 100644 --- a/index.css +++ b/index.css @@ -9,6 +9,13 @@ body { position: relative; } +hr { + margin: 1.5rem 0; + /*border: 0;*/ + /*height: 1px;*/ + /*background-image: linear-gradient(90deg, hsla(0, 0%, 86.3%, 0.4) 0%, hsla(0, 0%, 86.3%, 0.8) 2%, #ddd 50%, hsla(0, 0%, 86.3%, 0.8) 98%, hsla(0, 0%, 86.3%, 0.4));*/ +} + input[type="text"].form-control, input[type="password"].form-control, select.form-select { background-color: #ffffff; color: #5c5c5c; diff --git a/view/components/form/Form.tsx b/view/components/form/Form.tsx index 180b439..8d0700a 100644 --- a/view/components/form/Form.tsx +++ b/view/components/form/Form.tsx @@ -6,7 +6,7 @@ import { AlertDanger } from '../general/Alert.tsx' type FormProps = { children: ReactNode | ReactNode[] value: T - action: (data: T) => Promise + action: (data: T, rawValue: FormData) => Promise onResolve?: (data: R) => void onReject?: (error: unknown) => void onFinally?: () => void @@ -23,19 +23,20 @@ export function Form (props: FormProps) { onFinally, error } = props - const { start, stop } = useLoading() + const { raise, fall } = useLoading() const handleSubmit = async (event: FormEvent) => { event.preventDefault() - start() + const rawValue = new FormData(event.currentTarget) try { - const response = await action(value) + raise() + const response = await action(value, rawValue) onResolve && onResolve(response) } catch (error) { onReject && onReject(error) return } finally { - stop() + fall() onFinally && onFinally() } } diff --git a/view/hooks/useLoading.ts b/view/hooks/useLoading.ts index a8d216d..b8a3083 100644 --- a/view/hooks/useLoading.ts +++ b/view/hooks/useLoading.ts @@ -2,7 +2,7 @@ import { loadingStore } from '../stores/loading.ts' import { useState } from 'react' import { useBeforeUnload } from 'react-router-dom' -export function useLoading (): { loading: boolean, start: () => void, stop: () => void } { +export function useLoading (): { loading: boolean, raise: () => void, fall: () => void } { const [loading, setLoading] = useState(false) const subscriptionId = loadingStore.subscribe('loading', (value: unknown) => { @@ -14,7 +14,7 @@ export function useLoading (): { loading: boolean, start: () => void, stop: () = return { loading, - start: () => loadingStore.state.loading = true, - stop: () => loadingStore.state.loading = false + raise: () => loadingStore.state.loading = true, + fall: () => loadingStore.state.loading = false } as const } diff --git a/view/layouts/DashboardLayout.tsx b/view/layouts/DashboardLayout.tsx index 1a3ef0b..c4625a6 100644 --- a/view/layouts/DashboardLayout.tsx +++ b/view/layouts/DashboardLayout.tsx @@ -8,7 +8,7 @@ import { useLoading } from '../hooks/useLoading.ts' export function DashboardLayout () { const $t = useI18n('layouts.dashboard') - const { stop } = useLoading() + const { fall } = useLoading() const { session, auth } = useApp() return ( @@ -16,7 +16,7 @@ export function DashboardLayout () { auth.restore()} - onFinally={() => stop()} + onFinally={() => fall()} > diff --git a/view/layouts/PublicLayout.tsx b/view/layouts/PublicLayout.tsx index 73246b7..530b7e2 100644 --- a/view/layouts/PublicLayout.tsx +++ b/view/layouts/PublicLayout.tsx @@ -10,7 +10,7 @@ import { LayoutNavbar } from './general/LayoutNavbar.tsx' export function PublicLayout () { const $t = useI18n('layouts.public') - const { stop } = useLoading() + const { fall } = useLoading() const { session, auth } = useApp() return ( @@ -18,7 +18,7 @@ export function PublicLayout () { auth.restore()} - onFinally={() => stop()} + onFinally={() => fall()} > diff --git a/view/layouts/general/LayoutLoading.tsx b/view/layouts/general/LayoutLoading.tsx index 5215396..29efa1e 100644 --- a/view/layouts/general/LayoutLoading.tsx +++ b/view/layouts/general/LayoutLoading.tsx @@ -1,19 +1,34 @@ import { Loading } from '../../components/general/Loading.tsx' import { If } from '../../components/general/Conditional.tsx' import { loadingStore } from '../../stores/loading.ts' -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useBeforeUnload } from 'react-router-dom' export function LayoutLoading ({ label, initial = true }: { label: string, initial?: boolean }) { const [loading, setLoading] = useState(initial) + const [width, setWidth] = useState(0) const subscriptionId = loadingStore.subscribe('loading', (value: unknown) => { if (value !== loading) { setLoading(!!value) + setWidth(0) } }) useBeforeUnload(() => loadingStore.unsubscribe('loading', subscriptionId)) + useEffect(() => { + const interval = setInterval(() => { + setWidth((prev) => { + const factor = prev > 50 ? 20 : 10 + if (prev < 100) { + return prev + factor + } + return 100 + }) + }, 100) + return () => clearInterval(interval) + }, []) + return (
- +
diff --git a/view/pages/dashboard/DashboardSettingsPage.tsx b/view/pages/dashboard/DashboardSettingsPage.tsx index 5302c49..a535909 100644 --- a/view/pages/dashboard/DashboardSettingsPage.tsx +++ b/view/pages/dashboard/DashboardSettingsPage.tsx @@ -1,10 +1,9 @@ import { useState } from 'react' -import { isDevelopmentMode } from '../../../config/env.ts' import { Driver, DriverType } from '../../../src/Domain/Contracts.ts' import { useFormValue } from '../../components/form/hooks/useFormValue.ts' -import { Case, If, Switch } from '../../components/general/Conditional.tsx' +import { Case, Switch } from '../../components/general/Conditional.tsx' import { Form } from '../../components/form/Form.tsx' import { FormSelect } from '../../components/form/FormSelect.tsx' import { FormText } from '../../components/form/FormText.tsx' @@ -17,6 +16,7 @@ export function DashboardSettingsPage () { const { session } = useApp() const [error, setError] = useState('') const { loading } = useLoading() + const [ value, update, @@ -30,7 +30,7 @@ export function DashboardSettingsPage () { console.log(data) return new Promise((resolve) => { window.setTimeout( - () => resolve(stop()), + () => resolve('nuhh'), import.meta.env.VITE_IN_MEMORY_TIMEOUT ) }) @@ -145,10 +145,6 @@ export function DashboardSettingsPage () { {$t('save')}
- - -
{JSON.stringify(value, null, 2)}
-
-) + ) } From f313f83cfe147334185a6a5066750f5f1ca484d9 Mon Sep 17 00:00:00 2001 From: William Correa Date: Sun, 14 Apr 2024 17:38:13 -0300 Subject: [PATCH 16/17] feature: improve loader and other UX --- config/dependencies.ts | 10 +- config/resources/ptBR/default.ts | 3 + index.css | 1 + src/Domain/Admin/UserConfigRepository.ts | 5 + src/Domain/Auth/Auth.ts | 3 +- src/Domain/Contracts.ts | 2 + .../Memory/InMemoryUserConfigRepository.ts | 9 + view/components/form/Form.tsx | 56 ++-- view/components/form/FormInput.tsx | 2 +- view/components/form/FormPassword.tsx | 2 +- view/components/form/FormSelect.tsx | 2 +- view/components/form/FormText.tsx | 2 +- view/components/form/hooks/useFormValue.ts | 27 +- view/components/form/{index.tsx => index.ts} | 6 + view/hooks/index.ts | 4 + view/hooks/useApp.ts | 7 +- .../pages/dashboard/DashboardSettingsPage.tsx | 257 +++++++++--------- view/pages/public/auth/SignInPage.tsx | 70 +++-- 18 files changed, 274 insertions(+), 194 deletions(-) create mode 100644 src/Domain/Admin/UserConfigRepository.ts create mode 100644 src/Infrastructure/Memory/InMemoryUserConfigRepository.ts rename view/components/form/{index.tsx => index.ts} (68%) create mode 100644 view/hooks/index.ts diff --git a/config/dependencies.ts b/config/dependencies.ts index 3e47e86..617e931 100644 --- a/config/dependencies.ts +++ b/config/dependencies.ts @@ -4,11 +4,15 @@ import { container } from 'tsyringe' import { Data, Driver, DriverResolver, DriverType } from '../src/Domain/Contracts.ts' import { AuthService } from '../src/Application/Auth/AuthService.ts' -import SupabaseAuthRepository from '../src/Infrastructure/Supabase/SupabaseAuthRepository.ts' + import HttpAuthRepository from '../src/Infrastructure/Http/HttpAuthRepository.ts' + +import InMemoryAuthRepository from '../src/Infrastructure/Memory/InMemoryAuthRepository.ts' import InMemoryGameRepository from '../src/Infrastructure/Memory/InMemoryGameRepository.ts' +import InMemoryUserConfigRepository from '../src/Infrastructure/Memory/InMemoryUserConfigRepository.ts' + +import SupabaseAuthRepository from '../src/Infrastructure/Supabase/SupabaseAuthRepository.ts' import SupabaseGameRepository from '../src/Infrastructure/Supabase/SupabaseGameRepository.ts' -import InMemoryAuthRepository from '../src/Infrastructure/Memory/InMemoryAuthRepository.ts' import { getInheritDriver, getSessionDriver, isDevelopmentMode } from './env.ts' @@ -23,6 +27,7 @@ const binds: DriverResolver = { [DriverType.memory]: { AuthRepository: () => new InMemoryAuthRepository(), GameRepository: () => new InMemoryGameRepository(), + UserConfigRepository: () => new InMemoryUserConfigRepository(), }, [DriverType.supabase]: { AuthRepository: (config: Data) => SupabaseAuthRepository.build(config), @@ -58,6 +63,7 @@ export default function () { } } container.register(...factory('AuthRepository', authDriver)) + container.register(...factory('UserConfigRepository', authDriver)) // [end] structure stuff // game stuff diff --git a/config/resources/ptBR/default.ts b/config/resources/ptBR/default.ts index a2f13cb..46d5f57 100644 --- a/config/resources/ptBR/default.ts +++ b/config/resources/ptBR/default.ts @@ -170,6 +170,9 @@ export default function (name: string) { } }, save: 'Salvar', + reset: 'Redefinir', + success: 'Configurações salvas com sucesso', + error: 'Não foi possível salvar as configurações', } } }, diff --git a/index.css b/index.css index 43c2847..14e8882 100644 --- a/index.css +++ b/index.css @@ -50,6 +50,7 @@ form.form-component small.text-muted { form.form-component div.form-action { display: flex; align-items: center; + gap: 1rem; } form.form-component div.form-action.align-left { diff --git a/src/Domain/Admin/UserConfigRepository.ts b/src/Domain/Admin/UserConfigRepository.ts new file mode 100644 index 0000000..195844c --- /dev/null +++ b/src/Domain/Admin/UserConfigRepository.ts @@ -0,0 +1,5 @@ +import { Content, Data, Id } from '../Contracts.ts' + +export default interface UserConfigRepository { + update (id: Id, data: Data): Promise +} diff --git a/src/Domain/Auth/Auth.ts b/src/Domain/Auth/Auth.ts index 23c06c7..9f46a17 100644 --- a/src/Domain/Auth/Auth.ts +++ b/src/Domain/Auth/Auth.ts @@ -1,4 +1,4 @@ -import { Driver } from '../Contracts.ts' +import { Driver, Id } from '../Contracts.ts' export type Credential = { token: string @@ -8,6 +8,7 @@ export type Credential = { } | undefined export type Session = { + id?: Id username: string credential: Credential abilities: string[] diff --git a/src/Domain/Contracts.ts b/src/Domain/Contracts.ts index 00a5e06..028b31d 100644 --- a/src/Domain/Contracts.ts +++ b/src/Domain/Contracts.ts @@ -12,6 +12,8 @@ export interface Content { export type Data = { [property: string]: Data | unknown } +export type Id = string | number + export enum DriverType { http = 'http', memory = 'memory', diff --git a/src/Infrastructure/Memory/InMemoryUserConfigRepository.ts b/src/Infrastructure/Memory/InMemoryUserConfigRepository.ts new file mode 100644 index 0000000..80b4643 --- /dev/null +++ b/src/Infrastructure/Memory/InMemoryUserConfigRepository.ts @@ -0,0 +1,9 @@ +import UserConfigRepository from "../../Domain/Admin/UserConfigRepository"; +import { Content, Data, Id, Status } from '../../Domain/Contracts.ts' +import InMemoryRepository from '../Driver/Memory/InMemoryRepository.ts' + +export default class InMemoryUserConfigRepository extends InMemoryRepository implements UserConfigRepository { + update (id: Id, data: Data): Promise { + return this.promisify({ status: Status.success, data: { id, config: data } }) + } +} diff --git a/view/components/form/Form.tsx b/view/components/form/Form.tsx index 8d0700a..500112c 100644 --- a/view/components/form/Form.tsx +++ b/view/components/form/Form.tsx @@ -1,36 +1,40 @@ import { FormEvent, ReactNode } from 'react' -import { useLoading } from '../../hooks/useLoading.ts' +import { useLoading } from '../../hooks' import { AlertDanger } from '../general/Alert.tsx' -type FormProps = { - children: ReactNode | ReactNode[] - value: T - action: (data: T, rawValue: FormData) => Promise - onResolve?: (data: R) => void +type Children = ReactNode | ReactNode[] + +type ChildrenLoader = (loading: boolean) => Children + +type FormProps = { + fields: Children + action: (rawValue: FormData) => Promise + onResolve?: (data: T) => void onReject?: (error: unknown) => void onFinally?: () => void error?: string + buttons?: ChildrenLoader | Children } -export function Form (props: FormProps) { +export function Form (props: FormProps) { const { - children, - value, action, + fields, onResolve, onReject, onFinally, - error + error, + buttons, } = props - const { raise, fall } = useLoading() + const { raise, fall, loading } = useLoading() - const handleSubmit = async (event: FormEvent) => { + const onSubmit = async (event: FormEvent) => { event.preventDefault() const rawValue = new FormData(event.currentTarget) try { raise() - const response = await action(value, rawValue) + const response = await action(rawValue) onResolve && onResolve(response) } catch (error) { onReject && onReject(error) @@ -44,14 +48,30 @@ export function Form (props: FormProps) { return (
- {children} + {fields} + { + buttons && ( + <> +
+
+ { + typeof buttons === 'function' ? + buttons(loading) : + buttons + } +
+ + ) + } { error && ( - - {error} - +
+ + {error} + +
) }
diff --git a/view/components/form/FormInput.tsx b/view/components/form/FormInput.tsx index f330115..ec96f4b 100644 --- a/view/components/form/FormInput.tsx +++ b/view/components/form/FormInput.tsx @@ -1,4 +1,4 @@ -import { FormFieldProps } from './index.tsx' +import { FormFieldProps } from './index.ts' import { useFormComponent } from './hooks/useFormComponent.ts' type FormInputProps = FormFieldProps & { type?: string } diff --git a/view/components/form/FormPassword.tsx b/view/components/form/FormPassword.tsx index 0e7b009..8cf42cc 100644 --- a/view/components/form/FormPassword.tsx +++ b/view/components/form/FormPassword.tsx @@ -1,4 +1,4 @@ -import { FormFieldProps } from './index.tsx' +import { FormFieldProps } from './index.ts' import { FormInput } from './FormInput.tsx' export function FormPassword (props: FormFieldProps) { diff --git a/view/components/form/FormSelect.tsx b/view/components/form/FormSelect.tsx index 23589bb..68fdc5a 100644 --- a/view/components/form/FormSelect.tsx +++ b/view/components/form/FormSelect.tsx @@ -1,4 +1,4 @@ -import { FormFieldProps } from './index.tsx' +import { FormFieldProps } from './index.ts' import { useFormComponent } from './hooks/useFormComponent.ts' type OptionValue = string | number diff --git a/view/components/form/FormText.tsx b/view/components/form/FormText.tsx index cbf1c6c..1de37c0 100644 --- a/view/components/form/FormText.tsx +++ b/view/components/form/FormText.tsx @@ -1,4 +1,4 @@ -import { FormFieldProps } from './index.tsx' +import { FormFieldProps } from './index.ts' import { FormInput } from './FormInput.tsx' export function FormText (props: FormFieldProps) { diff --git a/view/components/form/hooks/useFormValue.ts b/view/components/form/hooks/useFormValue.ts index 17428d0..6c0e217 100644 --- a/view/components/form/hooks/useFormValue.ts +++ b/view/components/form/hooks/useFormValue.ts @@ -2,15 +2,26 @@ import { useState } from 'react' import { get, set } from 'lodash' import { FormValueUpdate, FormValueWatch, FormValueWatchCallback } from '../index.tsx' -export function useFormValue (initial: T): [T, FormValueUpdate, FormValueWatch] { - const [data, setData] = useState(initial) +function clone (value: T): T { + return JSON.parse(JSON.stringify(value)) +} + +export type FormValueManager = { + value: T + update: FormValueUpdate + watch: FormValueWatch + reset: () => void +} + +export function useFormValue (initial: T): FormValueManager { + const [value, setData] = useState(clone(initial)) const update: FormValueUpdate = (fieldName: string, current: unknown): void => { - const previous = get(data, fieldName) - const target = data as object + const previous = get(value, fieldName) + const target = value as object set(target, fieldName, current) setData({ - ...data, + ...value, ...target }) triggerWatch(fieldName, current, previous) @@ -29,5 +40,9 @@ export function useFormValue (initial: T): [T, FormValueUpdate, FormValueWatc } } - return [data, update, watch] as const + const reset = () => { + setData(clone(initial)) + } + + return { value, update, reset, watch } as const } diff --git a/view/components/form/index.tsx b/view/components/form/index.ts similarity index 68% rename from view/components/form/index.tsx rename to view/components/form/index.ts index e05b806..e13e32a 100644 --- a/view/components/form/index.tsx +++ b/view/components/form/index.ts @@ -15,3 +15,9 @@ export type FormFieldProps = { } export type FormElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement + +export { useFormValue } from './hooks/useFormValue.ts' +export { Form } from './Form.tsx' +export { FormPassword } from './FormPassword.tsx' +export { FormText } from './FormText.tsx' +export { FormSelect } from './FormSelect.tsx' diff --git a/view/hooks/index.ts b/view/hooks/index.ts new file mode 100644 index 0000000..c164f8d --- /dev/null +++ b/view/hooks/index.ts @@ -0,0 +1,4 @@ +export { useApp } from './useApp.ts' +export { useI18n } from './useI18n.ts' +export { useLoading } from './useLoading.ts' +export { useRunOnce } from './useRunOnce.ts' diff --git a/view/hooks/useApp.ts b/view/hooks/useApp.ts index 922c1a2..c9feff9 100644 --- a/view/hooks/useApp.ts +++ b/view/hooks/useApp.ts @@ -1,6 +1,7 @@ -import { useContext } from "react"; -import { AppContext } from "../components/app/AppContext"; +import { useContext } from 'react' +import { AppContext } from '../components/app/AppContext' +import { AppContextContract } from '../contracts.ts' -export function useApp () { +export function useApp (): AppContextContract { return useContext(AppContext) } diff --git a/view/pages/dashboard/DashboardSettingsPage.tsx b/view/pages/dashboard/DashboardSettingsPage.tsx index a535909..0797b03 100644 --- a/view/pages/dashboard/DashboardSettingsPage.tsx +++ b/view/pages/dashboard/DashboardSettingsPage.tsx @@ -1,150 +1,161 @@ import { useState } from 'react' -import { Driver, DriverType } from '../../../src/Domain/Contracts.ts' +import { Content, Driver, DriverType } from '../../../src/Domain/Contracts.ts' +import UserConfigRepository from '../../../src/Domain/Admin/UserConfigRepository.ts' -import { useFormValue } from '../../components/form/hooks/useFormValue.ts' +import { Form, FormSelect, FormText, useFormValue } from '../../components/form' import { Case, Switch } from '../../components/general/Conditional.tsx' -import { Form } from '../../components/form/Form.tsx' -import { FormSelect } from '../../components/form/FormSelect.tsx' -import { FormText } from '../../components/form/FormText.tsx' -import { useApp } from '../../hooks/useApp.ts' -import { useI18n } from '../../hooks/useI18n.ts' -import { useLoading } from '../../hooks/useLoading.ts' +import { useApp, useI18n } from '../../hooks' export function DashboardSettingsPage () { const $t = useI18n('pages.dashboard.settings') - const { session } = useApp() - const [error, setError] = useState('') - const { loading } = useLoading() + const { container, session } = useApp() + const [error, setError] = useState('') - const [ + const userConfigRepository = container.resolve('UserConfigRepository') + const { value, update, - watch - ] = useFormValue(session.driver) + watch, + reset, + } = useFormValue(session.driver) const config = { url: '', authorization: '', anonKey: '' } watch('type', () => update('config', config)) - function action (data: Driver): Promise { + function action (): Promise { setError('') - console.log(data) - return new Promise((resolve) => { - window.setTimeout( - () => resolve('nuhh'), - import.meta.env.VITE_IN_MEMORY_TIMEOUT - ) - }) + if (!session.id) { + return Promise.reject(new Error('No session id found')) + } + return userConfigRepository.update(session.id, value) } - function onResolve (data: unknown) { + function onResolve (data: Content) { console.log(data) } + function onReject (error: unknown) { + setError($t('error')) + console.error(error) + } + return ( - - value={value} - action={action} - onResolve={onResolve} - onReject={() => setError($t('error'))} - error={error} - > +

{$t('title')}

{$t('description')}

- - -
-

{$t('fields.config.label')}

- - - - - - - - - - + action={action} + onResolve={onResolve} + onReject={onReject} + error={error} + fields={ + <> + - + {/* config */} +
+

{$t('fields.config.label')}

- -

{$t('fields.config.drivers.memory')}

-
- - - - - - -
-
-
- -
- + + {/* DriverType.json */} + + + + {/* DriverType.http */} + + + + + {/* DriverType.memory */} + +

{$t('fields.config.drivers.memory')}

+
+ {/* DriverType.supabase */} + + + + +
+
+ + } + buttons={(loading: boolean) => { + return <> + + + + }} + /> +
) } diff --git a/view/pages/public/auth/SignInPage.tsx b/view/pages/public/auth/SignInPage.tsx index fd85e9d..1854abc 100644 --- a/view/pages/public/auth/SignInPage.tsx +++ b/view/pages/public/auth/SignInPage.tsx @@ -5,14 +5,9 @@ import { isDevelopmentMode } from '../../../../config/env.ts' import { Session } from '../../../../src/Domain/Auth/Auth.ts' import { DriverType } from '../../../../src/Domain/Contracts.ts' -import { FormText } from '../../../components/form/FormText.tsx' -import { useFormValue } from '../../../components/form/hooks/useFormValue.ts' +import { Form, FormPassword, FormText, useFormValue } from '../../../components/form' import { If } from '../../../components/general/Conditional.tsx' -import { useApp } from '../../../hooks/useApp.ts' -import { useI18n } from '../../../hooks/useI18n.ts' -import { useLoading } from '../../../hooks/useLoading.ts' -import { FormPassword } from '../../../components/form/FormPassword.tsx' -import { Form } from '../../../components/form/Form.tsx' +import { useApp, useI18n, useLoading } from '../../../hooks' type SignInData = { username: string @@ -31,17 +26,17 @@ export function SignInPage () { username: isDevelopmentMode() ? 'arretado@phpcomrapadura.org' : '', password: isDevelopmentMode() ? '***************' : '' } - const [ + const { value, update - ] = useFormValue(initial) + } = useFormValue(initial) - function action (data: SignInData): Promise { + function action (): Promise { setError('') if (type === 'password') { - return auth.signIn(data.username, data.password) + return auth.signIn(value.username, value.password) } - return auth.signIn(data.username) + return auth.signIn(value.username) } function onResolve (data: Session) { @@ -59,34 +54,35 @@ export function SignInPage () { >

{$t('title')}

- - value={value} + action={action} onResolve={onResolve} onReject={() => setError($t('error'))} error={error} - > - + fields={ + <> + - - - -
-
+ + + + + } + buttons={ -
- + } + />
) From 70338930d6e48888ad8f2407ad542dd028c773c9 Mon Sep 17 00:00:00 2001 From: William Correa Date: Sun, 14 Apr 2024 18:19:02 -0300 Subject: [PATCH 17/17] feature: review component imports --- view/App.tsx | 4 ---- view/components/auth/CredentialChecker.tsx | 5 +++-- view/components/form/Form.tsx | 1 + view/components/game/GameList.tsx | 5 +++-- .../game-play-session/GamePlaySessionInstruction.tsx | 3 ++- .../GamePlaySessionQuestionCorrect.tsx | 3 ++- .../GamePlaySessionQuestionTimeExpired.tsx | 2 +- .../GamePlaySessionQuestionUnanswered.tsx | 2 +- .../GamePlaySessionQuestionWrong.tsx | 4 ++-- view/components/general/Async.tsx | 2 +- view/layouts/DashboardLayout.tsx | 9 +++++---- view/layouts/PublicLayout.tsx | 6 +++--- view/layouts/dashboard/DashboardNavigation.tsx | 3 ++- view/layouts/general/LayoutLoading.tsx | 7 ++++--- view/layouts/general/LayoutNavbar.tsx | 2 +- view/pages/HomePage.tsx | 3 ++- view/pages/dashboard/DahboardMyAccountPage.tsx | 2 +- view/pages/dashboard/DashboardGamesPage.tsx | 2 +- view/pages/public/game/GameEndPage.tsx | 4 ++-- view/pages/public/game/GamePlayPage.tsx | 3 +-- view/pages/public/game/GameWelcomePage.tsx | 7 +++---- vite-env.d.ts => view/vite-env.d.ts | 0 22 files changed, 41 insertions(+), 38 deletions(-) rename vite-env.d.ts => view/vite-env.d.ts (100%) diff --git a/view/App.tsx b/view/App.tsx index fd69e26..793ebb1 100644 --- a/view/App.tsx +++ b/view/App.tsx @@ -19,16 +19,12 @@ import { GameEndPage } from './pages/public/game/GameEndPage.tsx' import { DashboardLayout } from './layouts/DashboardLayout.tsx' import { SignInPage } from './pages/public/auth/SignInPage.tsx' import { WaitOneTimePassword } from './pages/public/auth/WaitOneTimePassword.tsx' -import { useRunOnce } from './hooks/useRunOnce.ts' -import { name } from '../config/i18n.ts' import { DashboardGamesPage } from './pages/dashboard/DashboardGamesPage.tsx' import { DashboardSettingsPage } from './pages/dashboard/DashboardSettingsPage.tsx' import { DashboardMyAccountPage } from './pages/dashboard/DahboardMyAccountPage.tsx' export default function App () { - useRunOnce(() => document.title = name) - return ( diff --git a/view/components/auth/CredentialChecker.tsx b/view/components/auth/CredentialChecker.tsx index 132401d..88d6f16 100644 --- a/view/components/auth/CredentialChecker.tsx +++ b/view/components/auth/CredentialChecker.tsx @@ -1,6 +1,7 @@ -import { Navigate, Outlet, useLocation } from 'react-router-dom' -import { useApp } from '../../hooks/useApp.ts' import { ReactNode } from 'react' +import { Navigate, Outlet, useLocation } from 'react-router-dom' + +import { useApp } from '../../hooks' export type CredentialCheckerProps = { children?: ReactNode | ReactNode[] diff --git a/view/components/form/Form.tsx b/view/components/form/Form.tsx index 500112c..673a960 100644 --- a/view/components/form/Form.tsx +++ b/view/components/form/Form.tsx @@ -1,6 +1,7 @@ import { FormEvent, ReactNode } from 'react' import { useLoading } from '../../hooks' + import { AlertDanger } from '../general/Alert.tsx' type Children = ReactNode | ReactNode[] diff --git a/view/components/game/GameList.tsx b/view/components/game/GameList.tsx index 909b598..5bdbee3 100644 --- a/view/components/game/GameList.tsx +++ b/view/components/game/GameList.tsx @@ -1,9 +1,10 @@ import Game from '../../../src/Domain/Game/Game.ts' import { Link } from 'react-router-dom' -import styles from './game-list/index.module.css' +import { useI18n } from '../../hooks' import { AlertPrimary } from '../general/Alert.tsx' -import { useI18n } from '../../hooks/useI18n.ts' + +import styles from './game-list/index.module.css' export function GameList ({ games }: { games: Game[] }) { const $t = useI18n('components.game.list') diff --git a/view/components/game/game-play-session/GamePlaySessionInstruction.tsx b/view/components/game/game-play-session/GamePlaySessionInstruction.tsx index 3daca08..cc8e845 100644 --- a/view/components/game/game-play-session/GamePlaySessionInstruction.tsx +++ b/view/components/game/game-play-session/GamePlaySessionInstruction.tsx @@ -1,6 +1,7 @@ import Game from '../../../../src/Domain/Game/Game.ts' + +import { useI18n } from '../../../hooks' import { AlertPrimary } from '../../general/Alert.tsx' -import { useI18n } from '../../../hooks/useI18n.ts' export type GameInstructionProps = { timeout: number, diff --git a/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionCorrect.tsx b/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionCorrect.tsx index 4591b4e..05c2675 100644 --- a/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionCorrect.tsx +++ b/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionCorrect.tsx @@ -1,6 +1,7 @@ +import { useI18n } from '../../../../hooks' + import { GameQuestionComponentsProps } from '../GamePlaySessionQuestion.tsx' import { Celebrate } from '../../GameImage.tsx' -import { useI18n } from '../../../../hooks/useI18n.ts' export function GamePlaySessionQuestionCorrect ({ finishQuestion }: GameQuestionComponentsProps) { const $t = useI18n('pages.game.play.session') diff --git a/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionTimeExpired.tsx b/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionTimeExpired.tsx index 4fa803f..66e31c9 100644 --- a/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionTimeExpired.tsx +++ b/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionTimeExpired.tsx @@ -1,4 +1,4 @@ -import { useI18n } from '../../../../hooks/useI18n.ts' +import { useI18n } from '../../../../hooks' import { GameQuestionComponentsProps } from '../GamePlaySessionQuestion.tsx' import { Drink } from '../../GameImage.tsx' diff --git a/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionUnanswered.tsx b/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionUnanswered.tsx index 7db94d1..95e26ae 100644 --- a/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionUnanswered.tsx +++ b/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionUnanswered.tsx @@ -1,7 +1,7 @@ import Answer from '../../../../../src/Domain/Game/Answer.ts' import { Markdown } from '../../../general/Markdown.tsx' -import { useI18n } from '../../../../hooks/useI18n.ts' +import { useI18n } from '../../../../hooks' export type GameQuestionOptionsProps = { text: string diff --git a/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionWrong.tsx b/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionWrong.tsx index 18f1c62..e36b6a0 100644 --- a/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionWrong.tsx +++ b/view/components/game/game-play-session/game-play-session-question/GamePlaySessionQuestionWrong.tsx @@ -1,7 +1,7 @@ -import { useI18n } from '../../../../hooks/useI18n.ts' +import { useI18n } from '../../../../hooks' -import { GameQuestionComponentsProps } from '../GamePlaySessionQuestion.tsx' import { Drink } from '../../GameImage.tsx' +import { GameQuestionComponentsProps } from '../GamePlaySessionQuestion.tsx' export function GamePlaySessionQuestionWrong ({ finishQuestion }: GameQuestionComponentsProps) { const $t = useI18n('pages.game.play.session') diff --git a/view/components/general/Async.tsx b/view/components/general/Async.tsx index 38b6f7d..86bb7f8 100644 --- a/view/components/general/Async.tsx +++ b/view/components/general/Async.tsx @@ -1,5 +1,5 @@ import { ReactNode, useState } from 'react' -import { useRunOnce } from '../../hooks/useRunOnce.ts' +import { useRunOnce } from '../../hooks' export type AsyncElementProps = { children: ReactNode | ReactNode[] diff --git a/view/layouts/DashboardLayout.tsx b/view/layouts/DashboardLayout.tsx index c4625a6..3971404 100644 --- a/view/layouts/DashboardLayout.tsx +++ b/view/layouts/DashboardLayout.tsx @@ -1,16 +1,17 @@ -import { useApp } from '../hooks/useApp.ts' +import { useApp, useI18n, useLoading, useRunOnce } from '../hooks' import { Async, AsyncStatus, On } from '../components/general/Async.tsx' + +import { DashboardNavigation } from './dashboard/DashboardNavigation.tsx' import { LayoutLoading } from './general/LayoutLoading.tsx' import { LayoutNavbar } from './general/LayoutNavbar.tsx' -import { DashboardNavigation } from './dashboard/DashboardNavigation.tsx' -import { useI18n } from '../hooks/useI18n.ts' -import { useLoading } from '../hooks/useLoading.ts' export function DashboardLayout () { const $t = useI18n('layouts.dashboard') const { fall } = useLoading() const { session, auth } = useApp() + useRunOnce(() => document.title = $t('brand')) + return ( <> diff --git a/view/layouts/PublicLayout.tsx b/view/layouts/PublicLayout.tsx index 530b7e2..19c3632 100644 --- a/view/layouts/PublicLayout.tsx +++ b/view/layouts/PublicLayout.tsx @@ -1,9 +1,7 @@ import { Outlet } from 'react-router-dom' import { Async, AsyncStatus, On } from '../components/general/Async.tsx' -import { useApp } from '../hooks/useApp.ts' -import { useI18n } from '../hooks/useI18n.ts' -import { useLoading } from '../hooks/useLoading.ts' +import { useApp, useI18n, useLoading, useRunOnce } from '../hooks' import { LayoutLoading } from './general/LayoutLoading.tsx' import { LayoutNavbar } from './general/LayoutNavbar.tsx' @@ -13,6 +11,8 @@ export function PublicLayout () { const { fall } = useLoading() const { session, auth } = useApp() + useRunOnce(() => document.title = $t('brand')) + return ( <> diff --git a/view/layouts/dashboard/DashboardNavigation.tsx b/view/layouts/dashboard/DashboardNavigation.tsx index b6c3205..e0c4b2d 100644 --- a/view/layouts/dashboard/DashboardNavigation.tsx +++ b/view/layouts/dashboard/DashboardNavigation.tsx @@ -1,5 +1,6 @@ import { NavLink, Outlet } from 'react-router-dom' -import { useI18n } from '../../hooks/useI18n.ts' + +import { useI18n } from '../../hooks' export function DashboardNavigation () { const $t = useI18n('layouts.dashboard') diff --git a/view/layouts/general/LayoutLoading.tsx b/view/layouts/general/LayoutLoading.tsx index 29efa1e..c4173a3 100644 --- a/view/layouts/general/LayoutLoading.tsx +++ b/view/layouts/general/LayoutLoading.tsx @@ -1,9 +1,10 @@ -import { Loading } from '../../components/general/Loading.tsx' -import { If } from '../../components/general/Conditional.tsx' -import { loadingStore } from '../../stores/loading.ts' import { useEffect, useState } from 'react' import { useBeforeUnload } from 'react-router-dom' +import { loadingStore } from '../../stores/loading.ts' +import { Loading } from '../../components/general/Loading.tsx' +import { If } from '../../components/general/Conditional.tsx' + export function LayoutLoading ({ label, initial = true }: { label: string, initial?: boolean }) { const [loading, setLoading] = useState(initial) const [width, setWidth] = useState(0) diff --git a/view/layouts/general/LayoutNavbar.tsx b/view/layouts/general/LayoutNavbar.tsx index 6ea8527..6476f51 100644 --- a/view/layouts/general/LayoutNavbar.tsx +++ b/view/layouts/general/LayoutNavbar.tsx @@ -1,7 +1,7 @@ import { Link, useNavigate } from 'react-router-dom' import { Case, Switch } from '../../components/general/Conditional.tsx' import { AuthContract, Session } from '../../../src/Domain/Auth/Auth.ts' -import { useI18n } from '../../hooks/useI18n.ts' +import { useI18n } from '../../hooks' export type LayoutNavbarProps = { session: Session diff --git a/view/pages/HomePage.tsx b/view/pages/HomePage.tsx index b85fe7c..ff66b39 100644 --- a/view/pages/HomePage.tsx +++ b/view/pages/HomePage.tsx @@ -1,6 +1,7 @@ import { Link } from 'react-router-dom' + import { image } from '../../config/assets.ts' -import { useI18n } from '../hooks/useI18n.ts' +import { useI18n } from '../hooks' export function HomePage () { const $t = useI18n('pages.home') diff --git a/view/pages/dashboard/DahboardMyAccountPage.tsx b/view/pages/dashboard/DahboardMyAccountPage.tsx index 8d42ccb..c307169 100644 --- a/view/pages/dashboard/DahboardMyAccountPage.tsx +++ b/view/pages/dashboard/DahboardMyAccountPage.tsx @@ -1,4 +1,4 @@ -import { useI18n } from '../../hooks/useI18n.ts' +import { useI18n } from '../../hooks' export function DashboardMyAccountPage () { const $t = useI18n('pages.dashboard') diff --git a/view/pages/dashboard/DashboardGamesPage.tsx b/view/pages/dashboard/DashboardGamesPage.tsx index 42d6610..37d1260 100644 --- a/view/pages/dashboard/DashboardGamesPage.tsx +++ b/view/pages/dashboard/DashboardGamesPage.tsx @@ -1,4 +1,4 @@ -import { useI18n } from '../../hooks/useI18n.ts' +import { useI18n } from '../../hooks' export function DashboardGamesPage () { const $t = useI18n('pages.dashboard') diff --git a/view/pages/public/game/GameEndPage.tsx b/view/pages/public/game/GameEndPage.tsx index ba2c89e..165726d 100644 --- a/view/pages/public/game/GameEndPage.tsx +++ b/view/pages/public/game/GameEndPage.tsx @@ -1,8 +1,8 @@ -import { useNavigate, useParams } from 'react-router-dom' import { useEffect, useState } from 'react' +import { useNavigate, useParams } from 'react-router-dom' import { Done } from '../../../components/game/GameImage.tsx' -import { useI18n } from '../../../hooks/useI18n.ts' +import { useI18n } from '../../../hooks' export function GameEndPage () { const params = useParams() diff --git a/view/pages/public/game/GamePlayPage.tsx b/view/pages/public/game/GamePlayPage.tsx index f5f184f..f5f5ea7 100644 --- a/view/pages/public/game/GamePlayPage.tsx +++ b/view/pages/public/game/GamePlayPage.tsx @@ -8,8 +8,7 @@ import { Async, AsyncStatus, On } from '../../../components/general/Async.tsx' import { Loading } from '../../../components/general/Loading.tsx' import { AlertWarning } from '../../../components/general/Alert.tsx' import { GamePlaySession } from '../../../components/game/GamePlaySession.tsx' -import { useApp } from '../../../hooks/useApp.ts' -import { useI18n } from '../../../hooks/useI18n.ts' +import { useApp, useI18n } from '../../../hooks' export function GamePlayPage () { const params = useParams() diff --git a/view/pages/public/game/GameWelcomePage.tsx b/view/pages/public/game/GameWelcomePage.tsx index 47d12d2..f1dd021 100644 --- a/view/pages/public/game/GameWelcomePage.tsx +++ b/view/pages/public/game/GameWelcomePage.tsx @@ -3,13 +3,12 @@ import { useState } from 'react' import GameRepository from '../../../../src/Domain/Game/GameRepository.ts' import Game from '../../../../src/Domain/Game/Game.ts' -import { useApp } from '../../../hooks/useApp.ts' +import { useApp, useI18n } from '../../../hooks' + +import { AlertWarning } from '../../../components/general/Alert.tsx' import { Async, AsyncStatus, On } from '../../../components/general/Async.tsx' import { Loading } from '../../../components/general/Loading.tsx' -import { AlertWarning } from '../../../components/general/Alert.tsx' - import { GameList } from '../../../components/game/GameList.tsx' -import { useI18n } from '../../../hooks/useI18n.ts' export function GameWelcomePage () { const $t = useI18n('pages.game.welcome') diff --git a/vite-env.d.ts b/view/vite-env.d.ts similarity index 100% rename from vite-env.d.ts rename to view/vite-env.d.ts