Skip to content

Feature/add circle graph and flask#3351

Open
river0525 wants to merge 33 commits intostagingfrom
feature/add_circle_graph
Open

Feature/add circle graph and flask#3351
river0525 wants to merge 33 commits intostagingfrom
feature/add_circle_graph

Conversation

@river0525
Copy link
Copy Markdown
Collaborator

@river0525 river0525 commented Apr 3, 2026

投票機能に関する改修です。

以下の変更を行いました。

  • 統計ページの棒グラフをドーナツグラフに変更
  • フラスコアイコンを一覧表・投票ページの暫定グレードに追加

ローカルでcoderabbitによるレビューを4回行いましたが、念のためご確認よろしくお願いいたします。

Summary by CodeRabbit

リリースノート

  • New Features

    • 投票分布を円形チャートで可視化
    • 暫定グレードに視覚的インジケータを追加
    • タスク詳細への外部リンク追加
  • UI Changes

    • 投票ページを再設計(グレード・問題名・出典を表示)
    • ナビゲーション「グレード投票」→「投票」に変更
    • 検索・ソート機能を強化

river0525 and others added 30 commits April 1, 2026 10:02
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ookup

Median grade may have zero votes and therefore no segment, causing the
indicator line to be invisible. Compute the angle from the cumulative
distribution so zero-vote grades are handled correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The chart always starts at the top so the 50th percentile always
falls at the bottom of the ring. Remove getGradeAngle and draw
the line with hardcoded coordinates instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Using userSpaceOnUse with CSS custom property stop-color failed to render.
Switch to objectBoundingBox so the gradient spans the segment itself,
and hardcode the D6 color value (#432414).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- max-w-xs → max-w-md for a larger chart
- Add votedGrade prop; prefix matching segment label with ✅

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- OUTER_RADIUS 90→120, INNER_RADIUS 55→70 (ring width 35→50)
- CX/CY 130→160/155, viewBox 260×275→320×310
- max-w-md→max-w-lg
- Scale up font sizes proportionally

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove ExternalLinkWrapper; clicking the title now navigates to the
contest problem page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove footnote paragraph and "暫定グレード:" text
- Show flask icon left of grade icon when stats (provisional grade) exist
- Tooltip on flask explains the provisional grade rule

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Navbar: グレード投票 → 投票
- Default: show empty state (search required) to reduce initial load
- Search: limit to 20 results, sorted by task_id desc (newer first)
- Table: outer border + rounded corners, lighter row dividers
- Column order: グレード | 問題名 | 出典 | 票数
- Grade column: flask icon for provisional grades, "-" when no grade
- 問題名: add external link icon to problem page
- 出典: use getContestNameLabel helper
- service: add grade field to TaskWithVoteInfo, sort desc

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
task_id includes the problem letter suffix (_a, _b, ...) and localeCompare
is locale-dependent. contest_id comparison is consistent with
the existing compareByContestIdAndTaskId helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add task_table_index to TaskWithVoteInfo so the existing helper can be
used. This matches the grade-based view sort order: contest type
priority → contest_id desc → task_table_index asc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
river0525 and others added 2 commits April 3, 2026 09:06
Grade button itself is unchanged; flask appears to its right inside
an inline-flex wrapper. Visible only when task grade is PENDING and
an estimated grade from votes is being displayed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Widen compareByContestIdAndTaskId signature to accept any object with
  contest_id/task_table_index, removing double cast in +page.svelte
- Replace find() loop with Map in buildDonutSegments (O(n+m))
- Extract segLabel {@const} in VoteDonutChart to remove duplicate ternary
- Add aria-label to FlaskConical in VotableGrade
- Add aria-label to external link icon in votes list page
- Add qGrades/dGrades tests to grade_options.test.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 3, 2026

📝 Walkthrough

Walkthrough

投票機能に新しいドーナツチャート表示を追加し、タスク情報にグレードとテーブルインデックスを拡張。グレード分類の新しいユーティリティ、検索・ソート機能強化、ナビゲーション・ラベルの更新を実施。

Changes

Cohort / File(s) Summary
ドーナツチャート表示
src/features/votes/components/VoteDonutChart.svelte, src/features/votes/utils/donut_chart.ts, src/features/votes/utils/donut_chart.test.ts
投票分布をドーナツチャートで可視化。角度計算、セグメント生成、SVGパスレンダリング機能を実装。
投票サービス拡張
src/features/votes/services/vote_statistics.ts, src/features/votes/services/vote_statistics.test.ts
TaskWithVoteInfo型にgradetask_table_indexフィールドを追加し、ソート順序を昇順から降順へ変更。テストケースを更新。
グレード分類ユーティリティ
src/features/votes/utils/grade_options.ts, src/features/votes/utils/grade_options.test.ts
qGrades(Q系)とdGrades(D系)の新しいエクスポートを追加。
投票ページUI更新
src/routes/votes/+page.svelte
検索・ソート機能を強化し、グレード・問題名・出典列へレイアウト変更。外部リンクアイコン、provisional grade表示を追加。
投票詳細ページUI更新
src/routes/votes/[slug]/+page.svelte
ドーナツチャートコンポーネントを統合し、投票フォーム表示を再構成。provisional grade表示にツールチップを追加。
VotableGrade indicator
src/features/votes/components/VotableGrade.svelte
Provisional状態の可視化のためFlaskConicalアイコンを追加。
ユーティリティ更新
src/lib/utils/task.ts
compareByContestIdAndTaskIdのシグネチャを構造的型指定に変更。
ナビゲーション
src/lib/constants/navbar-links.ts
VOTES_PAGEのラベルを「グレード投票」→「投票」に変更。

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

ドーナツ揺れて投票を舞い、
セグメント輝く虹色に咲き、
暫定グレード光の矢で
Q と D が並び立つ
検索精錬、リンク統一の美 🎨📊

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed タイトルはプルリクエストの主な変更内容(ドーナツチャート追加とフラスコアイコン表示)を適切に要約しており、変更セット全体と関連している。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/add_circle_graph

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/features/votes/components/VotableGrade.svelte (1)

54-73: 🧹 Nitpick | 🔵 Trivial

ビジネスロジックの抽出を検討

onTriggerClick 内の fetch 処理は utils/ への切り出しが推奨されます(コーディングガイドライン準拠)。ただし、コンポーネント状態と密結合のため、現状でも許容範囲です。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/votes/components/VotableGrade.svelte` around lines 54 - 73, The
fetch logic inside the component's onTriggerClick function should be extracted
into a reusable utility to satisfy the coding guideline: create a new helper
(e.g., getMyVote or fetchMyVote) in the utils folder that accepts taskId and
returns the parsed JSON (or throws on error); then replace the inline fetch in
onTriggerClick with a call to that util and assign votedGrade from its result
while preserving the existing isOpening guard and try/catch/finally flow in
onTriggerClick so component state (isOpening, votedGrade) remains correct.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/features/votes/components/VoteDonutChart.svelte`:
- Around line 44-51: 空リングの半径計算が誤っており通常セグメントと形状が一致しないので、VoteDonutChart.svelte の該当
<circle>(現在 r={OUTER_RADIUS} を使っている箇所)を修正して中心半径を (INNER_RADIUS + OUTER_RADIUS) /
2 に変更し、stroke-width は従来どおり OUTER_RADIUS - INNER_RADIUS
を維持してください。これにより空リングの内外径が通常セグメント(INNER/OUTER)と一致します。

In `@src/lib/constants/navbar-links.ts`:
- Line 22: Three e2e tests in votes.spec.ts expect the old heading "グレード投票" and
should be updated to the new label "投票"; update those three assertions to expect
"投票". In the page component +page.svelte for the votes/[slug] route, change the
breadcrumb text currently showing "グレード投票" to "投票". Also find and update related
copy strings such as "この問題のグレードを投票してください" to use the consistent term "投票" (e.g.,
"この問題に投票してください") so navigation, heading, breadcrumb and in-page instruction all
match.

In `@src/routes/votes/`[slug]/+page.svelte:
- Around line 27-29: Move the display-grade decision logic out of the page
script into a utility function: create a helper in utils (e.g.,
getDisplayGrade(taskGrade: TaskGrade, stats?: Stats) that implements the PENDING
fallback (return stats.grade when taskGrade === TaskGrade.PENDING and
stats?.grade exists, otherwise return taskGrade). Replace the inline expression
that defines displayGrade with a call to that util (pass data.task.grade and
data.stats) and import the util at the top of the component; keep references to
TaskGrade, data.task.grade and data.stats.grade intact so callers behave the
same.
- Around line 53-54: Update the anchor(s) in +page.svelte that use
target="_blank" to include "noopener" in their rel attribute; specifically find
the <a> elements with attributes rel="noreferrer external" and target="_blank"
and change the rel value to include noopener (e.g., "noopener noreferrer
external") so the links open securely without allowing the new page to access
window.opener.
- Around line 42-47: The span with id "flask-icon" is not focusable so keyboard
users cannot open the Tooltip; make the trigger element (span id="flask-icon")
keyboard-accessible by adding tabindex="0" and an appropriate aria-label (e.g.,
aria-label="中央値の説明") and, if the span is acting like a control, role="button";
also ensure the Tooltip component (Tooltip triggeredBy="#flask-icon") still
binds to keyboard activation (handle Enter/Space keydown to toggle/show the
Tooltip) so both mouse and keyboard users can open the explanatory tooltip.

In `@src/routes/votes/`+page.svelte:
- Around line 68-71: Extract the duplicated provisional-grade logic into a
shared utility (e.g., a function like computeGradeInfo or two functions
computeIsProvisional and computeDisplayGrade) and use it from both +page.svelte
and VotableGrade.svelte; specifically, centralize the checks that reference
TaskGrade, task.grade and task.estimatedGrade (isProvisional: grade ===
TaskGrade.PENDING && estimatedGrade !== null; displayGrade: grade !==
TaskGrade.PENDING ? grade : (estimatedGrade ?? grade)) so both components call
the shared utility instead of duplicating the logic.

---

Outside diff comments:
In `@src/features/votes/components/VotableGrade.svelte`:
- Around line 54-73: The fetch logic inside the component's onTriggerClick
function should be extracted into a reusable utility to satisfy the coding
guideline: create a new helper (e.g., getMyVote or fetchMyVote) in the utils
folder that accepts taskId and returns the parsed JSON (or throws on error);
then replace the inline fetch in onTriggerClick with a call to that util and
assign votedGrade from its result while preserving the existing isOpening guard
and try/catch/finally flow in onTriggerClick so component state (isOpening,
votedGrade) remains correct.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 648b94d1-760a-4ae0-b64e-59e1e9cedc7c

📥 Commits

Reviewing files that changed from the base of the PR and between 58084cd and 0cfdb09.

📒 Files selected for processing (12)
  • src/features/votes/components/VotableGrade.svelte
  • src/features/votes/components/VoteDonutChart.svelte
  • src/features/votes/services/vote_statistics.test.ts
  • src/features/votes/services/vote_statistics.ts
  • src/features/votes/utils/donut_chart.test.ts
  • src/features/votes/utils/donut_chart.ts
  • src/features/votes/utils/grade_options.test.ts
  • src/features/votes/utils/grade_options.ts
  • src/lib/constants/navbar-links.ts
  • src/lib/utils/task.ts
  • src/routes/votes/+page.svelte
  • src/routes/votes/[slug]/+page.svelte

Comment on lines +44 to +51
<circle
cx={CX}
cy={CY}
r={OUTER_RADIUS}
fill="none"
stroke="currentColor"
stroke-width={OUTER_RADIUS - INNER_RADIUS}
class="text-gray-200 dark:text-gray-700"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

0票時リングの半径計算がずれており、通常時と形状が一致しません。

Line 47 の r={OUTER_RADIUS} だと、空リングの実効内外径が通常セグメント(INNER/OUTER)と一致しません。
空リングは「中心半径 = (INNER+OUTER)/2」を使ってください。

修正案
-    <circle
+    <circle
       cx={CX}
       cy={CY}
-      r={OUTER_RADIUS}
+      r={(OUTER_RADIUS + INNER_RADIUS) / 2}
       fill="none"
       stroke="currentColor"
       stroke-width={OUTER_RADIUS - INNER_RADIUS}
       class="text-gray-200 dark:text-gray-700"
       opacity="0.5"
     />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<circle
cx={CX}
cy={CY}
r={OUTER_RADIUS}
fill="none"
stroke="currentColor"
stroke-width={OUTER_RADIUS - INNER_RADIUS}
class="text-gray-200 dark:text-gray-700"
<circle
cx={CX}
cy={CY}
r={(OUTER_RADIUS + INNER_RADIUS) / 2}
fill="none"
stroke="currentColor"
stroke-width={OUTER_RADIUS - INNER_RADIUS}
class="text-gray-200 dark:text-gray-700"
opacity="0.5"
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/votes/components/VoteDonutChart.svelte` around lines 44 - 51,
空リングの半径計算が誤っており通常セグメントと形状が一致しないので、VoteDonutChart.svelte の該当 <circle>(現在
r={OUTER_RADIUS} を使っている箇所)を修正して中心半径を (INNER_RADIUS + OUTER_RADIUS) / 2
に変更し、stroke-width は従来どおり OUTER_RADIUS - INNER_RADIUS
を維持してください。これにより空リングの内外径が通常セグメント(INNER/OUTER)と一致します。

Comment on lines +99 to +107
const largeArcFlag = endAngle - startAngle > Math.PI ? 1 : 0;

return [
`M ${x1} ${y1}`,
`A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} 1 ${x2} ${y2}`,
`L ${x3} ${y3}`,
`A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 0 ${x4} ${y4}`,
'Z',
].join(' ');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In SVG path syntax, when using the A command, if the arc start point and end point are identical, is a full circle guaranteed to render? What is the recommended way to draw a full donut segment (100%)?

💡 Result:

No, a full circle is not guaranteed to render when using the A command with identical start and end points. The SVG specification states that if the endpoint of an elliptical arc segment is identical to the current point, the arc segment is entirely omitted. This is a defined error-handling rule for out-of-range parameters in elliptical arcs. The recommended way to draw a full donut segment (100% ring) in SVG path syntax is to use two consecutive elliptical arc commands that together complete exactly 360 degrees, typically starting from a point on the circumference: M cx cy-r a r r 0 1 0 -(r2) 0 a r r 0 1 0 (r2) 0 Z Where (cx, cy) is the center and r is the radius (adjust for inner/outer radii for thickness). This creates a closed path approximating a perfect circle. Alternatively, use elements with stroke for simpler donuts, but since the question specifies path syntax, the dual-arc path is standard. For donuts, draw outer ring path minus inner hole path, or use compound paths/subpaths.

Citations:


🏁 Script executed:

fd -t f donut_chart.ts

Repository: AtCoder-NoviSteps/AtCoderNoviSteps

Length of output: 117


🏁 Script executed:

find . -path "*test*" -name "*donut*" -type f

Repository: AtCoder-NoviSteps/AtCoderNoviSteps

Length of output: 124


🏁 Script executed:

find . -path "*spec*" -name "*donut*" -type f

Repository: AtCoder-NoviSteps/AtCoderNoviSteps

Length of output: 60


🏁 Script executed:

cat -n src/features/votes/utils/donut_chart.ts

Repository: AtCoder-NoviSteps/AtCoderNoviSteps

Length of output: 3959


🏁 Script executed:

cat -n src/features/votes/utils/donut_chart.test.ts

Repository: AtCoder-NoviSteps/AtCoderNoviSteps

Length of output: 3389


100%セグメント時にSVGパスが描画されない重大な問題があります。

arcPath() は全円(endAngle - startAngle ≈ 2π)の場合に対応していません。SVG仕様により、開始点と終了点が同一の場合、arcコマンドは完全に省略されるため、100%セグメントのドーナツ外周が描画されません。

現在のテストは buildDonutSegments で100%セグメント生成を確認していますが、arcPath に対する完全円角度のテストが欠落しており、コーディングガイドラインの「各関数は adjacent unit test を有すること」を満たしていません。

修正案のように、全周近傍で分岐して2つのarcコマンドで360度を描画する必要があります。

修正案(full-circle分岐)
 export function arcPath(
   cx: number,
   cy: number,
   outerRadius: number,
   innerRadius: number,
   startAngle: number,
   endAngle: number,
 ): string {
+  const sweep = endAngle - startAngle;
+  if (Math.abs(sweep) >= TAU - 1e-9) {
+    const sxOuter = cx + outerRadius * Math.cos(startAngle);
+    const syOuter = cy + outerRadius * Math.sin(startAngle);
+    const mxOuter = cx + outerRadius * Math.cos(startAngle + Math.PI);
+    const myOuter = cy + outerRadius * Math.sin(startAngle + Math.PI);
+    const sxInner = cx + innerRadius * Math.cos(startAngle);
+    const syInner = cy + innerRadius * Math.sin(startAngle);
+    const mxInner = cx + innerRadius * Math.cos(startAngle + Math.PI);
+    const myInner = cy + innerRadius * Math.sin(startAngle + Math.PI);
+
+    return [
+      `M ${sxOuter} ${syOuter}`,
+      `A ${outerRadius} ${outerRadius} 0 1 1 ${mxOuter} ${myOuter}`,
+      `A ${outerRadius} ${outerRadius} 0 1 1 ${sxOuter} ${syOuter}`,
+      `L ${sxInner} ${syInner}`,
+      `A ${innerRadius} ${innerRadius} 0 1 0 ${mxInner} ${myInner}`,
+      `A ${innerRadius} ${innerRadius} 0 1 0 ${sxInner} ${syInner}`,
+      'Z',
+    ].join(' ');
+  }
+
   const x1 = cx + outerRadius * Math.cos(startAngle);
   const y1 = cy + outerRadius * Math.sin(startAngle);
   const x2 = cx + outerRadius * Math.cos(endAngle);
   const y2 = cy + outerRadius * Math.sin(endAngle);
   const x3 = cx + innerRadius * Math.cos(endAngle);
   const y3 = cy + innerRadius * Math.sin(endAngle);
   const x4 = cx + innerRadius * Math.cos(startAngle);
   const y4 = cy + innerRadius * Math.sin(startAngle);
   const largeArcFlag = endAngle - startAngle > Math.PI ? 1 : 0;

加えて、arcPath に対して100%セグメントケースをテストするテストケースを追加してください。

{ title: `問題集`, path: WORKBOOKS_PAGE },
{ title: `一覧表`, path: PROBLEMS_PAGE },
{ title: `グレード投票`, path: VOTES_PAGE },
{ title: `投票`, path: VOTES_PAGE },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: アプリケーション内で "グレード投票" または "投票" の用語が一貫して使用されているかを確認

# "グレード投票" の使用箇所を検索
echo "=== 'グレード投票' の使用箇所 ==="
rg -n 'グレード投票' --type ts --type tsx --type js --type jsx

# "投票" の使用箇所を検索(コンテキスト付き)
echo -e "\n=== '投票' の使用箇所(ナビゲーション/タイトル関連) ==="
rg -n -C2 '投票' --type ts --type tsx --type js --type jsx -g '!node_modules' | head -50

Repository: AtCoder-NoviSteps/AtCoderNoviSteps

Length of output: 209


🏁 Script executed:

# ファイルの内容を確認
cat -n src/lib/constants/navbar-links.ts

# 正しいファイル拡張子でグレード投票と投票を検索
echo "=== グレード投票 の検索 ==="
rg 'グレード投票' --type-list | grep -i ts && rg 'グレード投票' --type ts .

echo "=== 投票 の検索(ナビゲーション、ページ関連) ==="
rg '投票' -g '*.ts' -g '*.js' -g '*.tsx' -g '*.jsx' | head -20

Repository: AtCoder-NoviSteps/AtCoderNoviSteps

Length of output: 2775


🏁 Script executed:

# e2e テストの該当部分を確認
cat -n e2e/votes.spec.ts | grep -A5 -B5 'グレード投票'

# 投票ページの実装を確認
find . -path ./node_modules -prune -o -name "+page.svelte" -o -name "+page.ts" | grep -i vote

# ページのメタデータやヘッダー設定を確認
rg -n "グレード投票|投票" -g "*.svelte" -g "*.ts" -g "*.js" src/routes/votes

Repository: AtCoder-NoviSteps/AtCoderNoviSteps

Length of output: 2404


🏁 Script executed:

# 投票ページのメインページの完全な内容を確認
cat -n src/routes/votes/+page.svelte

# 詳細ページの該当部分も確認
cat -n src/routes/votes/[slug]/+page.svelte | head -100

Repository: AtCoder-NoviSteps/AtCoderNoviSteps

Length of output: 9754


e2e テストの失敗が発生します。ナビゲーションバーのラベルを「投票」に変更していますが、以下の項目が更新されていません:

  • e2e/votes.spec.ts の3つのテスト(17行目、36行目、107行目)が、ページ見出しに「グレード投票」を期待しており、失敗します
  • src/routes/votes/[slug]/+page.svelte 35行目のパンくずリンクが「グレード投票」のまま。ナビゲーションと一貫性がありません
  • 関連テキスト(「この問題のグレードを投票してください」)との用語の整合性を確認してください

対応が必要:

  • e2e テスト3箇所を「投票」に更新
  • パンくずリンクテキストも「投票」に統一
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/constants/navbar-links.ts` at line 22, Three e2e tests in
votes.spec.ts expect the old heading "グレード投票" and should be updated to the new
label "投票"; update those three assertions to expect "投票". In the page component
+page.svelte for the votes/[slug] route, change the breadcrumb text currently
showing "グレード投票" to "投票". Also find and update related copy strings such as
"この問題のグレードを投票してください" to use the consistent term "投票" (e.g., "この問題に投票してください") so
navigation, heading, breadcrumb and in-page instruction all match.

Comment on lines +27 to +29
const displayGrade = $derived(
data.task.grade === TaskGrade.PENDING && data.stats?.grade ? data.stats.grade : data.task.grade,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

表示グレード決定ロジックは utils に切り出してください。

Line 27-29 は業務ルール(PENDING 時の代替表示)なので、route の <script> 直書きではなく utils に抽出したほうが保守性と再利用性が上がります。
As per coding guidelines, "Business logic belongs in utils/, not inside <script> blocks."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/votes/`[slug]/+page.svelte around lines 27 - 29, Move the
display-grade decision logic out of the page script into a utility function:
create a helper in utils (e.g., getDisplayGrade(taskGrade: TaskGrade, stats?:
Stats) that implements the PENDING fallback (return stats.grade when taskGrade
=== TaskGrade.PENDING and stats?.grade exists, otherwise return taskGrade).
Replace the inline expression that defines displayGrade with a call to that util
(pass data.task.grade and data.stats) and import the util at the top of the
component; keep references to TaskGrade, data.task.grade and data.stats.grade
intact so callers behave the same.

Comment on lines +42 to +47
<span id="flask-icon" class="cursor-help text-gray-500 dark:text-gray-400">
<FlaskConical class="w-5 h-5" />
</span>
<Tooltip triggeredBy="#flask-icon" placement="bottom">
3票以上集まると中央値が暫定グレードとして一覧表に反映されます。
</Tooltip>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Tooltip トリガーがキーボード操作不可です。

Line 42 の span はフォーカスできないため、キーボード利用者が補足説明を開けません。tabindex="0"aria-label(必要なら role="button")を追加してください。

修正案
-      <span id="flask-icon" class="cursor-help text-gray-500 dark:text-gray-400">
+      <span
+        id="flask-icon"
+        class="cursor-help text-gray-500 dark:text-gray-400"
+        tabindex="0"
+        role="button"
+        aria-label="暫定グレードの説明を表示"
+      >
         <FlaskConical class="w-5 h-5" />
       </span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span id="flask-icon" class="cursor-help text-gray-500 dark:text-gray-400">
<FlaskConical class="w-5 h-5" />
</span>
<Tooltip triggeredBy="#flask-icon" placement="bottom">
3票以上集まると中央値が暫定グレードとして一覧表に反映されます。
</Tooltip>
<span
id="flask-icon"
class="cursor-help text-gray-500 dark:text-gray-400"
tabindex="0"
role="button"
aria-label="暫定グレードの説明を表示"
>
<FlaskConical class="w-5 h-5" />
</span>
<Tooltip triggeredBy="#flask-icon" placement="bottom">
3票以上集まると中央値が暫定グレードとして一覧表に反映されます。
</Tooltip>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/votes/`[slug]/+page.svelte around lines 42 - 47, The span with id
"flask-icon" is not focusable so keyboard users cannot open the Tooltip; make
the trigger element (span id="flask-icon") keyboard-accessible by adding
tabindex="0" and an appropriate aria-label (e.g., aria-label="中央値の説明") and, if
the span is acting like a control, role="button"; also ensure the Tooltip
component (Tooltip triggeredBy="#flask-icon") still binds to keyboard activation
(handle Enter/Space keydown to toggle/show the Tooltip) so both mouse and
keyboard users can open the explanatory tooltip.

Comment on lines +53 to +54
rel="noreferrer external"
target="_blank"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

target="_blank" のリンクに noopener を追加してください。

Line 53-54 は noreferrer があっても、noopener を明示しておくほうが安全です。

  • 例: rel="noopener noreferrer external"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/votes/`[slug]/+page.svelte around lines 53 - 54, Update the
anchor(s) in +page.svelte that use target="_blank" to include "noopener" in
their rel attribute; specifically find the <a> elements with attributes
rel="noreferrer external" and target="_blank" and change the rel value to
include noopener (e.g., "noopener noreferrer external") so the links open
securely without allowing the new page to access window.opener.

Comment on lines +68 to +71
{@const isProvisional =
task.grade === TaskGrade.PENDING && task.estimatedGrade !== null}
{@const displayGrade =
task.grade !== TaskGrade.PENDING ? task.grade : (task.estimatedGrade ?? task.grade)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

暫定グレード判定ロジックの重複

この判定ロジックは VotableGrade.svelte (lines 47-49) と重複しています。共通のユーティリティ関数への抽出を検討してください。

  • isProvisional: grade === PENDING && estimatedGrade !== null
  • displayGrade: grade !== PENDING ? grade : (estimatedGrade ?? grade)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/routes/votes/`+page.svelte around lines 68 - 71, Extract the duplicated
provisional-grade logic into a shared utility (e.g., a function like
computeGradeInfo or two functions computeIsProvisional and computeDisplayGrade)
and use it from both +page.svelte and VotableGrade.svelte; specifically,
centralize the checks that reference TaskGrade, task.grade and
task.estimatedGrade (isProvisional: grade === TaskGrade.PENDING &&
estimatedGrade !== null; displayGrade: grade !== TaskGrade.PENDING ? grade :
(estimatedGrade ?? grade)) so both components call the shared utility instead of
duplicating the logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants