-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/#129 add ranking #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds a ranking feature that displays stamp usage statistics in both total and monthly views. The implementation includes a new page component with loading states, error handling, and tabbed ranking tables, along with a dedicated composable for managing the ranking data fetching logic.
Key Changes:
- New ranking page with lazy-loading approach (user clicks button to load data)
- Composable for fetching and processing stamp ranking data from multiple API endpoints
- Tabbed interface showing total and monthly usage rankings with pagination
Reviewed Changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| client/app/pages/ranking.vue | New page component that displays stamp rankings with multiple UI states (initial, loading, error, data display) and tabbed views for total and monthly rankings |
| client/app/composables/useRanking.ts | New composable that handles fetching ranking and stamp data, merges them, and provides reactive state management with error handling |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| :items-per-page="tableTotal?.tableApi?.getState().pagination.pageSize" | ||
| :total="tableTotal?.tableApi?.getFilteredRowModel().rows.length" |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessing nested optional properties with optional chaining (tableTotal.value?.tableApi?.getState()...) could fail if tableApi is not yet initialized during the initial render. Consider adding a fallback value or wrapping this in a computed property that checks for existence before accessing these properties to prevent potential runtime errors.
| <UPagination | ||
| v-model:page="pageMonthly" | ||
| :items-per-page="tableMonthly?.tableApi?.getState().pagination.pageSize" | ||
| :total="tableMonthly?.tableApi?.getFilteredRowModel().rows.length" |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the total ranking pagination, accessing nested optional properties with optional chaining could fail if tableApi is not yet initialized. Consider adding appropriate fallback values or null checks to prevent potential runtime errors.
| :total="tableMonthly?.tableApi?.getFilteredRowModel().rows.length" | |
| :total="tableMonthly?.tableApi?.getFilteredRowModel().rows.length ?? 0" |
| const { ranking, loading, error, initialized, loadRanking, refresh } = useRanking(); | ||
|
|
||
| // 初期状態にリセットする関数 | ||
| const resetToInitial = () => { | ||
| // composable内部の状態をリセット(実装上は新しいインスタンスの作成が必要) | ||
| // ここでは簡易的にページリロード | ||
| window.location.reload(); |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using window.location.reload() forces a full page reload, which is not ideal for a Vue/Nuxt SPA. Consider implementing a proper reset mechanism in the useRanking composable by adding a reset() method that clears the internal state, rather than forcing a page reload. This would provide a better user experience and follow Vue best practices.
| const { ranking, loading, error, initialized, loadRanking, refresh } = useRanking(); | |
| // 初期状態にリセットする関数 | |
| const resetToInitial = () => { | |
| // composable内部の状態をリセット(実装上は新しいインスタンスの作成が必要) | |
| // ここでは簡易的にページリロード | |
| window.location.reload(); | |
| const { ranking, loading, error, initialized, loadRanking, refresh, reset } = useRanking(); | |
| // 初期状態にリセットする関数 | |
| const resetToInitial = () => { | |
| // composable内部の状態をリセット | |
| reset(); |
| console.log('ランキングデータの読み込みを開始...'); | ||
|
|
||
| // 1. 両方のAPIを並行して取得 | ||
| const [rankingResponse, stampsResponse] = await Promise.all([ | ||
| apiClient.GET('/stamps/ranking'), | ||
| apiClient.GET('/stamps'), | ||
| ]); | ||
|
|
||
| console.log('API レスポンス - ranking:', rankingResponse); | ||
| console.log('API レスポンス - stamps:', stampsResponse); | ||
|
|
||
| // 2. エラーハンドリング | ||
| if (rankingResponse.error) { | ||
| throw new Error(`ランキングデータの取得に失敗しました: ${rankingResponse.error}`); | ||
| } | ||
| if (stampsResponse.error) { | ||
| throw new Error(`スタンプデータの取得に失敗しました: ${stampsResponse.error}`); | ||
| } | ||
|
|
||
| // 3. データの準備 | ||
| const rankings = rankingResponse.data || []; | ||
| const stamps = stampsResponse.data || []; | ||
|
|
||
| console.log(`取得データ - rankings: ${rankings.length}件, stamps: ${stamps.length}件`); | ||
|
|
||
| // 4. スタンプIDをキーとしたMapを作成 | ||
| const stampMap = new Map(stamps.map(stamp => [stamp.stamp_id, stamp])); | ||
|
|
||
| // 5. データを結合して処理済みランキングデータを生成 | ||
| const processedData = rankings.map(item => ({ | ||
| stamp_id: item.stamp_id, | ||
| total_count: item.total_count, | ||
| monthly_count: item.monthly_count, | ||
| stamp_name: stampMap.get(item.stamp_id)?.stamp_name ?? '不明なスタンプ', | ||
| rank: 0, // ソート時に設定 | ||
| count: item.total_count, // 初期値 | ||
| })); | ||
|
|
||
| ranking.value = processedData; | ||
| initialized.value = true; | ||
|
|
||
| console.log('ランキングデータの処理完了:', processedData.length, '件'); |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multiple console.log statements (lines 28, 36, 37, 51, 69) are left in production code. These should either be removed or replaced with a proper logging mechanism. Console logs in production code can expose sensitive information and clutter browser consoles.
client/app/pages/ranking.vue
Outdated
| variant="pill" | ||
| class="w-full" | ||
| > | ||
| > |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a stray > character on this line that will cause a syntax error. This should be removed.
| > | |
| <div class="flex items-center gap-3"> | ||
| <NuxtImg | ||
| :src="getFileUrl(row.original.stamp_id)" | ||
| class="m-auto w-12 h-12" |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The class m-auto should be mx-auto to match the consistent usage in line 162. Using m-auto sets margin on all sides, while mx-auto (used in the monthly ranking template) only centers horizontally, which is the correct approach for centering images.
| class="m-auto w-12 h-12" | |
| class="mx-auto w-12 h-12" |
|
|
||
| // メダル表示のマップ | ||
| const medalMap: Record<number, string> = { | ||
| 1: '👑 1', |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The medalMap object should use 🥇 (first place medal) for rank 1 instead of 👑 (crown) to maintain consistency with the medal theme established by ranks 2 (🥈) and 3 (🥉). If a crown is specifically desired for first place, consider documenting this design choice in a comment.
| 1: '👑 1', | |
| 1: '🥇 1', |
| throw new Error(`ランキングデータの取得に失敗しました: ${rankingResponse.error}`); | ||
| } | ||
| if (stampsResponse.error) { | ||
| throw new Error(`スタンプデータの取得に失敗しました: ${stampsResponse.error}`); |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error messages in lines 41 and 44 embed the raw error object in the string, which may not provide useful information to users. Consider extracting the error message or status code from the response object. For example: ${rankingResponse.error.message || 'Unknown error'} or handle specific error codes to provide more user-friendly messages.
| throw new Error(`ランキングデータの取得に失敗しました: ${rankingResponse.error}`); | |
| } | |
| if (stampsResponse.error) { | |
| throw new Error(`スタンプデータの取得に失敗しました: ${stampsResponse.error}`); | |
| throw new Error(`ランキングデータの取得に失敗しました: ${ | |
| typeof rankingResponse.error === 'object' | |
| ? rankingResponse.error.message || JSON.stringify(rankingResponse.error) | |
| : rankingResponse.error || 'Unknown error' | |
| }`); | |
| } | |
| if (stampsResponse.error) { | |
| throw new Error(`スタンプデータの取得に失敗しました: ${ | |
| typeof stampsResponse.error === 'object' | |
| ? stampsResponse.error.message || JSON.stringify(stampsResponse.error) | |
| : stampsResponse.error || 'Unknown error' | |
| }`); |
| const refresh = async () => { | ||
| initialized.value = false; | ||
| ranking.value = []; | ||
| await loadRanking(); |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The refresh function resets initialized to false and clears ranking, but if loadRanking is called when data already exists, it will return early (lines 20-22) without actually refreshing. This creates a logical inconsistency - refresh should always fetch fresh data. Consider removing the early return check in loadRanking or adding a parameter to force reload.
No description provided.