diff --git a/apps/govquests-api/govquests/gamification/lib/gamification.rb b/apps/govquests-api/govquests/gamification/lib/gamification.rb index 32e0a39a..46ec9df4 100644 --- a/apps/govquests-api/govquests/gamification/lib/gamification.rb +++ b/apps/govquests-api/govquests/gamification/lib/gamification.rb @@ -4,6 +4,7 @@ require_relative "gamification/game_profile" require_relative "gamification/leaderboard" require_relative "gamification/badge" +require_relative "gamification/user_badge" ACTION_BADGE_NAMESPACE_UUID = "5FA78373-03E0-4D0B-91D1-3F2C6CA3F088" @@ -77,5 +78,9 @@ class CommandHandler < Infra::CommandHandlerRegistry handle "Gamification::CreateBadge", aggregate: Badge do |badge, cmd| badge.create(cmd.display_data, cmd.badgeable_id, cmd.badgeable_type) end + + handle "Gamification::EarnBadge", aggregate: UserBadge do |user_badge, cmd| + user_badge.earn_badge(cmd.user_id, cmd.badgeable_id, cmd.badgeable_type, cmd.earned_at) + end end end diff --git a/apps/govquests-api/govquests/gamification/lib/gamification/commands.rb b/apps/govquests-api/govquests/gamification/lib/gamification/commands.rb index af7e3245..2d8217dc 100644 --- a/apps/govquests-api/govquests/gamification/lib/gamification/commands.rb +++ b/apps/govquests-api/govquests/gamification/lib/gamification/commands.rb @@ -53,13 +53,6 @@ class MaintainStreak < Infra::Command alias_method :aggregate_id, :profile_id end - class EarnBadge < Infra::Command - attribute :profile_id, Infra::Types::UUID - attribute :badge, Infra::Types::String - - alias_method :aggregate_id, :profile_id - end - class UpdateLeaderboard < Infra::Command attribute :leaderboard_id, Infra::Types::UUID attribute :profile_id, Infra::Types::UUID @@ -76,4 +69,13 @@ class CreateBadge < Infra::Command alias_method :aggregate_id, :badge_id end + + class EarnBadge < Infra::Command + attribute :user_id, Infra::Types::UUID + attribute :badgeable_id, Infra::Types::String + attribute :badgeable_type, Infra::Types::String + attribute :earned_at, Infra::Types::DateTime.default { Time.current.to_datetime } + + alias_method :aggregate_id, :user_id + end end diff --git a/apps/govquests-api/govquests/gamification/lib/gamification/events.rb b/apps/govquests-api/govquests/gamification/lib/gamification/events.rb index 3fbabd63..c4ac2788 100644 --- a/apps/govquests-api/govquests/gamification/lib/gamification/events.rb +++ b/apps/govquests-api/govquests/gamification/lib/gamification/events.rb @@ -46,12 +46,7 @@ class StreakMaintained < Infra::Event attribute :profile_id, Infra::Types::UUID attribute :streak, Infra::Types::Integer end - - class BadgeEarned < Infra::Event - attribute :profile_id, Infra::Types::UUID - attribute :badge, Infra::Types::String - end - + class LeaderboardUpdated < Infra::Event attribute :leaderboard_id, Infra::Types::UUID attribute :profile_id, Infra::Types::UUID @@ -64,4 +59,11 @@ class BadgeCreated < Infra::Event attribute :badgeable_id, Infra::Types::UUID attribute :badgeable_type, Infra::Types::String end + + class BadgeEarned < Infra::Event + attribute :user_id, Infra::Types::UUID + attribute :badgeable_id, Infra::Types::String + attribute :badgeable_type, Infra::Types::String + attribute :earned_at, Infra::Types::DateTime + end end diff --git a/apps/govquests-api/govquests/gamification/lib/gamification/user_badge.rb b/apps/govquests-api/govquests/gamification/lib/gamification/user_badge.rb new file mode 100644 index 00000000..cd3fdf5b --- /dev/null +++ b/apps/govquests-api/govquests/gamification/lib/gamification/user_badge.rb @@ -0,0 +1,27 @@ +module Gamification + class UserBadge + include AggregateRoot + + def initialize(id) + @id = id + end + + def earn_badge(user_id, badgeable_id, badgeable_type, earned_at) + apply BadgeEarned.new(data: { + user_id:, + badgeable_id:, + badgeable_type:, + earned_at:, + }) + end + + private + + on BadgeEarned do |event| + @user_id = event.data[:user_id] + @badgeable_id = event.data[:badgeable_id] + @badgeable_type = event.data[:badgeable_type] + @earned_at = event.data[:earned_at] + end + end +end diff --git a/apps/govquests-api/govquests/processes/lib/processes.rb b/apps/govquests-api/govquests/processes/lib/processes.rb index dba79645..ec490c4c 100644 --- a/apps/govquests-api/govquests/processes/lib/processes.rb +++ b/apps/govquests-api/govquests/processes/lib/processes.rb @@ -10,6 +10,8 @@ require_relative "processes/notify_on_badge_earned" require_relative "processes/notify_on_tier_achieved" require_relative "processes/create_badge_on_track_or_quest_created" +require_relative "processes/reward_badge_on_quest_or_track_completed" +require_relative "processes/complete_track_on_quest_completed" require_relative "processes/deliver_notification_on_created" module Processes @@ -20,13 +22,15 @@ def call(event_store, command_bus) UpdateProfileOnRewardIssued.new(event_store, command_bus).subscribe DistributeRewardsOnQuestCompleted.new(event_store, command_bus).subscribe CreateBadgeOnTrackOrQuestCreated.new(event_store, command_bus).subscribe + RewardBadgeOnQuestOrTrackCompleted.new(event_store, command_bus).subscribe + CompleteTrackOnQuestCompleted.new(event_store, command_bus).subscribe NotifyOnQuestCompleted.new(event_store, command_bus).subscribe NotifyOnRewardIssued.new(event_store, command_bus).subscribe NotifyOnBadgeEarned.new(event_store, command_bus).subscribe NotifyOnTierAchieved.new(event_store, command_bus).subscribe - DeliverNotificationOnCreated.new(event_store, command_bus).subscribe + DeliverNotificationOnCreated.new(event_store, command_bus).subscribe end end end diff --git a/apps/govquests-api/govquests/processes/lib/processes/complete_track_on_quest_completed.rb b/apps/govquests-api/govquests/processes/lib/processes/complete_track_on_quest_completed.rb new file mode 100644 index 00000000..5ca39da6 --- /dev/null +++ b/apps/govquests-api/govquests/processes/lib/processes/complete_track_on_quest_completed.rb @@ -0,0 +1,38 @@ +module Processes + class CompleteTrackOnQuestCompleted + def initialize(event_store, command_bus) + @event_store = event_store + @command_bus = command_bus + end + + def subscribe + @event_store.subscribe(self, to: [Questing::QuestCompleted]) + end + + def call(event) + quest = Questing::QuestReadModel.find_by(quest_id: event.data[:quest_id]) + return unless quest&.track_id + + track = quest.track + user_id = event.data[:user_id] + + completed_quests_count = track.quests + .joins(:user_quests) + .where( + user_quests: { + user_id: user_id, + status: "completed" + } + ).count + + if completed_quests_count == track.quests.count + @command_bus.call( + Questing::CompleteTrack.new( + user_id: user_id, + track_id: track.track_id + ) + ) + end + end + end +end \ No newline at end of file diff --git a/apps/govquests-api/govquests/processes/lib/processes/create_badge_on_track_or_quest_created.rb b/apps/govquests-api/govquests/processes/lib/processes/create_badge_on_track_or_quest_created.rb index 260b9ac6..a6d5f640 100644 --- a/apps/govquests-api/govquests/processes/lib/processes/create_badge_on_track_or_quest_created.rb +++ b/apps/govquests-api/govquests/processes/lib/processes/create_badge_on_track_or_quest_created.rb @@ -10,19 +10,27 @@ def subscribe end def call(event) - display_data = event.data[:badge_display_data] - entity_type = event.class.name.split('::').last.gsub('Created', '') - entity_id = event.data["#{entity_type.downcase}_id".to_sym] + source_name = event.class.name.split('::').last.gsub('Created', '') + source_type = "::Questing::#{source_name}ReadModel" - badge_id = Gamification.generate_badge_id(entity_type, entity_id) + source_id_field = "#{source_name.downcase}_id".to_sym + source_uuid = event.data[source_id_field] + + source_sequential_id = source_type.constantize.find_by(source_id_field => source_uuid).id + + display_data = event.data[:badge_display_data].merge({ + sequence_number: source_sequential_id + }) + + badge_id = Gamification.generate_badge_id(source_type, source_uuid) @command_bus.call( ::Gamification::CreateBadge.new( badge_id:, display_data:, - badgeable_id: entity_id, - badgeable_type: entity_type + badgeable_id: source_uuid, + badgeable_type: source_name ) ) end diff --git a/apps/govquests-api/govquests/processes/lib/processes/notify_on_badge_earned.rb b/apps/govquests-api/govquests/processes/lib/processes/notify_on_badge_earned.rb index 91240dbd..217028d9 100644 --- a/apps/govquests-api/govquests/processes/lib/processes/notify_on_badge_earned.rb +++ b/apps/govquests-api/govquests/processes/lib/processes/notify_on_badge_earned.rb @@ -10,14 +10,18 @@ def subscribe end def call(event) - profile_id = event.data[:profile_id] - badge = event.data[:badge] + user_id = event.data[:user_id] + badgeable_id = event.data[:badgeable_id] + badgeable_type = event.data[:badgeable_type] + + badge_record = badgeable_type.constantize.find_by(id: badgeable_id) + badge_title = badge_record&.display_data&.dig("title") if badge_record @command_bus.call( ::Notifications::CreateNotification.new( notification_id: SecureRandom.uuid, - user_id: profile_id, - content: "Congratulations! You've earned the #{badge} badge!", + user_id: , + content: "Congratulations! You've earned the #{badge_title} badge!", notification_type: "badge_earned" ) ) diff --git a/apps/govquests-api/govquests/processes/lib/processes/reward_badge_on_quest_or_track_completed.rb b/apps/govquests-api/govquests/processes/lib/processes/reward_badge_on_quest_or_track_completed.rb new file mode 100644 index 00000000..4ed70bc8 --- /dev/null +++ b/apps/govquests-api/govquests/processes/lib/processes/reward_badge_on_quest_or_track_completed.rb @@ -0,0 +1,40 @@ +module Processes + class RewardBadgeOnQuestOrTrackCompleted + def initialize(event_store, command_bus) + @event_store = event_store + @command_bus = command_bus + end + + def subscribe + @event_store.subscribe(self, to: [::Questing::QuestCompleted, ::Questing::TrackCompleted]) + end + + def call(event) + user_id = event.data[:user_id] + + entity_name = event.class.name.split('::').last.gsub('Completed', '') + entity_type = case entity_name + when 'Quest' then 'Questing::QuestReadModel' + when 'Track' then 'Questing::TrackReadModel' + end + entity_id = event.data["#{entity_name.downcase}_id".to_sym] + + entity = entity_type.constantize.find_by("#{entity_name.downcase}_id": entity_id) + + badge = Gamification::BadgeReadModel.find_by( + badgeable_type: entity_type, + badgeable_id: entity.id.to_s, + ) + + return unless badge + + @command_bus.call( + ::Gamification::EarnBadge.new( + user_id:, + badgeable_id: badge.id.to_s, + badgeable_type: badge.class.name, + ) + ) + end + end +end diff --git a/apps/govquests-api/govquests/questing/lib/questing.rb b/apps/govquests-api/govquests/questing/lib/questing.rb index 99c2c628..b424a15f 100644 --- a/apps/govquests-api/govquests/questing/lib/questing.rb +++ b/apps/govquests-api/govquests/questing/lib/questing.rb @@ -59,5 +59,9 @@ class CommandHandler < Infra::CommandHandlerRegistry badge_display_data: cmd.badge_display_data ) end + + handle "Questing::CompleteTrack", aggregate: Track do |track, cmd| + track.complete(user_id: cmd.user_id) + end end end diff --git a/apps/govquests-api/govquests/questing/lib/questing/commands.rb b/apps/govquests-api/govquests/questing/lib/questing/commands.rb index 7109332a..c20b3998 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/commands.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/commands.rb @@ -57,4 +57,11 @@ class CreateTrack < Infra::Command alias_method :aggregate_id, :track_id end + + class CompleteTrack < Infra::Command + attribute :user_id, Infra::Types::UUID + attribute :track_id, Infra::Types::UUID + + alias_method :aggregate_id, :track_id + end end diff --git a/apps/govquests-api/govquests/questing/lib/questing/events.rb b/apps/govquests-api/govquests/questing/lib/questing/events.rb index edca5617..c047a9e5 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/events.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/events.rb @@ -43,4 +43,9 @@ class TrackCreated < Infra::Event attribute :quest_ids, Infra::Types::Array attribute :badge_display_data, Infra::Types::Hash end + + class TrackCompleted < Infra::Event + attribute :user_id, Infra::Types::UUID + attribute :track_id, Infra::Types::UUID + end end diff --git a/apps/govquests-api/govquests/questing/lib/questing/track.rb b/apps/govquests-api/govquests/questing/lib/questing/track.rb index 73ab3566..9595f8a5 100644 --- a/apps/govquests-api/govquests/questing/lib/questing/track.rb +++ b/apps/govquests-api/govquests/questing/lib/questing/track.rb @@ -24,10 +24,20 @@ def create(display_data:, quest_ids:, badge_display_data:) ) end + def complete(user_id:) + apply(TrackCompleted.new(data: { + user_id:, + track_id: @id + })) + end + on TrackCreated do |event| @display_data = event.data[:display_data] @quest_ids = event.data[:quest_ids] @badge_display_data = event.data[:badge_display_data] end + + on TrackCompleted do |event| + end end end diff --git a/apps/govquests-api/rails_app/app/graphql/resolvers/fetch_badges.rb b/apps/govquests-api/rails_app/app/graphql/resolvers/fetch_badges.rb index 375dc269..db828596 100644 --- a/apps/govquests-api/rails_app/app/graphql/resolvers/fetch_badges.rb +++ b/apps/govquests-api/rails_app/app/graphql/resolvers/fetch_badges.rb @@ -2,17 +2,24 @@ module Resolvers class FetchBadges < BaseResolver type [Types::BadgeType], null: true - argument :special, Boolean, required: false, default_value: nil + def resolve + current_user = context[:current_user] + badges = Gamification::BadgeReadModel.includes(:user_badges) - def resolve(special:) - case special - when true - Gamification::BadgeReadModel.special - when false - Gamification::BadgeReadModel.normal - else - Gamification::BadgeReadModel.all - end + return badges unless current_user + + earned_badge_ids = Gamification::UserBadgeReadModel + .where(user_id: current_user.user_id) + .where(badgeable_type: "Gamification::BadgeReadModel") + .pluck(:badgeable_id) + + return badges if earned_badge_ids.empty? + + badges.order( + Arel.sql( + "CASE WHEN badges.id IN (#{earned_badge_ids.join(",")}) THEN 0 ELSE 1 END" + ) + ) end end end diff --git a/apps/govquests-api/rails_app/app/graphql/resolvers/fetch_special_badge.rb b/apps/govquests-api/rails_app/app/graphql/resolvers/fetch_special_badge.rb new file mode 100644 index 00000000..10245549 --- /dev/null +++ b/apps/govquests-api/rails_app/app/graphql/resolvers/fetch_special_badge.rb @@ -0,0 +1,11 @@ +module Resolvers + class FetchSpecialBadge < BaseResolver + type Types::SpecialBadgeType, null: true + + argument :id, ID, required: true + + def resolve(id:) + Gamification::SpecialBadgeReadModel.find_by(badge_id: id) + end + end +end diff --git a/apps/govquests-api/rails_app/app/graphql/resolvers/fetch_special_badges.rb b/apps/govquests-api/rails_app/app/graphql/resolvers/fetch_special_badges.rb new file mode 100644 index 00000000..677c69a2 --- /dev/null +++ b/apps/govquests-api/rails_app/app/graphql/resolvers/fetch_special_badges.rb @@ -0,0 +1,25 @@ +module Resolvers + class FetchSpecialBadges < BaseResolver + type [Types::SpecialBadgeType], null: true + + def resolve + current_user = context[:current_user] + badges = Gamification::SpecialBadgeReadModel.includes(:user_badges) + + return badges unless current_user + + earned_badge_ids = Gamification::UserBadgeReadModel + .where(user_id: current_user.user_id) + .where(badgeable_type: "Gamification::SpecialBadgeReadModel") + .pluck(:badgeable_id) + + return badges if earned_badge_ids.empty? + + badges.order( + Arel.sql( + "CASE WHEN special_badges.id IN (#{earned_badge_ids.join(",")}) THEN 0 ELSE 1 END" + ) + ) + end + end +end diff --git a/apps/govquests-api/rails_app/app/graphql/types/badge_display_data_type.rb b/apps/govquests-api/rails_app/app/graphql/types/badge_display_data_type.rb index 6430f767..b42c467a 100644 --- a/apps/govquests-api/rails_app/app/graphql/types/badge_display_data_type.rb +++ b/apps/govquests-api/rails_app/app/graphql/types/badge_display_data_type.rb @@ -3,5 +3,6 @@ class BadgeDisplayDataType < Types::BaseObject field :title, String, null: true field :description, String, null: true field :image_url, String, null: true + field :sequence_number, Integer, null: true end end diff --git a/apps/govquests-api/rails_app/app/graphql/types/badge_type.rb b/apps/govquests-api/rails_app/app/graphql/types/badge_type.rb index f1a08c90..765eef1e 100644 --- a/apps/govquests-api/rails_app/app/graphql/types/badge_type.rb +++ b/apps/govquests-api/rails_app/app/graphql/types/badge_type.rb @@ -3,10 +3,15 @@ class BadgeType < Types::BaseObject field :id, ID, null: false, method: :badge_id field :display_data, Types::BadgeDisplayDataType, null: false field :badgeable, Types::BadgeableUnion, null: true - field :special, Boolean, null: + field :user_badges, [Types::UserBadgeType], null: false + field :earned_by_current_user, Boolean, null: false - def special - object.special? + def user_badges + object.user_badges.where(user: context[:current_user]) + end + + def earned_by_current_user + object.user_badges.exists?(user: context[:current_user]) end end end diff --git a/apps/govquests-api/rails_app/app/graphql/types/query_type.rb b/apps/govquests-api/rails_app/app/graphql/types/query_type.rb index 1fa9abdc..6d522dd4 100644 --- a/apps/govquests-api/rails_app/app/graphql/types/query_type.rb +++ b/apps/govquests-api/rails_app/app/graphql/types/query_type.rb @@ -29,6 +29,9 @@ def nodes(ids:) field :badge, resolver: Resolvers::FetchBadge field :badges, resolver: Resolvers::FetchBadges + field :special_badge, resolver: Resolvers::FetchSpecialBadge + field :special_badges, resolver: Resolvers::FetchSpecialBadges + field :current_user, resolver: Resolvers::CurrentUser, preauthorize: {with: AuthenticatedGraphqlPolicy} field :notifications, diff --git a/apps/govquests-api/rails_app/app/graphql/types/special_badge_type.rb b/apps/govquests-api/rails_app/app/graphql/types/special_badge_type.rb new file mode 100644 index 00000000..faab4e05 --- /dev/null +++ b/apps/govquests-api/rails_app/app/graphql/types/special_badge_type.rb @@ -0,0 +1,18 @@ +module Types + class SpecialBadgeType < Types::BaseObject + field :id, ID, null: false, method: :badge_id + field :display_data, Types::BadgeDisplayDataType, null: false + field :points, Integer, null: false + field :badge_type, String, null: false + field :user_badges, [Types::UserBadgeType], null: false + field :earned_by_current_user, Boolean, null: false + + def user_badges + object.user_badges.where(user: context[:current_user]) + end + + def earned_by_current_user + object.user_badges.exists?(user: context[:current_user]) + end + end +end diff --git a/apps/govquests-api/rails_app/app/graphql/types/user_badge_type.rb b/apps/govquests-api/rails_app/app/graphql/types/user_badge_type.rb new file mode 100644 index 00000000..04cd32d7 --- /dev/null +++ b/apps/govquests-api/rails_app/app/graphql/types/user_badge_type.rb @@ -0,0 +1,6 @@ +module Types + class UserBadgeType < Types::BaseObject + field :user_id, ID, null: false, method: :user_id + field :earned_at, GraphQL::Types::ISO8601DateTime, null: false + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/gamification/badge_read_model.rb b/apps/govquests-api/rails_app/app/read_models/gamification/badge_read_model.rb index 884a5b93..69aafedd 100644 --- a/apps/govquests-api/rails_app/app/read_models/gamification/badge_read_model.rb +++ b/apps/govquests-api/rails_app/app/read_models/gamification/badge_read_model.rb @@ -2,27 +2,14 @@ module Gamification class BadgeReadModel < ApplicationRecord self.table_name = "badges" - belongs_to :badgeable, polymorphic: true, optional: true + belongs_to :badgeable, polymorphic: true validates :badge_id, presence: true, uniqueness: true validates :display_data, presence: true - validate :validate_badgeable_consistency - def special? - badgeable_id.nil? && badgeable_type.nil? - end - - scope :special, -> { where(badgeable_type: nil, badgeable_id: nil) } - scope :normal, -> { where.not(badgeable_type: nil, badgeable_id: nil) } - - private - - def validate_badgeable_consistency - if (badgeable_type.present? && badgeable_id.nil?) || - (badgeable_type.nil? && badgeable_id.present?) - errors.add(:base, "Both badgeable type and ID must be present, or both must be nil") - end - end + has_many :user_badges, + class_name: "Gamification::UserBadgeReadModel", + as: :badgeable end end @@ -31,15 +18,15 @@ def validate_badgeable_consistency # Table name: badges # # id :bigint not null, primary key -# badgeable_type :string +# badgeable_type :string not null # display_data :jsonb not null # created_at :datetime not null # updated_at :datetime not null # badge_id :string not null -# badgeable_id :string +# badgeable_id :string not null # # Indexes # # index_badges_on_badge_id (badge_id) UNIQUE -# index_badges_on_badgeable_type_and_badgeable_id (badgeable_type,badgeable_id) UNIQUE WHERE ((badgeable_type IS NOT NULL) AND (badgeable_id IS NOT NULL)) +# index_badges_on_badgeable_type_and_badgeable_id (badgeable_type,badgeable_id) UNIQUE # diff --git a/apps/govquests-api/rails_app/app/read_models/gamification/on_badge_earned.rb b/apps/govquests-api/rails_app/app/read_models/gamification/on_badge_earned.rb index 64d6f442..cb65f98d 100644 --- a/apps/govquests-api/rails_app/app/read_models/gamification/on_badge_earned.rb +++ b/apps/govquests-api/rails_app/app/read_models/gamification/on_badge_earned.rb @@ -1,21 +1,24 @@ module Gamification class OnBadgeEarned def call(event) - profile_id = event.data[:profile_id] - badge = event.data[:badge] + user_id = event.data[:user_id] + badgeable_id = event.data[:badgeable_id] + badgeable_type = event.data[:badgeable_type] + earned_at = event.data[:earned_at] - game_profile = GameProfileReadModel.find_by(profile_id: profile_id) - if game_profile - badges = game_profile.badges || [] - if badges.include?(badge) - Rails.logger.info "Badge '#{badge}' already exists for GameProfile #{profile_id}" - else - game_profile.update(badges: badges + [badge]) - Rails.logger.info "Added badge '#{badge}' to GameProfile #{profile_id}" - end - else - Rails.logger.warn "GameProfile #{profile_id} not found for BadgeEarned event" - end + return if badgeable_type == "Gamification::BadgeReadModel" && + UserBadgeReadModel.exists?( + user_id:, + badgeable_id: badgeable_id, + badgeable_type: badgeable_type + ) + + UserBadgeReadModel.create!( + user_id:, + badgeable_id: badgeable_id, + badgeable_type: badgeable_type, + earned_at: earned_at + ) end end end diff --git a/apps/govquests-api/rails_app/app/read_models/gamification/special_badge_read_model.rb b/apps/govquests-api/rails_app/app/read_models/gamification/special_badge_read_model.rb new file mode 100644 index 00000000..01b1866b --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/gamification/special_badge_read_model.rb @@ -0,0 +1,33 @@ +module Gamification + class SpecialBadgeReadModel < ApplicationRecord + self.table_name = "special_badges" + + validates :badge_id, presence: true, uniqueness: true + validates :display_data, presence: true + validates :badge_type, presence: true + validates :badge_data, presence: true + validates :points, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 0} + + has_many :user_badges, + class_name: "Gamification::UserBadgeReadModel", + as: :badgeable + end +end + +# == Schema Information +# +# Table name: special_badges +# +# id :bigint not null, primary key +# badge_data :jsonb not null +# badge_type :string not null +# display_data :jsonb not null +# points :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# badge_id :string not null +# +# Indexes +# +# index_special_badges_on_badge_id (badge_id) UNIQUE +# diff --git a/apps/govquests-api/rails_app/app/read_models/gamification/user_badge_read_model.rb b/apps/govquests-api/rails_app/app/read_models/gamification/user_badge_read_model.rb new file mode 100644 index 00000000..e57b98d4 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/gamification/user_badge_read_model.rb @@ -0,0 +1,43 @@ +module Gamification + class UserBadgeReadModel < ApplicationRecord + self.table_name = "user_badges" + + belongs_to :user, class_name: "Authentication::UserReadModel", foreign_key: "user_id", primary_key: "user_id" + belongs_to :badgeable, polymorphic: true + + validates :user_id, presence: true + validates :earned_at, presence: true + + scope :special, -> { where(badgeable_type: "Gamification::SpecialBadgeReadModel") } + scope :normal, -> { where(badgeable_type: "Gamification::BadgeReadModel") } + scope :earned_between, ->(start_date, end_date) { where(earned_at: start_date..end_date) } + scope :ordered_by_earned, -> { order(earned_at: :desc) } + + def special? + badgeable_type == "Gamification::SpecialBadgeReadModel" + end + + def normal? + badgeable_type == "Gamification::BadgeReadModel" + end + end +end + +# == Schema Information +# +# Table name: user_badges +# +# id :bigint not null, primary key +# badgeable_type :string not null +# earned_at :datetime not null +# created_at :datetime not null +# updated_at :datetime not null +# badgeable_id :string not null +# user_id :string not null +# +# Indexes +# +# index_user_badges_on_badgeable_type_and_badgeable_id (badgeable_type,badgeable_id) +# index_user_badges_on_earned_at (earned_at) +# unique_normal_badges_index (user_id,badgeable_type,badgeable_id) UNIQUE WHERE ((badgeable_type)::text = 'Gamification::BadgeReadModel'::text) +# diff --git a/apps/govquests-api/rails_app/app/read_models/questing/on_track_completed.rb b/apps/govquests-api/rails_app/app/read_models/questing/on_track_completed.rb new file mode 100644 index 00000000..97becdd6 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/questing/on_track_completed.rb @@ -0,0 +1,13 @@ +module Questing + class OnTrackCompleted + def call(event) + UserTrackReadModel.create_or_find_by!( + user_id: event.data[:user_id], + track_id: event.data[:track_id] + ).update!( + status: "completed", + completed_at: Time.current + ) + end + end +end diff --git a/apps/govquests-api/rails_app/app/read_models/questing/on_track_created.rb b/apps/govquests-api/rails_app/app/read_models/questing/on_track_created.rb index 0d02e479..b5f101df 100644 --- a/apps/govquests-api/rails_app/app/read_models/questing/on_track_created.rb +++ b/apps/govquests-api/rails_app/app/read_models/questing/on_track_created.rb @@ -1,11 +1,16 @@ module Questing class OnTrackCreated def call(event) - TrackReadModel.create!( + track = TrackReadModel.create!( track_id: event.data[:track_id], - display_data: event.data[:display_data], - quest_ids: event.data[:quest_ids] + display_data: event.data[:display_data] ) + + event.data[:quest_ids].each_with_index do |quest_id, index| + QuestReadModel.find_by(quest_id: quest_id)&.update!( + track_id: track.track_id + ) + end end end end diff --git a/apps/govquests-api/rails_app/app/read_models/questing/quest_read_model.rb b/apps/govquests-api/rails_app/app/read_models/questing/quest_read_model.rb index 5f703fb3..d66f7cbf 100644 --- a/apps/govquests-api/rails_app/app/read_models/questing/quest_read_model.rb +++ b/apps/govquests-api/rails_app/app/read_models/questing/quest_read_model.rb @@ -7,6 +7,12 @@ class QuestReadModel < ApplicationRecord has_many :user_quests, class_name: "Questing::UserQuestReadModel", foreign_key: "quest_id", primary_key: "quest_id" has_many :reward_pools, class_name: "Rewarding::RewardPoolReadModel", foreign_key: "quest_id", primary_key: "quest_id" + belongs_to :track, + class_name: "Questing::TrackReadModel", + foreign_key: "track_id", + primary_key: "track_id", + optional: true + validates :quest_id, presence: true, uniqueness: true validates :slug, presence: true validates :audience, presence: true @@ -31,8 +37,10 @@ class QuestReadModel < ApplicationRecord # created_at :datetime not null # updated_at :datetime not null # quest_id :string not null +# track_id :string # # Indexes # # index_quests_on_quest_id (quest_id) UNIQUE +# index_quests_on_track_id (track_id) # diff --git a/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb b/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb index 5adc752f..2aa051e4 100644 --- a/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb +++ b/apps/govquests-api/rails_app/app/read_models/questing/read_model_configuration.rb @@ -6,6 +6,7 @@ def call(event_store) event_store.subscribe(OnQuestStarted, to: [Questing::QuestStarted]) event_store.subscribe(OnQuestCompleted, to: [Questing::QuestCompleted]) event_store.subscribe(OnTrackCreated, to: [Questing::TrackCreated]) + event_store.subscribe(OnTrackCompleted, to: [Questing::TrackCompleted]) end end end diff --git a/apps/govquests-api/rails_app/app/read_models/questing/track_read_model.rb b/apps/govquests-api/rails_app/app/read_models/questing/track_read_model.rb index 61a20dae..65ff5847 100644 --- a/apps/govquests-api/rails_app/app/read_models/questing/track_read_model.rb +++ b/apps/govquests-api/rails_app/app/read_models/questing/track_read_model.rb @@ -2,15 +2,19 @@ module Questing class TrackReadModel < ApplicationRecord self.table_name = "tracks" - attribute :quest_ids, :jsonb, array: true + has_many :quests, + class_name: "Questing::QuestReadModel", + foreign_key: "track_id", + primary_key: "track_id" + + has_many :user_tracks, + class_name: "Questing::UserTrackReadModel", + foreign_key: "track_id", + primary_key: "track_id" validates :track_id, presence: true, uniqueness: true validates :display_data, presence: true - def quests - QuestReadModel.where(quest_id: quest_ids) - end - has_one :badge, class_name: "Gamification::BadgeReadModel", as: :badgeable diff --git a/apps/govquests-api/rails_app/app/read_models/questing/user_track_read_model.rb b/apps/govquests-api/rails_app/app/read_models/questing/user_track_read_model.rb new file mode 100644 index 00000000..a3eb3596 --- /dev/null +++ b/apps/govquests-api/rails_app/app/read_models/questing/user_track_read_model.rb @@ -0,0 +1,43 @@ +module Questing + class UserTrackReadModel < ApplicationRecord + self.table_name = "user_tracks" + + belongs_to :user, + class_name: "Authentication::UserReadModel", + foreign_key: "user_id", + primary_key: "user_id" + + belongs_to :track, + class_name: "Questing::TrackReadModel", + foreign_key: "track_id", + primary_key: "track_id" + + validates :user_id, presence: true + validates :track_id, presence: true + validates :completed_at, presence: true, if: :completed? + validates :user_id, uniqueness: {scope: :track_id} + + def completed? + status == "completed" + end + end +end + +# == Schema Information +# +# Table name: user_tracks +# +# id :bigint not null, primary key +# completed_at :datetime +# status :string default("in_progress"), not null +# created_at :datetime not null +# updated_at :datetime not null +# track_id :string not null +# user_id :string not null +# +# Indexes +# +# index_user_tracks_on_status (status) +# index_user_tracks_on_track_id (track_id) +# index_user_tracks_on_user_id_and_track_id (user_id,track_id) UNIQUE +# diff --git a/apps/govquests-api/rails_app/db/migrate/20250115203038_make_badgeable_optional_in_badges.rb b/apps/govquests-api/rails_app/db/migrate/20250115203038_make_badgeable_optional_in_badges.rb deleted file mode 100644 index ef961ce8..00000000 --- a/apps/govquests-api/rails_app/db/migrate/20250115203038_make_badgeable_optional_in_badges.rb +++ /dev/null @@ -1,11 +0,0 @@ -class MakeBadgeableOptionalInBadges < ActiveRecord::Migration[8.1] - def change - change_column_null :badges, :badgeable_type, true - change_column_null :badges, :badgeable_id, true - - remove_index :badges, [:badgeable_type, :badgeable_id] - add_index :badges, [:badgeable_type, :badgeable_id], - unique: true, - where: "badgeable_type IS NOT NULL AND badgeable_id IS NOT NULL" - end -end diff --git a/apps/govquests-api/rails_app/db/migrate/20250116122834_create_special_badges.rb b/apps/govquests-api/rails_app/db/migrate/20250116122834_create_special_badges.rb new file mode 100644 index 00000000..7ef5f16a --- /dev/null +++ b/apps/govquests-api/rails_app/db/migrate/20250116122834_create_special_badges.rb @@ -0,0 +1,14 @@ +class CreateSpecialBadges < ActiveRecord::Migration[8.1] + def change + create_table :special_badges do |t| + t.string :badge_id, null: false + t.jsonb :display_data, null: false + t.string :badge_type, null: false + t.jsonb :badge_data, null: false + t.integer :points, null: false + t.timestamps + end + + add_index :special_badges, :badge_id, unique: true + end +end diff --git a/apps/govquests-api/rails_app/db/migrate/20250116135942_create_user_badges.rb b/apps/govquests-api/rails_app/db/migrate/20250116135942_create_user_badges.rb new file mode 100644 index 00000000..d0dadd81 --- /dev/null +++ b/apps/govquests-api/rails_app/db/migrate/20250116135942_create_user_badges.rb @@ -0,0 +1,20 @@ +class CreateUserBadges < ActiveRecord::Migration[8.1] + def change + create_table :user_badges do |t| + t.string :user_id, null: false + t.string :badgeable_type, null: false + t.string :badgeable_id, null: false + t.datetime :earned_at, null: false + t.timestamps + end + + add_index :user_badges, [:badgeable_type, :badgeable_id] + add_index :user_badges, :earned_at + + # Optional: Add a partial unique index for normal badges + add_index :user_badges, [:user_id, :badgeable_type, :badgeable_id], + unique: true, + where: "badgeable_type = 'Gamification::BadgeReadModel'", + name: 'unique_normal_badges_index' + end +end diff --git a/apps/govquests-api/rails_app/db/migrate/20250116195445_create_user_tracks.rb b/apps/govquests-api/rails_app/db/migrate/20250116195445_create_user_tracks.rb new file mode 100644 index 00000000..544e727d --- /dev/null +++ b/apps/govquests-api/rails_app/db/migrate/20250116195445_create_user_tracks.rb @@ -0,0 +1,15 @@ +class CreateUserTracks < ActiveRecord::Migration[8.1] + def change + create_table :user_tracks do |t| + t.string :track_id, null: false + t.string :user_id, null: false + t.string :status, null: false, default: 'in_progress' + t.datetime :completed_at + t.timestamps + end + + add_index :user_tracks, [:user_id, :track_id], unique: true + add_index :user_tracks, :track_id + add_index :user_tracks, :status + end +end diff --git a/apps/govquests-api/rails_app/db/migrate/20250116200408_add_track_id_to_quests.rb b/apps/govquests-api/rails_app/db/migrate/20250116200408_add_track_id_to_quests.rb new file mode 100644 index 00000000..dbcec008 --- /dev/null +++ b/apps/govquests-api/rails_app/db/migrate/20250116200408_add_track_id_to_quests.rb @@ -0,0 +1,7 @@ +class AddTrackIdToQuests < ActiveRecord::Migration[8.1] + def change + add_column :quests, :track_id, :string + + add_index :quests, :track_id + end +end diff --git a/apps/govquests-api/rails_app/db/schema.rb b/apps/govquests-api/rails_app/db/schema.rb index 7f7bb98f..00771fec 100644 --- a/apps/govquests-api/rails_app/db/schema.rb +++ b/apps/govquests-api/rails_app/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2025_01_15_203038) do +ActiveRecord::Schema[8.1].define(version: 2025_01_16_200408) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -47,10 +47,10 @@ t.jsonb "display_data", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.string "badgeable_type" - t.string "badgeable_id" + t.string "badgeable_type", null: false + t.string "badgeable_id", null: false t.index ["badge_id"], name: "index_badges_on_badge_id", unique: true - t.index ["badgeable_type", "badgeable_id"], name: "index_badges_on_badgeable_type_and_badgeable_id", unique: true, where: "((badgeable_type IS NOT NULL) AND (badgeable_id IS NOT NULL))" + t.index ["badgeable_type", "badgeable_id"], name: "index_badges_on_badgeable_type_and_badgeable_id", unique: true end create_table "event_store_events", force: :cascade do |t| @@ -137,7 +137,9 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "slug" + t.string "track_id" t.index ["quest_id"], name: "index_quests_on_quest_id", unique: true + t.index ["track_id"], name: "index_quests_on_track_id" end create_table "reward_issuances", force: :cascade do |t| @@ -178,6 +180,17 @@ t.index ["reward_id"], name: "index_rewards_on_reward_id", unique: true end + create_table "special_badges", force: :cascade do |t| + t.string "badge_id", null: false + t.jsonb "display_data", null: false + t.string "badge_type", null: false + t.jsonb "badge_data", null: false + t.integer "points", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["badge_id"], name: "index_special_badges_on_badge_id", unique: true + end + create_table "tracks", force: :cascade do |t| t.string "track_id", null: false t.jsonb "display_data", default: {}, null: false @@ -187,6 +200,18 @@ t.index ["track_id"], name: "index_tracks_on_track_id", unique: true end + create_table "user_badges", force: :cascade do |t| + t.string "user_id", null: false + t.string "badgeable_type", null: false + t.string "badgeable_id", null: false + t.datetime "earned_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["badgeable_type", "badgeable_id"], name: "index_user_badges_on_badgeable_type_and_badgeable_id" + t.index ["earned_at"], name: "index_user_badges_on_earned_at" + t.index ["user_id", "badgeable_type", "badgeable_id"], name: "unique_normal_badges_index", unique: true, where: "((badgeable_type)::text = 'Gamification::BadgeReadModel'::text)" + end + create_table "user_game_profiles", force: :cascade do |t| t.string "profile_id", null: false t.integer "tier", default: 0 @@ -234,6 +259,18 @@ t.index ["user_id"], name: "index_user_sessions_on_user_id" end + create_table "user_tracks", force: :cascade do |t| + t.string "track_id", null: false + t.string "user_id", null: false + t.string "status", default: "in_progress", null: false + t.datetime "completed_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["status"], name: "index_user_tracks_on_status" + t.index ["track_id"], name: "index_user_tracks_on_track_id" + t.index ["user_id", "track_id"], name: "index_user_tracks_on_user_id_and_track_id", unique: true + end + create_table "users", force: :cascade do |t| t.string "user_id", null: false t.string "email" diff --git a/apps/govquests-api/rails_app/schema.graphql b/apps/govquests-api/rails_app/schema.graphql index 04a34351..18b7c7fd 100644 --- a/apps/govquests-api/rails_app/schema.graphql +++ b/apps/govquests-api/rails_app/schema.graphql @@ -57,13 +57,15 @@ type BackgroundGradient { type Badge { badgeable: BadgeableUnion displayData: BadgeDisplayData! + earnedByCurrentUser: Boolean! id: ID! - special: Boolean + userBadges: [UserBadge!]! } type BadgeDisplayData { description: String imageUrl: String + sequenceNumber: Int title: String } @@ -524,7 +526,7 @@ type PageInfo { type Query { badge(id: ID!): Badge - badges(special: Boolean): [Badge!] + badges: [Badge!] currentUser: User notifications( """ @@ -551,6 +553,8 @@ type Query { ): NotificationConnection! quest(slug: String!): Quest quests: [Quest!]! + specialBadge(id: ID!): SpecialBadge + specialBadges: [SpecialBadge!] track(id: ID!): Track tracks: [Track!] unreadNotificationsCount: Int! @@ -651,6 +655,15 @@ type SignOutPayload { success: Boolean! } +type SpecialBadge { + badgeType: String! + displayData: BadgeDisplayData! + earnedByCurrentUser: Boolean! + id: ID! + points: Int! + userBadges: [UserBadge!]! +} + """ Autogenerated input type of StartActionExecution """ @@ -716,6 +729,11 @@ type User { userType: String! } +type UserBadge { + earnedAt: ISO8601DateTime! + userId: ID! +} + type UserQuest { completedAt: ISO8601DateTime id: ID! diff --git a/apps/govquests-frontend/src/domains/action_tracking/hooks/useCompleteActionExecution.ts b/apps/govquests-frontend/src/domains/action_tracking/hooks/useCompleteActionExecution.ts index d0161e87..de4dfd75 100644 --- a/apps/govquests-frontend/src/domains/action_tracking/hooks/useCompleteActionExecution.ts +++ b/apps/govquests-frontend/src/domains/action_tracking/hooks/useCompleteActionExecution.ts @@ -15,7 +15,9 @@ export const useCompleteActionExecution = (invalidateKey: string[]) => { mutationFn: completeActionExecution, onSuccess: () => { queryClient.invalidateQueries({ queryKey: invalidateKey }); - queryClient.invalidateQueries({ queryKey: ["notifications"] }); + queryClient.invalidateQueries({ queryKey: ["notifications"] }); + queryClient.invalidateQueries({ queryKey: ["badge"] }); + }, }); }; diff --git a/apps/govquests-frontend/src/domains/action_tracking/hooks/useStartActionExecution.ts b/apps/govquests-frontend/src/domains/action_tracking/hooks/useStartActionExecution.ts index e013af64..a73f7fe5 100644 --- a/apps/govquests-frontend/src/domains/action_tracking/hooks/useStartActionExecution.ts +++ b/apps/govquests-frontend/src/domains/action_tracking/hooks/useStartActionExecution.ts @@ -15,6 +15,7 @@ export const useStartActionExecution = () => { mutationFn: startActionExecution, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["notifications"] }); + queryClient.invalidateQueries({ queryKey: ["badge"] }); }, }); }; diff --git a/apps/govquests-frontend/src/domains/gamification/components/BadgeCard.tsx b/apps/govquests-frontend/src/domains/gamification/components/BadgeCard.tsx index c3ec71b6..ce4fca3a 100644 --- a/apps/govquests-frontend/src/domains/gamification/components/BadgeCard.tsx +++ b/apps/govquests-frontend/src/domains/gamification/components/BadgeCard.tsx @@ -7,20 +7,21 @@ import { useFetchBadge } from "../hooks/useFetchBadge"; interface BadgeCardProps { badgeId: string; - isCompleted: boolean; className?: string; withTitle?: boolean; header?: string; + revealIncomplete?: boolean; } export const BadgeCard = ({ badgeId, - isCompleted, className, withTitle = false, header, + revealIncomplete = false, }: BadgeCardProps) => { const { data } = useFetchBadge(badgeId); + const revealCard = data?.badge.earnedByCurrentUser || revealIncomplete; const Card = data && (
{data.badge.displayData.title} diff --git a/apps/govquests-frontend/src/domains/gamification/components/BadgeDetails.tsx b/apps/govquests-frontend/src/domains/gamification/components/BadgeDetails.tsx index 198ff343..070fee07 100644 --- a/apps/govquests-frontend/src/domains/gamification/components/BadgeDetails.tsx +++ b/apps/govquests-frontend/src/domains/gamification/components/BadgeDetails.tsx @@ -3,30 +3,33 @@ import { DialogDescription, DialogHeader, } from "@/components/ui/dialog"; -import { useFetchBadge } from "../hooks/useFetchBadge"; import { BadgeCard } from "./BadgeCard"; import { SimpleBadgeContent } from "./SimpleBadgeContent"; import { SpecialBadgeContent } from "./SpecialBadgeContent"; -export const BadgeDetails = ({ badgeId }: { badgeId: string }) => { - const { data } = useFetchBadge(badgeId); +interface BadgeDetailsProps { + badgeId: string; + special?: boolean; +} +export const BadgeDetails = ({ + badgeId, + special = false, +}: BadgeDetailsProps) => { return ( - data && ( - - - -
- -
- {data.badge.special ? ( - - ) : ( - - )} -
-
-
- ) + + + +
+ +
+ {special ? ( + + ) : ( + + )} +
+
+
); }; diff --git a/apps/govquests-frontend/src/domains/gamification/components/GalleryBadgesSection.tsx b/apps/govquests-frontend/src/domains/gamification/components/GalleryBadgesSection.tsx index 2be3d0c0..2a4d08ba 100644 --- a/apps/govquests-frontend/src/domains/gamification/components/GalleryBadgesSection.tsx +++ b/apps/govquests-frontend/src/domains/gamification/components/GalleryBadgesSection.tsx @@ -10,7 +10,7 @@ export const SimpleBadgesSection: React.FC = () => { const params = useSearchParams(); const queryBadgeId = params.get("badgeId"); - const { data } = useFetchBadges(false); + const { data } = useFetchBadges(); return ( data && ( @@ -22,13 +22,12 @@ export const SimpleBadgesSection: React.FC = () => {
{data.badges.map((badge, index) => ( - + diff --git a/apps/govquests-frontend/src/domains/gamification/components/SimpleBadgeContent.tsx b/apps/govquests-frontend/src/domains/gamification/components/SimpleBadgeContent.tsx index 454901e4..a7d3fb82 100644 --- a/apps/govquests-frontend/src/domains/gamification/components/SimpleBadgeContent.tsx +++ b/apps/govquests-frontend/src/domains/gamification/components/SimpleBadgeContent.tsx @@ -1,25 +1,27 @@ import { Button } from "@/components/ui/Button"; import Link from "next/link"; import { useCallback } from "react"; -import { Badge } from "../types/badgeTypes"; +import { useFetchBadge } from "../hooks/useFetchBadge"; -export const SimpleBadgeContent = ({ badge }: { badge: Badge }) => { - // TODO - get user badge - OP-677 - const isCompleted = true; +export const SimpleBadgeContent = ({ badgeId }: { badgeId: string }) => { + const { data } = useFetchBadge(badgeId); + + const isCompleted = data.badge.earnedByCurrentUser; const linkToBadgeable = useCallback(() => { - switch (badge.badgeable.__typename) { + if (!data?.badge) return ""; + switch (data.badge.badgeable.__typename) { case "Quest": - return `/quests/${badge.badgeable.slug}`; + return `/quests/${data.badge.badgeable.slug}`; case "Track": - return `/quests?trackId=${badge.badgeable.id}`; + return `/quests?trackId=${data.badge.badgeable.id}`; default: return ""; } - }, [badge]); + }, [data]); - const badgeableTitle = badge.badgeable.displayData.title; - const badgeableType = badge.badgeable.__typename; + const badgeableTitle = data.badge.badgeable.displayData.title; + const badgeableType = data.badge.badgeable.__typename; return (
@@ -30,11 +32,11 @@ export const SimpleBadgeContent = ({ badge }: { badge: Badge }) => { {isCompleted ? `You collected this badge by completing the ${badgeableTitle} ${badgeableType}.` - : `Complete the ${badgeableTitle} ${badgeableType} to unlock the ${badge.displayData.title} badge and add it to your collection.`} + : `Complete the ${badgeableTitle} ${badgeableType} to unlock the ${data.badge.displayData.title} badge and add it to your collection.`}
diff --git a/apps/govquests-frontend/src/domains/gamification/components/SpecialBadgeContent.tsx b/apps/govquests-frontend/src/domains/gamification/components/SpecialBadgeContent.tsx index ce0b8c4f..da9c72d9 100644 --- a/apps/govquests-frontend/src/domains/gamification/components/SpecialBadgeContent.tsx +++ b/apps/govquests-frontend/src/domains/gamification/components/SpecialBadgeContent.tsx @@ -1,39 +1,43 @@ import { Button } from "@/components/ui/Button"; -import { Badge } from "../types/badgeTypes"; import { useState } from "react"; +import { useFetchSpecialBadge } from "../hooks/useFetchSpecialBadge"; -export const SpecialBadgeContent = ({ badge }: { badge: Badge }) => { - // TODO - get user badge - OP-677 - const isCompleted = true; +export const SpecialBadgeContent = ({ badgeId }: { badgeId: string }) => { + const { data } = useFetchSpecialBadge(badgeId); + + const isCompleted = data.specialBadge.earnedByCurrentUser; const [error, setError] = useState(null); return ( -
-

- {isCompleted - ? "Special Badge Unlocked!" - : "This Special Badge is waiting for you!"} -

- - How to win -
- {badge.displayData.description} -
- - If you’ve already reached this milestone, click to collect this badge - now. - -
- - {error && ( -

- Sorry, you haven't met the requirements.Try again another time. -

- )} + data && ( +
+

+ {isCompleted + ? "Special Badge Unlocked!" + : "This Special Badge is waiting for you!"} +

+ + How to win +
+ {data.specialBadge.displayData.description} +
+ + If you’ve already reached this milestone, click to collect this badge + now. + +
+ + {error && ( +

+ Sorry, you haven't met the requirements.Try again another + time. +

+ )} +
-
+ ) ); }; diff --git a/apps/govquests-frontend/src/domains/gamification/components/SpecialBadgesSection.tsx b/apps/govquests-frontend/src/domains/gamification/components/SpecialBadgesSection.tsx index 3873e74a..b6e6193e 100644 --- a/apps/govquests-frontend/src/domains/gamification/components/SpecialBadgesSection.tsx +++ b/apps/govquests-frontend/src/domains/gamification/components/SpecialBadgesSection.tsx @@ -11,7 +11,7 @@ import { UseEmblaCarouselType } from "embla-carousel-react"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { useSearchParams } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; -import { useFetchBadges } from "../hooks/useFetchBadges"; +import { useFetchSpecialBadges } from "../hooks/useFetchSpecialBadges"; import { BadgeCard } from "./BadgeCard"; import { BadgeDetails } from "./BadgeDetails"; @@ -20,7 +20,7 @@ export const SpecialBadgesSection: React.FC = () => { const params = useSearchParams(); const queryBadgeId = params.get("badgeId"); - const { data } = useFetchBadges(true); + const { data } = useFetchSpecialBadges(); const hasNavigationButtons = useMemo( () => api && (api.canScrollPrev() || api.canScrollNext()), @@ -61,7 +61,7 @@ export const SpecialBadgesSection: React.FC = () => { - {data.badges.map((badge, index) => ( + {data.specialBadges.map((badge, index) => ( { - +
))} diff --git a/apps/govquests-frontend/src/domains/gamification/graphql/badgeQuery.ts b/apps/govquests-frontend/src/domains/gamification/graphql/badgeQuery.ts index 271eff51..2c12dc41 100644 --- a/apps/govquests-frontend/src/domains/gamification/graphql/badgeQuery.ts +++ b/apps/govquests-frontend/src/domains/gamification/graphql/badgeQuery.ts @@ -4,7 +4,7 @@ export const BadgeQuery = graphql(` query GetBadge($id: ID!) { badge(id: $id) { id - special + earnedByCurrentUser displayData { title description diff --git a/apps/govquests-frontend/src/domains/gamification/graphql/badgesQuery.ts b/apps/govquests-frontend/src/domains/gamification/graphql/badgesQuery.ts index 92d914b8..d8ecde6f 100644 --- a/apps/govquests-frontend/src/domains/gamification/graphql/badgesQuery.ts +++ b/apps/govquests-frontend/src/domains/gamification/graphql/badgesQuery.ts @@ -1,9 +1,22 @@ import { graphql } from "gql.tada"; export const BadgesQuery = graphql(` - query GetBadges($special: Boolean) { - badges(special: $special) { + query GetBadges { + badges { id + earnedByCurrentUser + displayData { + sequenceNumber + } + badgeable { + __typename + ... on Quest { + id + } + ... on Track { + id + } + } } } `); diff --git a/apps/govquests-frontend/src/domains/gamification/graphql/specialBadgeQuery.ts b/apps/govquests-frontend/src/domains/gamification/graphql/specialBadgeQuery.ts new file mode 100644 index 00000000..3fab3c83 --- /dev/null +++ b/apps/govquests-frontend/src/domains/gamification/graphql/specialBadgeQuery.ts @@ -0,0 +1,17 @@ +import { graphql } from "gql.tada"; + +export const SpecialBadgeQuery = graphql(` + query GetSpecialBadge($id: ID!) { + specialBadge(id: $id) { + id + points + badgeType + earnedByCurrentUser + displayData { + title + description + imageUrl + } + } + } +`); diff --git a/apps/govquests-frontend/src/domains/gamification/graphql/specialBadgesQuery.ts b/apps/govquests-frontend/src/domains/gamification/graphql/specialBadgesQuery.ts new file mode 100644 index 00000000..d3248962 --- /dev/null +++ b/apps/govquests-frontend/src/domains/gamification/graphql/specialBadgesQuery.ts @@ -0,0 +1,10 @@ +import { graphql } from "gql.tada"; + +export const SpecialBadgesQuery = graphql(` + query GetSpecialBadges { + specialBadges { + id + earnedByCurrentUser + } + } +`); diff --git a/apps/govquests-frontend/src/domains/gamification/hooks/useFetchBadges.ts b/apps/govquests-frontend/src/domains/gamification/hooks/useFetchBadges.ts index e18ee66d..f2722b41 100644 --- a/apps/govquests-frontend/src/domains/gamification/hooks/useFetchBadges.ts +++ b/apps/govquests-frontend/src/domains/gamification/hooks/useFetchBadges.ts @@ -3,9 +3,9 @@ import { useQuery } from "@tanstack/react-query"; import { fetchAllBadges } from "../services/badgeService"; -export const useFetchBadges = (special) => { +export const useFetchBadges = () => { return useQuery({ - queryKey: ["badges", special], - queryFn: () => fetchAllBadges(special), + queryKey: ["badges"], + queryFn: () => fetchAllBadges(), }); }; diff --git a/apps/govquests-frontend/src/domains/gamification/hooks/useFetchSpecialBadge.ts b/apps/govquests-frontend/src/domains/gamification/hooks/useFetchSpecialBadge.ts new file mode 100644 index 00000000..1cd4ec97 --- /dev/null +++ b/apps/govquests-frontend/src/domains/gamification/hooks/useFetchSpecialBadge.ts @@ -0,0 +1,12 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { fetchSpecialBadge } from "../services/specialBadgeService"; + +export const useFetchSpecialBadge = (id: string) => { + return useQuery({ + queryKey: ["specialBadge", id], + queryFn: () => fetchSpecialBadge(id), + enabled: !!id, + }); +}; diff --git a/apps/govquests-frontend/src/domains/gamification/hooks/useFetchSpecialBadges.ts b/apps/govquests-frontend/src/domains/gamification/hooks/useFetchSpecialBadges.ts new file mode 100644 index 00000000..7fc0227b --- /dev/null +++ b/apps/govquests-frontend/src/domains/gamification/hooks/useFetchSpecialBadges.ts @@ -0,0 +1,11 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { fetchAllSpecialBadges } from "../services/specialBadgeService"; + +export const useFetchSpecialBadges = () => { + return useQuery({ + queryKey: ["specialBadges"], + queryFn: () => fetchAllSpecialBadges(), + }); +}; diff --git a/apps/govquests-frontend/src/domains/gamification/services/badgeService.ts b/apps/govquests-frontend/src/domains/gamification/services/badgeService.ts index 01647eae..c3356471 100644 --- a/apps/govquests-frontend/src/domains/gamification/services/badgeService.ts +++ b/apps/govquests-frontend/src/domains/gamification/services/badgeService.ts @@ -7,6 +7,6 @@ export const fetchBadge = async (id: string) => { return await request(API_URL, BadgeQuery, { id }); }; -export const fetchAllBadges = async (special: boolean) => { - return await request(API_URL, BadgesQuery, { special }); +export const fetchAllBadges = async () => { + return await request(API_URL, BadgesQuery); }; diff --git a/apps/govquests-frontend/src/domains/gamification/services/specialBadgeService.ts b/apps/govquests-frontend/src/domains/gamification/services/specialBadgeService.ts new file mode 100644 index 00000000..bc6928e4 --- /dev/null +++ b/apps/govquests-frontend/src/domains/gamification/services/specialBadgeService.ts @@ -0,0 +1,12 @@ +import { API_URL } from "@/lib/utils"; +import request from "graphql-request"; +import { SpecialBadgeQuery } from "../graphql/specialBadgeQuery"; +import { SpecialBadgesQuery } from "../graphql/specialBadgesQuery"; + +export const fetchSpecialBadge = async (id: string) => { + return await request(API_URL, SpecialBadgeQuery, { id }); +}; + +export const fetchAllSpecialBadges = async () => { + return await request(API_URL, SpecialBadgesQuery); +}; diff --git a/apps/govquests-frontend/src/domains/questing/components/QuestDetails.tsx b/apps/govquests-frontend/src/domains/questing/components/QuestDetails.tsx index 1f8e14a6..d5a52a20 100644 --- a/apps/govquests-frontend/src/domains/questing/components/QuestDetails.tsx +++ b/apps/govquests-frontend/src/domains/questing/components/QuestDetails.tsx @@ -61,7 +61,6 @@ const QuestDetails = ({ quest }: QuestDetailsProps) => { diff --git a/apps/govquests-frontend/src/domains/questing/components/track/TrackDescription.tsx b/apps/govquests-frontend/src/domains/questing/components/track/TrackDescription.tsx index 03820be2..76405389 100644 --- a/apps/govquests-frontend/src/domains/questing/components/track/TrackDescription.tsx +++ b/apps/govquests-frontend/src/domains/questing/components/track/TrackDescription.tsx @@ -21,7 +21,6 @@ export const TrackDescription = ({ track }: TrackDescriptionProps) => { diff --git a/apps/govquests-frontend/src/graphql-env.d.ts b/apps/govquests-frontend/src/graphql-env.d.ts index 66e048f3..7a0c5c1c 100644 --- a/apps/govquests-frontend/src/graphql-env.d.ts +++ b/apps/govquests-frontend/src/graphql-env.d.ts @@ -7,8 +7,8 @@ export type introspection_types = { 'ActionDisplayData': { kind: 'OBJECT'; name: 'ActionDisplayData'; fields: { 'description': { name: 'description'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'title': { name: 'title'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; 'ActionExecution': { kind: 'OBJECT'; name: 'ActionExecution'; fields: { 'actionData': { name: 'actionData'; type: { kind: 'INTERFACE'; name: 'ActionDataInterface'; ofType: null; } }; 'actionId': { name: 'actionId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'actionType': { name: 'actionType'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'completedAt': { name: 'completedAt'; type: { kind: 'SCALAR'; name: 'ISO8601DateTime'; ofType: null; } }; 'completionData': { name: 'completionData'; type: { kind: 'INTERFACE'; name: 'CompletionDataInterface'; ofType: null; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'nonce': { name: 'nonce'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'startData': { name: 'startData'; type: { kind: 'INTERFACE'; name: 'StartDataInterface'; ofType: null; } }; 'startedAt': { name: 'startedAt'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ISO8601DateTime'; ofType: null; }; } }; 'status': { name: 'status'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'userId': { name: 'userId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; }; }; 'BackgroundGradient': { kind: 'OBJECT'; name: 'BackgroundGradient'; fields: { 'fromColor': { name: 'fromColor'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'toColor': { name: 'toColor'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; - 'Badge': { kind: 'OBJECT'; name: 'Badge'; fields: { 'badgeable': { name: 'badgeable'; type: { kind: 'UNION'; name: 'BadgeableUnion'; ofType: null; } }; 'displayData': { name: 'displayData'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'BadgeDisplayData'; ofType: null; }; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'special': { name: 'special'; type: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; } }; }; }; - 'BadgeDisplayData': { kind: 'OBJECT'; name: 'BadgeDisplayData'; fields: { 'description': { name: 'description'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'imageUrl': { name: 'imageUrl'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'title': { name: 'title'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; + 'Badge': { kind: 'OBJECT'; name: 'Badge'; fields: { 'badgeable': { name: 'badgeable'; type: { kind: 'UNION'; name: 'BadgeableUnion'; ofType: null; } }; 'displayData': { name: 'displayData'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'BadgeDisplayData'; ofType: null; }; } }; 'earnedByCurrentUser': { name: 'earnedByCurrentUser'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'userBadges': { name: 'userBadges'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserBadge'; ofType: null; }; }; }; } }; }; }; + 'BadgeDisplayData': { kind: 'OBJECT'; name: 'BadgeDisplayData'; fields: { 'description': { name: 'description'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'imageUrl': { name: 'imageUrl'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'sequenceNumber': { name: 'sequenceNumber'; type: { kind: 'SCALAR'; name: 'Int'; ofType: null; } }; 'title': { name: 'title'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; 'BadgeableUnion': { kind: 'UNION'; name: 'BadgeableUnion'; fields: {}; possibleTypes: 'Quest' | 'Track'; }; 'Boolean': unknown; 'CompleteActionExecutionInput': { kind: 'INPUT_OBJECT'; name: 'CompleteActionExecutionInput'; isOneOf: false; inputFields: [{ name: 'actionType'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }, { name: 'clientMutationId'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }, { name: 'discourseVerificationCompletionData'; type: { kind: 'INPUT_OBJECT'; name: 'DiscourseVerificationCompletionDataInput'; ofType: null; }; defaultValue: null }, { name: 'executionId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'gitcoinScoreCompletionData'; type: { kind: 'INPUT_OBJECT'; name: 'GitcoinScoreCompletionDataInput'; ofType: null; }; defaultValue: null }, { name: 'nonce'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }]; }; @@ -46,7 +46,7 @@ export type introspection_types = { 'NotificationDeliveryEdge': { kind: 'OBJECT'; name: 'NotificationDeliveryEdge'; fields: { 'cursor': { name: 'cursor'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'node': { name: 'node'; type: { kind: 'OBJECT'; name: 'NotificationDelivery'; ofType: null; } }; }; }; 'NotificationEdge': { kind: 'OBJECT'; name: 'NotificationEdge'; fields: { 'cursor': { name: 'cursor'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'node': { name: 'node'; type: { kind: 'OBJECT'; name: 'Notification'; ofType: null; } }; }; }; 'PageInfo': { kind: 'OBJECT'; name: 'PageInfo'; fields: { 'endCursor': { name: 'endCursor'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'hasNextPage': { name: 'hasNextPage'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'hasPreviousPage': { name: 'hasPreviousPage'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'startCursor': { name: 'startCursor'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; - 'Query': { kind: 'OBJECT'; name: 'Query'; fields: { 'badge': { name: 'badge'; type: { kind: 'OBJECT'; name: 'Badge'; ofType: null; } }; 'badges': { name: 'badges'; type: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Badge'; ofType: null; }; }; } }; 'currentUser': { name: 'currentUser'; type: { kind: 'OBJECT'; name: 'User'; ofType: null; } }; 'notifications': { name: 'notifications'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'NotificationConnection'; ofType: null; }; } }; 'quest': { name: 'quest'; type: { kind: 'OBJECT'; name: 'Quest'; ofType: null; } }; 'quests': { name: 'quests'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Quest'; ofType: null; }; }; }; } }; 'track': { name: 'track'; type: { kind: 'OBJECT'; name: 'Track'; ofType: null; } }; 'tracks': { name: 'tracks'; type: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Track'; ofType: null; }; }; } }; 'unreadNotificationsCount': { name: 'unreadNotificationsCount'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; }; }; + 'Query': { kind: 'OBJECT'; name: 'Query'; fields: { 'badge': { name: 'badge'; type: { kind: 'OBJECT'; name: 'Badge'; ofType: null; } }; 'badges': { name: 'badges'; type: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Badge'; ofType: null; }; }; } }; 'currentUser': { name: 'currentUser'; type: { kind: 'OBJECT'; name: 'User'; ofType: null; } }; 'notifications': { name: 'notifications'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'NotificationConnection'; ofType: null; }; } }; 'quest': { name: 'quest'; type: { kind: 'OBJECT'; name: 'Quest'; ofType: null; } }; 'quests': { name: 'quests'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Quest'; ofType: null; }; }; }; } }; 'specialBadge': { name: 'specialBadge'; type: { kind: 'OBJECT'; name: 'SpecialBadge'; ofType: null; } }; 'specialBadges': { name: 'specialBadges'; type: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'SpecialBadge'; ofType: null; }; }; } }; 'track': { name: 'track'; type: { kind: 'OBJECT'; name: 'Track'; ofType: null; } }; 'tracks': { name: 'tracks'; type: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Track'; ofType: null; }; }; } }; 'unreadNotificationsCount': { name: 'unreadNotificationsCount'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; }; }; 'Quest': { kind: 'OBJECT'; name: 'Quest'; fields: { 'actions': { name: 'actions'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Action'; ofType: null; }; }; }; } }; 'audience': { name: 'audience'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'badge': { name: 'badge'; type: { kind: 'OBJECT'; name: 'Badge'; ofType: null; } }; 'displayData': { name: 'displayData'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'DisplayData'; ofType: null; }; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'rewardPools': { name: 'rewardPools'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'RewardPool'; ofType: null; }; }; }; } }; 'slug': { name: 'slug'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'status': { name: 'status'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'userQuests': { name: 'userQuests'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserQuest'; ofType: null; }; }; }; } }; }; }; 'ReadDocumentActionData': { kind: 'OBJECT'; name: 'ReadDocumentActionData'; fields: { 'actionType': { name: 'actionType'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'documentUrl': { name: 'documentUrl'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; }; }; 'RewardDefinition': { kind: 'OBJECT'; name: 'RewardDefinition'; fields: { 'amount': { name: 'amount'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; 'type': { name: 'type'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; }; }; @@ -57,6 +57,7 @@ export type introspection_types = { 'SignInWithEthereumPayload': { kind: 'OBJECT'; name: 'SignInWithEthereumPayload'; fields: { 'clientMutationId': { name: 'clientMutationId'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'errors': { name: 'errors'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; }; } }; 'user': { name: 'user'; type: { kind: 'OBJECT'; name: 'User'; ofType: null; } }; }; }; 'SignOutInput': { kind: 'INPUT_OBJECT'; name: 'SignOutInput'; isOneOf: false; inputFields: [{ name: 'clientMutationId'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }]; }; 'SignOutPayload': { kind: 'OBJECT'; name: 'SignOutPayload'; fields: { 'clientMutationId': { name: 'clientMutationId'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'success': { name: 'success'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; }; }; + 'SpecialBadge': { kind: 'OBJECT'; name: 'SpecialBadge'; fields: { 'badgeType': { name: 'badgeType'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'displayData': { name: 'displayData'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'BadgeDisplayData'; ofType: null; }; } }; 'earnedByCurrentUser': { name: 'earnedByCurrentUser'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'points': { name: 'points'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; 'userBadges': { name: 'userBadges'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'UserBadge'; ofType: null; }; }; }; } }; }; }; 'StartActionExecutionInput': { kind: 'INPUT_OBJECT'; name: 'StartActionExecutionInput'; isOneOf: false; inputFields: [{ name: 'actionId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'actionType'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; defaultValue: null }, { name: 'clientMutationId'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }, { name: 'questId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; }; defaultValue: null }, { name: 'sendEmailVerificationInput'; type: { kind: 'INPUT_OBJECT'; name: 'SendEmailVerificationInput'; ofType: null; }; defaultValue: null }]; }; 'StartActionExecutionPayload': { kind: 'OBJECT'; name: 'StartActionExecutionPayload'; fields: { 'actionExecution': { name: 'actionExecution'; type: { kind: 'OBJECT'; name: 'ActionExecution'; ofType: null; } }; 'clientMutationId': { name: 'clientMutationId'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'errors': { name: 'errors'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; }; } }; }; }; 'StartDataInterface': { kind: 'INTERFACE'; name: 'StartDataInterface'; fields: { 'actionType': { name: 'actionType'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; }; possibleTypes: 'DiscourseVerificationStartData' | 'EmptyActionStartData' | 'EnsStartData' | 'GitcoinScoreStartData' | 'SendEmailStartData'; }; @@ -64,6 +65,7 @@ export type introspection_types = { 'Track': { kind: 'OBJECT'; name: 'Track'; fields: { 'badge': { name: 'badge'; type: { kind: 'OBJECT'; name: 'Badge'; ofType: null; } }; 'displayData': { name: 'displayData'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'TrackDisplayData'; ofType: null; }; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'isCompleted': { name: 'isCompleted'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; 'points': { name: 'points'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; 'quests': { name: 'quests'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Quest'; ofType: null; }; }; }; } }; }; }; 'TrackDisplayData': { kind: 'OBJECT'; name: 'TrackDisplayData'; fields: { 'backgroundGradient': { name: 'backgroundGradient'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'BackgroundGradient'; ofType: null; }; } }; 'description': { name: 'description'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'title': { name: 'title'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; }; }; 'User': { kind: 'OBJECT'; name: 'User'; fields: { 'address': { name: 'address'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'chainId': { name: 'chainId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Int'; ofType: null; }; } }; 'email': { name: 'email'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'userType': { name: 'userType'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; }; }; + 'UserBadge': { kind: 'OBJECT'; name: 'UserBadge'; fields: { 'earnedAt': { name: 'earnedAt'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ISO8601DateTime'; ofType: null; }; } }; 'userId': { name: 'userId'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; }; }; 'UserQuest': { kind: 'OBJECT'; name: 'UserQuest'; fields: { 'completedAt': { name: 'completedAt'; type: { kind: 'SCALAR'; name: 'ISO8601DateTime'; ofType: null; } }; 'id': { name: 'id'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'startedAt': { name: 'startedAt'; type: { kind: 'SCALAR'; name: 'ISO8601DateTime'; ofType: null; } }; 'status': { name: 'status'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; }; }; };