-
Notifications
You must be signed in to change notification settings - Fork 75
プロフィールとダッシュボードに連続学習記録の表示を追加 #9173
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
Changes from all commits
ed399dd
5aa10fe
9d2e257
d283a19
82bcd57
82b5c03
5cc2f75
79ce2d6
3b39936
13559af
88c1530
7abe0f4
03aa758
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| .a-card.streak-card | ||
| .card-header.is-sm | ||
| h2.card-header__title | ||
| | 学習連続記録 | ||
| hr.a-border-tint | ||
| .card-body | ||
| .streak-container | ||
| .streak-item | ||
| .streak-item__content | ||
| .streak-item__label | ||
| | 現在の連続記録 | ||
| .streak-item__days | ||
| .streak-item__number | ||
| = current_streak_days | ||
| .streak-item__unit | ||
| | 日 | ||
| - if current_streak? | ||
| .streak-item__period | ||
| = current_streak_period | ||
| .streak-item | ||
| .streak-item__content | ||
| .streak-item__label | ||
| | 連続最高記録 | ||
| .streak-item__days | ||
| .streak-item__number | ||
| = longest_streak_days | ||
| .streak-item__unit | ||
| | 日 | ||
| - if longest_streak? | ||
| .streak-item__period | ||
| = longest_streak_period |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class StudyStreak::StudyStreakTrackerComponent < ViewComponent::Base | ||
| def initialize(study_streak:) | ||
| @study_streak = study_streak | ||
| end | ||
|
|
||
| def current_streak? | ||
| @study_streak.current_days.to_i.positive? | ||
| end | ||
|
|
||
| def longest_streak? | ||
| @study_streak.longest_days.to_i.positive? | ||
| end | ||
|
|
||
| def current_streak_days | ||
| @study_streak.current_days.to_i | ||
| end | ||
|
|
||
| def longest_streak_days | ||
| @study_streak.longest_days.to_i | ||
| end | ||
|
|
||
| def current_streak_period | ||
| format_period( | ||
| days: @study_streak.current_days, | ||
| start_on: @study_streak.current_start_on, | ||
| end_on: @study_streak.current_end_on | ||
| ) | ||
| end | ||
|
|
||
| def longest_streak_period | ||
| format_period( | ||
| days: @study_streak.longest_days, | ||
| start_on: @study_streak.longest_start_on, | ||
| end_on: @study_streak.longest_end_on | ||
| ) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def format_period(days:, start_on:, end_on:) | ||
| return '' if days.to_i.zero? || start_on.blank? || end_on.blank? | ||
|
|
||
| format = if start_on.year == end_on.year && start_on.year == Time.zone.today.year | ||
| :sm | ||
| else | ||
| :short | ||
| end | ||
| "#{I18n.l(start_on, format:)} 〜 #{I18n.l(end_on, format:)}" | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| @use "../../../config/mixins/break-points" | ||
| @use "../../../config/mixins/text" | ||
|
|
||
| .streak-card | ||
| margin-bottom: 1.5rem | ||
|
|
||
| .streak-container | ||
| display: flex | ||
| justify-content: center | ||
| +break-points.media-breakpoint-up(md) | ||
| gap: 1rem | ||
| +break-points.media-breakpoint-down(sm) | ||
| gap: .5rem | ||
jun-kondo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| .streak-item | ||
| flex: 1 | ||
| max-width: 10rem | ||
| display: flex | ||
| justify-content: center | ||
| align-items: center | ||
| +break-points.media-breakpoint-up(md) | ||
| padding-block: 1rem | ||
| +break-points.media-breakpoint-down(sm) | ||
| padding-block: .75rem | ||
|
|
||
| .streak-item__content | ||
| display: flex | ||
| flex-direction: column | ||
| align-items: center | ||
|
|
||
| .streak-item__days | ||
| display: flex | ||
| align-items: baseline | ||
|
|
||
| .streak-item__number | ||
| font-weight: 700 | ||
| color: #f7941d | ||
| +break-points.media-breakpoint-up(md) | ||
| +text.text-block(2.5rem 1) | ||
| +break-points.media-breakpoint-down(sm) | ||
| +text.text-block(2rem 1) | ||
|
|
||
| .streak-item__label | ||
| font-weight: 600 | ||
| text-align: center | ||
| +break-points.media-breakpoint-up(md) | ||
| +text.text-block(1rem 1.4) | ||
| +break-points.media-breakpoint-down(sm) | ||
| +text.text-block(.875rem 1.4) | ||
|
|
||
| .streak-item__unit | ||
| text-align: center | ||
| color: var(--muted-text) | ||
| +break-points.media-breakpoint-up(md) | ||
| +text.text-block(1rem 1.4) | ||
| +break-points.media-breakpoint-down(sm) | ||
| +text.text-block(.75rem 1.4) | ||
|
|
||
| .streak-item__period | ||
| margin-top: .125rem | ||
| text-align: center | ||
| +break-points.media-breakpoint-up(md) | ||
| +text.text-block(.875rem 1.4) | ||
| +break-points.media-breakpoint-down(sm) | ||
| +text.text-block(.75rem 1.4) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| class StudyStreak | ||
| attr_reader :current_start_on, :current_end_on, :current_days, :longest_start_on, :longest_end_on, :longest_days | ||
|
|
||
| def initialize(reports, include_wip: false) | ||
| @include_wip = include_wip | ||
| @study_dates = report_dates(reports) | ||
|
|
||
| current_period = streak_periods.last | ||
| @current_start_on = current_period&.[](:start_on) | ||
| @current_end_on = current_period&.[](:end_on) | ||
| @current_days = current_period&.[](:days) | ||
|
|
||
| longest_period = find_longest_period | ||
| @longest_start_on = longest_period&.[](:start_on) | ||
| @longest_end_on = longest_period&.[](:end_on) | ||
| @longest_days = longest_period&.[](:days) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| attr_reader :study_dates, :include_wip | ||
|
|
||
| def report_dates(reports) | ||
| reports = reports.not_wip unless include_wip | ||
| reports.order(reported_on: :asc).pluck(:reported_on) | ||
| end | ||
|
|
||
| def streak_periods | ||
| @streak_periods ||= begin | ||
| return [] if study_dates.empty? | ||
|
|
||
| study_dates.chunk_while { |a, b| b == a.next_day }.map do |chunk| | ||
| { start_on: chunk.first, end_on: chunk.last, days: chunk.size } | ||
| end | ||
| end | ||
| end | ||
|
|
||
| def find_longest_period | ||
| # days が最大、同点ならより新しい end_on を優先 | ||
| streak_periods.max_by { |p| [p[:days], p[:end_on]] } | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'test_helper' | ||
| require 'supports/report_helper' | ||
|
|
||
| class StudyStreak::StudyStreakTrackerComponentTest < ViewComponent::TestCase | ||
| include ReportHelper | ||
|
|
||
| setup do | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 他の場所にあわせるならば上に1行空行をあけるといいかも
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. こちらリベースで修正致しました。 test/components/study_streak/study_streak_tracker_component_test.rb |
||
| travel_to Time.zone.local(2024, 9, 1) | ||
|
|
||
| @user = users(:kimura) | ||
|
|
||
| LearningTime.joins(:report).where(reports: { user_id: @user.id }).delete_all | ||
| Report.where(user: @user, reported_on: Date.new(2024, 8, 1)..Time.zone.today).delete_all | ||
|
|
||
| submitted_dates = %w[ | ||
| 2024-08-07 2024-08-08 2024-08-09 | ||
| 2024-08-20 2024-08-21 2024-08-22 | ||
| ] | ||
| submitted_dates.each { |d| create_report_data_with_learning_times(user: @user, on: d) } | ||
| reports = @user.reports_with_learning_times | ||
| @study_streak = StudyStreak.new(reports) | ||
| end | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| teardown do | ||
| travel_back | ||
| end | ||
|
Comment on lines
+26
to
+28
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. teardownは最後の処理なので、setupの次に書くか、ファイルの最後に書くのがわかりやすいとおもいます。(他のテストもほとんどそうなっているとおもいます)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. すみません。setupのブロックが長くなっていて分かり辛いのですが、このテストファイルではsetupの次にteardownを書いています。
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jun-kondo すみません、読み間違っていました。 |
||
|
|
||
| test 'renders current streak information' do | ||
| render_inline(StudyStreak::StudyStreakTrackerComponent.new(study_streak: @study_streak)) | ||
|
|
||
| # 現在の連続学習日数と期間を表示する | ||
| assert_selector '.streak-container' | ||
| assert_text '日' | ||
| assert_selector '.streak-item__number', text: '3', count: 2 | ||
| assert_selector '.streak-item__period', text: '08/20 〜 08/22' | ||
| end | ||
|
|
||
| test 'renders longest streak information (ties resolved by most recent)' do | ||
| render_inline(StudyStreak::StudyStreakTrackerComponent.new(study_streak: @study_streak)) | ||
|
|
||
| # 最長連続日数も3日で、同率の場合は最新の期間を選ぶ | ||
| assert_selector '.streak-item__number', text: '3' | ||
| assert_selector '.streak-item__period', text: '08/20 〜 08/22' | ||
| end | ||
|
|
||
| test 'renders zero streak when no learning days' do | ||
| user_without_learning = users(:hatsuno) # learning_times を含むレポートがないユーザー | ||
| reports = user_without_learning.reports_with_learning_times | ||
| study_streak = StudyStreak.new(reports, include_wip: false) | ||
| render_inline(StudyStreak::StudyStreakTrackerComponent.new(study_streak:)) | ||
|
|
||
| # 0日を表示し、期間テキストは表示しない | ||
| assert_selector '.streak-item__number', text: '0', count: 2 | ||
|
|
||
| # 期間要素は連続日数が1以上のときのみ条件付きでレンダリングされる | ||
| refute_selector '.streak-item__period' | ||
| end | ||
|
|
||
| test 'structure has required CSS classes' do | ||
| render_inline(StudyStreak::StudyStreakTrackerComponent.new(study_streak: @study_streak)) | ||
|
|
||
| assert_selector '.streak-container' | ||
| assert_selector '.streak-item', count: 2 | ||
| assert_selector '.streak-item__content', count: 2 | ||
| assert_selector '.streak-item__number', count: 2 | ||
| assert_selector '.streak-item__unit', text: '日', count: 2 | ||
| assert_selector '.streak-item__label', count: 2 | ||
| end | ||
|
|
||
| test 'date format follows "mm/dd 〜 mm/dd" pattern for the current year' do | ||
| render_inline(StudyStreak::StudyStreakTrackerComponent.new(study_streak: @study_streak)) | ||
|
|
||
| assert_selector '.streak-item__period', text: '08/20 〜 08/22' | ||
| end | ||
|
|
||
| test 'date format follows "yyyy/mm/dd 〜 yyyy/mm/dd" pattern for a past year' do | ||
| travel_to Time.zone.local(2025, 1, 1) | ||
| render_inline(StudyStreak::StudyStreakTrackerComponent.new(study_streak: @study_streak)) | ||
| assert_selector '.streak-item__period', text: '2024/08/20 〜 2024/08/22' | ||
| end | ||
|
|
||
| test 'date format follows "yyyy/mm/dd 〜 yyyy/mm/dd" pattern across different years' do | ||
| user = users(:machida) | ||
| LearningTime.joins(:report).where(reports: { user_id: user.id }).delete_all | ||
| Report.where(user:).delete_all | ||
| submitted_dates = %w[2023-12-30 2023-12-31 2024-01-01 2024-01-02] | ||
| submitted_dates.each { |date| create_report_data_with_learning_times(user:, on: date) } | ||
| reports = user.reports_with_learning_times | ||
| study_streak = StudyStreak.new(reports) | ||
|
|
||
| render_inline(StudyStreak::StudyStreakTrackerComponent.new(study_streak:)) | ||
| assert_selector '.streak-item__period', text: '2023/12/30 〜 2024/01/02' | ||
| end | ||
| end | ||
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.
componentとして作っているのがいいですね!
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.
ありがとうございます。View Componentは使ったことがなかったので触ってみたいと思っていました。
プレゼンテーションロジックをコンポーネントのクラスにまとめられたことや単体テストが出来てテストが容易であることが利点だなと感じました👀