Skip to content

Commit

Permalink
Add check for variable only arguments (#7295)
Browse files Browse the repository at this point in the history
* Add check for variable only arguemnts

* More content

* Improve things

* Improve example

* Simplify next exercise selection

* Use 2DP and fix multiplication

* WIP

* Improve unlocking logic

* Fix tests

* Update idx

* WIP

* Updates

* Add golf introduction

* Double check instructions

* Don't show locked exercises

* Green
  • Loading branch information
iHiD authored Jan 14, 2025
1 parent 96081e9 commit 2dab2d4
Show file tree
Hide file tree
Showing 83 changed files with 1,436 additions and 417 deletions.
27 changes: 27 additions & 0 deletions app/commands/bootcamp/exercise/available_for_user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class Bootcamp::Exercise::AvailableForUser
include Mandate

initialize_with :exercise, :user

def call
# If the exercise is gloabally locked, it's locked
return false if exercise.locked?

# Otherwise the previous solution must be completed
previous_exercises_completed?
end

private
delegate :project, to: :exercise

def previous_exercises_completed?
previous_exercises = project.exercises.where.not(id: exercise.id).select do |prev_ex|
prev_ex.level_idx < exercise.level_idx ||
(prev_ex.level_idx == exercise.level_idx && prev_ex.idx < exercise.idx)
end

completed_exercise_ids = user.bootcamp_solutions.completed.where(exercise_id: previous_exercises.map(&:id)).pluck(:exercise_id)

previous_exercises.all? { |ex| completed_exercise_ids.include?(ex.id) }
end
end
20 changes: 4 additions & 16 deletions app/commands/bootcamp/select_next_exercise.rb
Original file line number Diff line number Diff line change
@@ -1,28 +1,16 @@
class Bootcamp::SelectNextExercise
include Mandate

initialize_with :user, project: nil
initialize_with :user

def call
return next_user_project_exercise if next_user_project_exercise

Bootcamp::Exercise.unlocked.where.not(project: user.bootcamp_projects).
Bootcamp::Exercise.unlocked.
where.not(id: completed_exercise_ids).first
end

def user_project
if project
user_project = Bootcamp::UserProject.for!(user, project)
return user_project if user_project.available?
end

user.bootcamp_user_projects.where(status: :available).first
end

private
memoize
def next_user_project_exercise = user_project&.next_exercise

def completed_exercise_ids
user.bootcamp_solutions.where.not(completed_at: nil).select(:exercise_id)
user.bootcamp_solutions.completed.select(:exercise_id)
end
end
8 changes: 7 additions & 1 deletion app/commands/bootcamp/solution/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ class Bootcamp::Solution::Create
initialize_with :user, :exercise

def call
return existing_solution if existing_solution

guard!

begin
Expand All @@ -21,8 +23,12 @@ def call
end

private
def existing_solution
Bootcamp::Solution.find_by(user:, exercise:)
end

def guard!
raise ExerciseLockedError unless user_project.exercise_available?(exercise)
raise ExerciseLockedError unless Bootcamp::Exercise::AvailableForUser.(exercise, user)
end

def code
Expand Down
6 changes: 3 additions & 3 deletions app/commands/bootcamp/update_user_level.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@ def call

max = level_idx
end
user.bootcamp_data.update!(level_idx: max)
user.bootcamp_data.update!(level_idx: max + 1)
end

memoize
def exercise_ids_by_level_idx
Bootcamp::Exercise.pluck(:level_idx, :id).
group_by(&:first).
transform_values { |v| v.map(&:last) }.
transform_values { |v| v.map(&:last).sort }.
sort.to_h
end

def solved_exercise_ids_by_level_idx
user.bootcamp_solutions.completed.joins(:exercise).pluck(:level_idx, :exercise_id).
group_by(&:first).
transform_values { |v| v.map(&:last) }
transform_values { |v| v.map(&:last).sort }
end
end
2 changes: 1 addition & 1 deletion app/controllers/api/bootcamp/solutions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class API::Bootcamp::SolutionsController < API::Bootcamp::BaseController
def complete
Bootcamp::Solution::Complete.(@solution)

next_exercise = Bootcamp::SelectNextExercise.(current_user, project: @solution.project)
next_exercise = Bootcamp::SelectNextExercise.(current_user)

render json: {
next_exercise: SerializeBootcampExercise.(next_exercise)
Expand Down
5 changes: 5 additions & 0 deletions app/controllers/bootcamp/base_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class Bootcamp::BaseController < ApplicationController
layout "bootcamp-ui"
before_action :redirect_unless_attendee!
before_action :setup_bootcamp_data!

private
def redirect_unless_attendee!
Expand All @@ -20,6 +21,10 @@ def redirect_unless_attendee!
redirect_to bootcamp_path
end

def setup_bootcamp_data!
current_user.bootcamp_data || current_user.create_bootcamp_data!
end

def use_project
@project = Bootcamp::Project.find_by!(slug: params[:project_slug])
end
Expand Down
5 changes: 4 additions & 1 deletion app/controllers/bootcamp/dashboard_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ class Bootcamp::DashboardController < Bootcamp::BaseController
def index
@exercise = Bootcamp::SelectNextExercise.(current_user)
@solution = current_user.bootcamp_solutions.find_by(exercise: @exercise)
@level = Bootcamp::Level.find_by!(idx: 1)

level_idx = [Bootcamp::Settings.level_idx, current_user.bootcamp_data.level_idx].min
level_idx = 1 if level_idx.zero?
@level = Bootcamp::Level.find_by!(idx: level_idx)
end
end
151 changes: 63 additions & 88 deletions app/css/bootcamp/pages/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,108 +3,83 @@ body.namespace-bootcamp.controller-dashboard.action-index {
}

#page-bootcamp-dashboard {
section.intro {
@apply pt-24;
h1 {
@apply text-[36px] leading-140;
@apply font-bold text-textColor1;
@apply mb-4;
}
h2 {
@apply text-22 leading-150;
@apply font-semibold text-textColor1;
@apply mt-20 mb-4;
}
p.large {
@apply text-20 leading-150;
@apply mb-8;
}
@apply pt-24;
.exercise {
@paply w-full;
@apply py-12 px-16 rounded-8 border-1 border-borderColor5 block;
@apply shadow-base flex flex-col items-stretch;
@apply bg-white;
}
h1 {
@apply text-[36px] leading-140;
@apply font-bold text-textColor1;
@apply mb-4;
}
.level-content {
p,
ul {
@apply text-16 leading-150;
@apply text-20 leading-150;
@apply mb-8;
}
ul {
@apply list-disc pl-20;
}
}
h2 {
@apply text-23 leading-150;
@apply font-semibold text-textColor1;
}
h3 {
@apply text-20 leading-150;
@apply mb-4;
@apply font-semibold;
}
h4 {
@apply text-16 leading-150;
@apply mb-4;
@apply font-semibold;
}
.tag {
@apply flex items-center;
@apply border-1 rounded-100;
@apply font-semibold leading-170;
@apply px-12 py-4 ml-auto;
@apply whitespace-nowrap;

section.normal {
@apply pt-24;
.exercise {
@paply w-full;
@apply py-12 px-16 rounded-8 border-1 border-borderColor5 block;
@apply shadow-base flex flex-col items-stretch;
@apply bg-white;
}
h1 {
@apply text-[36px] leading-140;
@apply font-bold text-textColor1;
@apply mb-4;
&.completed {
background: #e7fdf6;
border-color: #43b593;
color: #43b593;
}
p.large {
@apply text-20 leading-150;
@apply mb-8;
}
h2 {
@apply text-23 leading-150;
@apply font-semibold text-textColor1;
}
p {
@apply text-16 leading-140;
}

.c-youtube-container {
@apply mt-20;
iframe {
@apply w-full;
}
h3 {
@apply text-20 leading-150;
@apply mb-4;
@apply font-semibold;
}
}
.rhs {
.section-link {
@apply border-1 border-borderColor5 block rounded-5 px-20 py-12 bg-white shadow-sm;
@apply flex gap-16;
img {
@apply w-[40px] h-[40px];
}
h4 {
@apply text-16 leading-150;
@apply mb-4;
@apply font-semibold;
}
.tag {
@apply flex items-center;
@apply border-1 rounded-100;
@apply font-semibold leading-170;
@apply px-12 py-4 ml-auto;
@apply whitespace-nowrap;

&.completed {
background: #e7fdf6;
border-color: #43b593;
color: #43b593;
}
@apply text-18 font-semibold mb-2;
}
p {
@apply text-16 leading-140;
}

.c-youtube-container {
@apply mt-20;
iframe {
@apply w-full;
}
@apply text-15 leading-140;
}
}
.rhs {
.section-link {
@apply border-1 border-borderColor5 block rounded-5 px-20 py-12 bg-white shadow-sm;
@apply flex gap-16;
img {
@apply w-[40px] h-[40px];
}
h4 {
@apply text-18 font-semibold mb-2;
}
p {
@apply text-15 leading-140;
}
}

.level-number {
@apply rounded-circle text-[24px] leading-140;
@apply grid place-items-center flex-shrink-0;
@apply border-3 border-purple;
@apply text-purple font-bold;
@apply w-[48px] h-[48px];
}
.level-number {
@apply rounded-circle text-[24px] leading-140;
@apply grid place-items-center flex-shrink-0;
@apply border-3 border-purple;
@apply text-purple font-bold;
@apply w-[48px] h-[48px];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class AnimationTimeline {

public populateTimeline(animations: Animation[]) {
animations.forEach((animation: Animation) => {
// console.log(animation.offset)
this.animationTimeline.add(
{ ...animation, ...animation.transformations },
animation.offset
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ export function placeholderExtension() {
}

function generateArrowSVG(length: number, right: number, hoverRight: number) {
console.log(hoverRight)
let offset = 30
right = right + offset
length = length + 10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import type { StaticError } from '@/interpreter/error'
import { INFO_HIGHLIGHT_COLOR } from '../CodeMirror/extensions/lineHighlighter'
import { scrollToHighlightedLine } from './scrollToHighlightedLine'

const FRAME_DURATION = 50

export function useScrubber({
setIsPlaying,
testResult,
Expand All @@ -32,7 +34,7 @@ export function useScrubber({
testResult.animationTimeline.onUpdate((anime) => {
setTimeout(() => {
setValue(anime.currentTime)
}, 50)
}, FRAME_DURATION)
})
}
}, [testResult.view?.id, testResult.animationTimeline?.completed])
Expand Down Expand Up @@ -155,7 +157,7 @@ export function useScrubber({
targets: { value },
// if progress is closer to duration than time, then snap to duration
value: closestTime,
duration: 50,
duration: FRAME_DURATION,
easing: 'easeOutQuad',
update: function (anim) {
const newTime = Number(anim.animations[0].currentValue)
Expand Down Expand Up @@ -214,7 +216,7 @@ export function useScrubber({
scrubberValueAnimation.current = anime({
targets: { value },
value: targetTime,
duration: 50,
duration: FRAME_DURATION,
easing: 'easeOutQuad',
update: function (anim) {
const animatedTime = Number(anim.animations[0].currentValue)
Expand Down Expand Up @@ -253,7 +255,7 @@ export function useScrubber({
scrubberValueAnimation.current = anime({
targets: { value },
value: targetTime,
duration: 50,
duration: FRAME_DURATION,
easing: 'easeOutQuad',
update: function (anim) {
const animatedTime = Number(anim.animations[0].currentValue)
Expand Down
Loading

0 comments on commit 2dab2d4

Please sign in to comment.