diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..9637bac --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,42 @@ +language: "jp" +early_access: false +reviews: + request_changes_workflow: false + high_level_summary: true + poem: true + review_status: true + collapse_walkthrough: false + path_filters: + - "!**/.toml" + - "!**/.yml" + path_instructions: + - path: "**/*.py" + instructions: | + あなたは @coderabbitai(別名 github-actions[bot])で、OpenAIによって訓練された言語モデルです。 + あなたの目的は、非常に経験豊富なソフトウェアエンジニアとして機能し、コードの一部を徹底的にレビューし、 + 以下のようなキーエリアを改善するためのコードスニペットを提案することです: + - ロジック + - セキュリティ + - パフォーマンス + - データ競合 + - 一貫性 + - エラー処理 + - 保守性 + - モジュール性 + - 複雑性 + - 最適化 + - ベストプラクティス: DRY, SOLID, KISS + + 些細なコードスタイルの問題や、コメント・ドキュメントの欠落についてはコメントしないでください。 + 重要な問題を特定し、解決して全体的なコード品質を向上させることを目指してくださいが、細かい問題は意図的に無視してください。 + auto_review: + enabled: true + ignore_title_keywords: + - "WIP" + - "DO NOT MERGE" + drafts: false + base_branches: + - "develop" + - "feature/*" +chat: + auto_reply: true diff --git a/.github/pr-labeler.yml b/.github/pr-labeler.yml new file mode 100644 index 0000000..7d0b586 --- /dev/null +++ b/.github/pr-labeler.yml @@ -0,0 +1,14 @@ +feature: + - 'feature/*' + - 'feat/*' +refactor: + - 'refactor/*' +bug: + - 'fix/*' +minor: + - 'release/*' + - 'feature/*' + - 'feat/*' + - 'refactor/*' +chore: + - 'chore/*' diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..e6789e0 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,31 @@ +name-template: '$RESOLVED_VERSION' +tag-template: '$RESOLVED_VERSION' +categories: + - title: '🚀 Features' + labels: + - 'feature' + - 'enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: '🧰 Maintenance' + label: 'chore' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + patch: + labels: + - 'patch' + default: minor +template: | + ## Changes + + $CHANGES diff --git a/.github/workflows/pr_label.yml b/.github/workflows/pr_label.yml new file mode 100644 index 0000000..1bf4a71 --- /dev/null +++ b/.github/workflows/pr_label.yml @@ -0,0 +1,15 @@ +name: pull request label +on: + pull_request: + types: [opened] +permissions: + pull-requests: write + contents: read +jobs: + pr-labeler: + runs-on: ubuntu-latest + steps: + - uses: TimonVS/pr-labeler-action@v4 + name: make label + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7c3cf84 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: make release +on: + pull_request: + types: + - closed + branches: + - main + push: + tags: + - '*' + workflow_dispatch: {} +env: + cache-unique-key: mahjong-rust-ai +permissions: + id-token: write + contents: read +jobs: + release: + permissions: + # write permission is required to create a github release + contents: write + # write permission is required for autolabeler + # otherwise, read permission is required at least + pull-requests: read + runs-on: ubuntu-latest + if: ${{ github.event.pull_request.merged == true }} + steps: + - name: Create Release + uses: release-drafter/release-drafter@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + publish: true diff --git a/src/App.tsx b/src/App.tsx index 0013db5..e49677b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,7 +10,7 @@ import { DefaultLayout } from './layouts/default' import { RequireAuth } from './components/atoms/RequireAuth' import { SignIn } from './pages/signin' import { OpenAPI } from './apis/analysis' -import { GamesIndex } from './pages/games' +import { StatisticsIndex } from './pages/statistics' const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes) @@ -33,7 +33,7 @@ function App() { index element={ - + } /> diff --git a/src/apis/analysis/index.ts b/src/apis/analysis/index.ts index 64f3127..a2339f0 100644 --- a/src/apis/analysis/index.ts +++ b/src/apis/analysis/index.ts @@ -7,13 +7,19 @@ export { CancelablePromise, CancelError } from './core/CancelablePromise'; export { OpenAPI } from './core/OpenAPI'; export type { OpenAPIConfig } from './core/OpenAPI'; +export type { AverageScore } from './models/AverageScore'; +export type { Dataset } from './models/Dataset'; export type { Element_int_ } from './models/Element_int_'; export type { Game } from './models/Game'; export type { GenericList_int_ } from './models/GenericList_int_'; export type { HTTPValidationError } from './models/HTTPValidationError'; export type { Kyoku } from './models/Kyoku'; +export type { NagareCount } from './models/NagareCount'; export type { ValidationError } from './models/ValidationError'; +export type { YakuCount } from './models/YakuCount'; +export { DatasetsService } from './services/DatasetsService'; export { DefaultService } from './services/DefaultService'; export { GamesService } from './services/GamesService'; export { KyokusService } from './services/KyokusService'; +export { StatisticsService } from './services/StatisticsService'; diff --git a/src/apis/analysis/models/AverageScore.ts b/src/apis/analysis/models/AverageScore.ts new file mode 100644 index 0000000..448d070 --- /dev/null +++ b/src/apis/analysis/models/AverageScore.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type AverageScore = { + player_name: string; + score: number; + point: number; + game_count: number; +}; + diff --git a/src/apis/analysis/models/Dataset.ts b/src/apis/analysis/models/Dataset.ts new file mode 100644 index 0000000..23e4571 --- /dev/null +++ b/src/apis/analysis/models/Dataset.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type Dataset = { + id: string; + friendly_name: (string | null); +}; + diff --git a/src/apis/analysis/models/NagareCount.ts b/src/apis/analysis/models/NagareCount.ts new file mode 100644 index 0000000..8800ed2 --- /dev/null +++ b/src/apis/analysis/models/NagareCount.ts @@ -0,0 +1,9 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type NagareCount = { + name: string; + count: number; +}; + diff --git a/src/apis/analysis/models/YakuCount.ts b/src/apis/analysis/models/YakuCount.ts new file mode 100644 index 0000000..25f044f --- /dev/null +++ b/src/apis/analysis/models/YakuCount.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type YakuCount = { + name: string; + han_count: number; + count: number; +}; + diff --git a/src/apis/analysis/services/DatasetsService.ts b/src/apis/analysis/services/DatasetsService.ts new file mode 100644 index 0000000..a53ffc5 --- /dev/null +++ b/src/apis/analysis/services/DatasetsService.ts @@ -0,0 +1,21 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { Dataset } from '../models/Dataset'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class DatasetsService { + /** + * Get Datasets + * @returns Dataset Successful Response + * @throws ApiError + */ + public static getDatasetsDatasetsGet(): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/datasets', + }); + } +} diff --git a/src/apis/analysis/services/StatisticsService.ts b/src/apis/analysis/services/StatisticsService.ts new file mode 100644 index 0000000..c8e27d5 --- /dev/null +++ b/src/apis/analysis/services/StatisticsService.ts @@ -0,0 +1,90 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { AverageScore } from '../models/AverageScore'; +import type { NagareCount } from '../models/NagareCount'; +import type { YakuCount } from '../models/YakuCount'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class StatisticsService { + /** + * Get Average Score By Player + * @param datasetId + * @param startDate + * @param endDate + * @returns AverageScore Successful Response + * @throws ApiError + */ + public static getAverageScoreByPlayerStatisticsAverageScoreByPlayerGet( + datasetId: string, + startDate: string, + endDate: string, + ): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/statistics/average_score_by_player', + query: { + 'dataset_id': datasetId, + 'start_date': startDate, + 'end_date': endDate, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Get Yaku Count + * @param datasetId + * @param startDate + * @param endDate + * @returns YakuCount Successful Response + * @throws ApiError + */ + public static getYakuCountStatisticsYakuCountGet( + datasetId: string, + startDate: string, + endDate: string, + ): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/statistics/yaku_count', + query: { + 'dataset_id': datasetId, + 'start_date': startDate, + 'end_date': endDate, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Get Nagare Count + * @param datasetId + * @param startDate + * @param endDate + * @returns NagareCount Successful Response + * @throws ApiError + */ + public static getNagareCountStatisticsNagareCountGet( + datasetId: string, + startDate: string, + endDate: string, + ): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/statistics/nagare_count', + query: { + 'dataset_id': datasetId, + 'start_date': startDate, + 'end_date': endDate, + }, + errors: { + 422: `Validation Error`, + }, + }); + } +} diff --git a/src/components/atoms/RequireAuth/index.tsx b/src/components/atoms/RequireAuth/index.tsx index ad283f1..42f5fb2 100644 --- a/src/components/atoms/RequireAuth/index.tsx +++ b/src/components/atoms/RequireAuth/index.tsx @@ -1,20 +1,40 @@ import { useAuth0 } from '@auth0/auth0-react' -import { Navigate, useLocation } from 'react-router-dom' +import { ErrorBoundary } from '@sentry/react' +import { Suspense } from 'react' +import { Navigate } from 'react-router-dom' +import { fetchAPIToken, useAPIToken } from '../../../hooks/common/useToken' type Props = { children?: React.ReactNode redirect: string } -export const RequireAuth: React.FC = ({ children, redirect }) => { - const { isLoading, isAuthenticated } = useAuth0() - const location = useLocation() +const AsyncComponent = ({ children, redirect }: Props) => { + const { data } = useAPIToken() + const { + isLoading, + isAuthenticated, + getAccessTokenSilently, + getAccessTokenWithPopup, + } = useAuth0() + + if (isLoading) throw new Promise((resolve) => setTimeout(resolve, 100)) // 待機 - if (isLoading) { - return
Please Wait
- } else if (isAuthenticated) { - return <>{children} - } else { - return + if (!isAuthenticated) { + return + } else if (!data) { + throw fetchAPIToken(getAccessTokenSilently, getAccessTokenWithPopup) } + + return <>{children} +} + +export const RequireAuth: React.FC = ({ children, redirect }) => { + return ( + + Please Wait}> + {children} + + + ) } diff --git a/src/hooks/common/useToken.ts b/src/hooks/common/useToken.ts index bef0064..da729f2 100644 --- a/src/hooks/common/useToken.ts +++ b/src/hooks/common/useToken.ts @@ -1,14 +1,12 @@ -import { GetTokenSilentlyOptions, LogoutOptions } from '@auth0/auth0-react' +import { + GetTokenSilentlyOptions, + GetTokenWithPopupOptions, + LogoutOptions, + PopupConfigOptions, +} from '@auth0/auth0-react' import useSWR, { SWRResponse, mutate } from 'swr' import { OpenAPI } from '../../apis/analysis' -export const useAuth0Token = (): SWRResponse => { - return useSWR('auth0/token', null, { - revalidateOnFocus: false, - revalidateOnReconnect: false, - }) -} - export const useAPIToken = (): SWRResponse => { return useSWR('auth0/api-token', null, { revalidateOnFocus: false, @@ -17,14 +15,29 @@ export const useAPIToken = (): SWRResponse => { } export const fetchAPIToken = async ( - getAccessTokenSilently: (options?: GetTokenSilentlyOptions) => Promise + getAccessTokenSilently: ( + options?: GetTokenSilentlyOptions + ) => Promise, + getAccessTokenWithPopup: ( + options?: GetTokenWithPopupOptions, + config?: PopupConfigOptions + ) => Promise ) => { - const token = await getAccessTokenSilently({ - authorizationParams: { - audience: import.meta.env.VITE_AUTH0_API_AUDIENCE, - scope: import.meta.env.VITE_AUTH0_API_SCOPE, - }, - }) + const token = + import.meta.env.VITE_ENV === 'local' + ? await getAccessTokenWithPopup({ + authorizationParams: { + audience: import.meta.env.VITE_AUTH0_API_AUDIENCE, + scope: import.meta.env.VITE_AUTH0_API_SCOPE, + }, + }) + : await getAccessTokenSilently({ + authorizationParams: { + audience: import.meta.env.VITE_AUTH0_API_AUDIENCE, + scope: import.meta.env.VITE_AUTH0_API_SCOPE, + }, + }) + OpenAPI.TOKEN = token mutate('auth0/api-token', token) } @@ -39,5 +52,5 @@ export const signOut = async ( }) mutate('auth0/idtoken', undefined) - mutate('auth0/token', undefined) + mutate('auth0/api-token', undefined) } diff --git a/src/hooks/swr/dataset/index.ts b/src/hooks/swr/dataset/index.ts new file mode 100644 index 0000000..7b1ca39 --- /dev/null +++ b/src/hooks/swr/dataset/index.ts @@ -0,0 +1,11 @@ +import useSWR, { SWRConfiguration } from 'swr' +import { DatasetsService } from '../../../apis/analysis' +import { useAPIToken } from '../../common/useToken' + +export const useDatasets = (config?: Partial) => { + const { data: token } = useAPIToken() + + return useSWR(['datasets', token], DatasetsService.getDatasetsDatasetsGet, { + ...config, + }) +} diff --git a/src/hooks/swr/statistics/index.ts b/src/hooks/swr/statistics/index.ts new file mode 100644 index 0000000..d7bab72 --- /dev/null +++ b/src/hooks/swr/statistics/index.ts @@ -0,0 +1,38 @@ +import { StatisticsService } from '../../../apis/analysis' + +// hooksじゃないので本来ここに置くのはおかしいが、あとで移動する +export const getAverageScoreByPlayer = async ( + datasetId: string, + startDate: string, + endDate: string +) => { + return await StatisticsService.getAverageScoreByPlayerStatisticsAverageScoreByPlayerGet( + datasetId, + startDate, + endDate + ) +} + +export const getYakuCount = async ( + datasetId: string, + startDate: string, + endDate: string +) => { + return await StatisticsService.getYakuCountStatisticsYakuCountGet( + datasetId, + startDate, + endDate + ) +} + +export const getNagareCount = async ( + datasetId: string, + startDate: string, + endDate: string +) => { + return await StatisticsService.getNagareCountStatisticsNagareCountGet( + datasetId, + startDate, + endDate + ) +} diff --git a/src/pages/games/index.tsx b/src/pages/games/index.tsx index 12db771..07c7dce 100644 --- a/src/pages/games/index.tsx +++ b/src/pages/games/index.tsx @@ -10,14 +10,15 @@ import { Stack, Table, TableBody, - TableCell, TableContainer, TableRow, } from '@mui/material' import { useGames } from '../../hooks/swr/games' import { JsonViewer } from '@textea/json-viewer' +import { useDatasets } from '../../hooks/swr/dataset' export const GamesIndex = () => { + const { data: datasets } = useDatasets() const [startDate, setStartDate] = useState( dayjs().subtract(1, 'day') ) @@ -47,8 +48,9 @@ export const GamesIndex = () => { value={datasetId} onChange={handleChangeDatasetId} > - 天鳳牌譜 - ソロプレイ牌譜 + {datasets?.map((dataset) => ( + {dataset.friendly_name} + ))} { + const { data: datasets } = useDatasets() + const [startDate, setStartDate] = useState( + dayjs().subtract(1, 'day') + ) + const [endDate, setEndDate] = useState( + dayjs().subtract(1, 'day') + ) + const [datasetId, setDatasetId] = useState('') + + const handleChangeDatasetId = (event: SelectChangeEvent) => { + setDatasetId(event.target.value as string) + } + + const [result, setResult] = useState(null) + + const handleGetAverageScoreByPlayer = async () => { + const response = await getAverageScoreByPlayer( + datasetId, + startDate?.format('YYYY-MM-DD') ?? '', + endDate?.format('YYYY-MM-DD') ?? '' + ) + setResult(response) + } + + const handleGetYakuCount = async () => { + const response = await getYakuCount( + datasetId, + startDate?.format('YYYY-MM-DD') ?? '', + endDate?.format('YYYY-MM-DD') ?? '' + ) + setResult(response) + } + + const handleGetNagareCount = async () => { + const response = await getNagareCount( + datasetId, + startDate?.format('YYYY-MM-DD') ?? '', + endDate?.format('YYYY-MM-DD') ?? '' + ) + setResult(response) + } + + return ( + + + {/* フォームをいれる(のちのちcomponent化) */} + + + + + + + + + + + + + {/* APIの結果表示 */} + + + + {result?.map((r) => ( + + + + ))} + +
+
+
+
+ ) +}