diff --git a/app/controllers/bootcamp/base_controller.rb b/app/controllers/bootcamp/base_controller.rb
new file mode 100644
index 0000000000..a811eaff30
--- /dev/null
+++ b/app/controllers/bootcamp/base_controller.rb
@@ -0,0 +1,15 @@
+class Bootcamp::BaseController < ApplicationController
+ layout "bootcamp-ui"
+
+ def use_project
+ @project = Bootcamp::Project.find_by!(slug: params[:project_slug])
+ end
+
+ def use_exercise
+ @exercise = @project.exercises.find_by!(slug: params[:exercise_slug])
+ end
+
+ def use_solution
+ @solution = current_user.bootcamp_solutions.find_by!(uuid: params[:solution_uuid])
+ end
+end
diff --git a/app/controllers/bootcamp/concepts_controller.rb b/app/controllers/bootcamp/concepts_controller.rb
new file mode 100644
index 0000000000..c05c713433
--- /dev/null
+++ b/app/controllers/bootcamp/concepts_controller.rb
@@ -0,0 +1,9 @@
+class Bootcamp::ConceptsController < Bootcamp::BaseController
+ def index
+ @concepts = Bootcamp::Concept.all
+ end
+
+ def show
+ @concept = Bootcamp::Concept.find_by!(slug: params[:slug])
+ end
+end
diff --git a/app/controllers/bootcamp/dashboard_controller.rb b/app/controllers/bootcamp/dashboard_controller.rb
new file mode 100644
index 0000000000..3a953a5555
--- /dev/null
+++ b/app/controllers/bootcamp/dashboard_controller.rb
@@ -0,0 +1,6 @@
+class Bootcamp::DashboardController < Bootcamp::BaseController
+ def index
+ @exercise = Bootcamp::SelectNextExercise.(current_user)
+ @solution = current_user.bootcamp_solutions.find_by(exercise: @exercise)
+ end
+end
diff --git a/app/controllers/bootcamp/exercises_controller.rb b/app/controllers/bootcamp/exercises_controller.rb
new file mode 100644
index 0000000000..937d83f089
--- /dev/null
+++ b/app/controllers/bootcamp/exercises_controller.rb
@@ -0,0 +1,22 @@
+class Bootcamp::ExercisesController < Bootcamp::BaseController
+ before_action :use_project
+ before_action :use_exercise, only: %i[show edit]
+
+ def index
+ @exercises = Bootcamp::Exercise.all
+ end
+
+ def show
+ redirect_to action: :edit
+ end
+
+ def edit
+ @solution = Bootcamp::Solution::Create.(current_user, @exercise)
+ rescue ExerciseLockedError
+ redirect_to action: :show
+ end
+
+ def use_exercise
+ @exercise = @project.exercises.find_by!(slug: params[:slug])
+ end
+end
diff --git a/app/controllers/bootcamp/levels_controller.rb b/app/controllers/bootcamp/levels_controller.rb
new file mode 100644
index 0000000000..80289570a6
--- /dev/null
+++ b/app/controllers/bootcamp/levels_controller.rb
@@ -0,0 +1,9 @@
+class Bootcamp::LevelsController < Bootcamp::BaseController
+ def index
+ @levels = Bootcamp::Level.all.index_by(&:idx)
+ end
+
+ def show
+ @level = Bootcamp::Level.find_by!(idx: params[:idx])
+ end
+end
diff --git a/app/controllers/bootcamp/projects_controller.rb b/app/controllers/bootcamp/projects_controller.rb
new file mode 100644
index 0000000000..18dcb3c6ef
--- /dev/null
+++ b/app/controllers/bootcamp/projects_controller.rb
@@ -0,0 +1,17 @@
+class Bootcamp::ProjectsController < Bootcamp::BaseController
+ before_action :use_project, only: %i[show]
+
+ def index
+ @user_projects = current_user.bootcamp_user_projects
+ end
+
+ def show
+ @exercises = @project.exercises
+ @solutions = current_user.bootcamp_solutions.where(exercise: @exercises).index_by(&:exercise_id)
+ end
+
+ def use_project
+ @project = Bootcamp::Project.find_by!(slug: params[:slug])
+ @user_project = current_user.bootcamp_user_projects.find_by!(project: @project)
+ end
+end
diff --git a/app/css/bootcamp/components/breadcrumbs.css b/app/css/bootcamp/components/breadcrumbs.css
new file mode 100644
index 0000000000..50b9f9e01a
--- /dev/null
+++ b/app/css/bootcamp/components/breadcrumbs.css
@@ -0,0 +1,36 @@
+.c-breadcrumbs {
+ @apply border-b-1 border-[#c8d5ef] mb-20;
+
+ .container {
+ @apply flex items-center;
+ @apply py-12;
+ @apply text-14 text-gray-800 leading-150;
+ @apply overflow-x-auto;
+ }
+
+ a {
+ @apply text-lightBlue;
+ @apply font-medium;
+ @apply mr-16;
+ @apply flex items-center;
+ @apply whitespace-nowrap;
+
+ .c-icon {
+ height: 20px;
+ width: 20px;
+ @apply mr-8;
+ filter: var(--primary-blue-filter);
+ }
+ }
+ .seperator {
+ @apply text-15 text-[#6E82AA];
+ @apply mr-16;
+ }
+ .title {
+ @apply mr-16;
+ @apply whitespace-nowrap;
+ }
+ .seperator {
+ @apply mr-16;
+ }
+}
diff --git a/app/css/bootcamp/components/codemirror.css b/app/css/bootcamp/components/codemirror.css
new file mode 100644
index 0000000000..9e977e557f
--- /dev/null
+++ b/app/css/bootcamp/components/codemirror.css
@@ -0,0 +1,87 @@
+@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap");
+@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap");
+
+.editor-wrapper {
+ overflow: hidden;
+ @apply h-[60%] flex-grow-0;
+ @apply rounded-br-3;
+ @apply border-1 border-slate-400;
+}
+.editor {
+ height: 100%;
+ background: #fff;
+ overflow-y: auto;
+ @apply flex flex-grow;
+
+ .cm-gutters {
+ background: transparent;
+ @apply border-r-0;
+ }
+ .cm-lineNumbers .cm-gutterElement,
+ .cm-icon-container-gutter .cm-gutterElement {
+ @apply grid items-center;
+ /* @apply p-0; */
+ }
+
+ .cm-icon-container-gutter .cm-gutterElement {
+ @apply px-4;
+ }
+ .cm-line {
+ padding: 2px 2px 2px 6px;
+ }
+ .cm-lockedLine,
+ .cm-lockedGutter {
+ opacity: 0.6;
+ background: #eee;
+ }
+ .ͼ1 .cm-underline {
+ text-decoration: none;
+ border-bottom: 2px solid red;
+ background: rgba(255, 0, 0, 0.2);
+ }
+}
+.cm-editor {
+ font-family: "Source Code Pro", "Courier New", Courier, monospace;
+ text-align: left;
+ width: 100%;
+ color: #aaa;
+ font-size: 16px;
+
+ .cm-scroller {
+ font-family: "Source Code Pro", "Courier New", Courier, monospace;
+ }
+ .cm-foldGutter span {
+ display: none;
+ }
+ .cm-gutterElement {
+ @apply pl-12 pr-8;
+ }
+}
+
+.declaration-arrow {
+ position: absolute;
+}
+
+.cm-line {
+ display: flex;
+ align-items: center;
+}
+
+.custom-tooltip {
+ font-family: Poppins;
+ font-size: 14px;
+ padding: 4px;
+ color: #130b43;
+ line-height: 160%;
+ background: #ffd38f;
+ border-radius: 8px;
+}
+
+.cm-tooltip {
+ border: none !important;
+}
+
+.cm-lineNumbers {
+ /* width: 2em; */
+ text-align: right;
+}
diff --git a/app/css/bootcamp/components/completed-bar.css b/app/css/bootcamp/components/completed-bar.css
new file mode 100644
index 0000000000..cf8cfbf2d7
--- /dev/null
+++ b/app/css/bootcamp/components/completed-bar.css
@@ -0,0 +1,25 @@
+.c-completed-bar {
+ @apply flex items-center py-12 px-32 shadow-lg rounded-8 mb-24;
+ @apply border-2 border-darkGreen;
+ @apply bg-[#B8EADB];
+
+ .check-mark-icon {
+ height: 32px;
+ width: 32px;
+ @apply mr-16;
+ }
+ .text {
+ @apply flex-grow;
+ @apply text-18 leading-150 font-semibold text-gray-900;
+ }
+ .stat {
+ @apply font-semibold text-gray-600;
+ }
+
+ c-prominent-link {
+ @apply text-lightBlue;
+ /* .c-icon {
+ filter: var(--iconFilterNotificationsProminentLink);
+ } */
+ }
+}
diff --git a/app/css/bootcamp/components/concept-widget.css b/app/css/bootcamp/components/concept-widget.css
new file mode 100644
index 0000000000..9c9daf8200
--- /dev/null
+++ b/app/css/bootcamp/components/concept-widget.css
@@ -0,0 +1,15 @@
+.c-concept-widget {
+ @apply py-12 px-16 rounded-8 border-1 block;
+ @apply flex flex-col items-stretch;
+ @apply bg-white;
+ @apply shadow-base;
+
+ .title {
+ @apply text-17 leading-140;
+ @apply font-semibold;
+ @apply mb-4;
+ }
+ .description {
+ @apply text-15 leading-140;
+ }
+}
diff --git a/app/css/bootcamp/components/editor-information-tooltip.css b/app/css/bootcamp/components/editor-information-tooltip.css
new file mode 100644
index 0000000000..143b01a68c
--- /dev/null
+++ b/app/css/bootcamp/components/editor-information-tooltip.css
@@ -0,0 +1,54 @@
+.information-tooltip {
+ max-width: 450px;
+ transition: opacity 0.3s;
+ @apply z-tooltip;
+ /* pointer-events: none; */
+ @apply text-15 leading-150;
+ @apply absolute rounded-8 opacity-0;
+
+ & :not(pre) > code {
+ @apply bg-thick-border-blue px-[6px] py-[1px] rounded-5;
+ }
+
+ pre {
+ @apply mt-4;
+ }
+
+ p:not(:last-of-type) {
+ @apply mb-10;
+ }
+
+ &.description {
+ @apply py-16 px-20;
+ @apply bg-white text-primary-blue;
+
+ filter: drop-shadow(0px 4px 8px rgba(79, 114, 205, 0.5));
+ .tooltip-arrow {
+ @apply bg-white;
+ }
+ }
+
+ &.error {
+ @apply bg-white border-2 border-red-500;
+
+ filter: drop-shadow(0px 4px 8px rgba(79, 114, 205, 0.5));
+
+ .tooltip-arrow {
+ @apply bg-white border-2 border-red-500;
+ }
+
+ h2 {
+ @apply text-red-900;
+ @apply font-semibold;
+ @apply py-8 px-20;
+ @apply relative z-tooltip-content bg-red-100;
+ @apply rounded-t-8;
+ }
+ .content {
+ @apply text-primary-blue;
+ @apply pt-10 px-20 pb-12;
+ @apply relative z-tooltip-content bg-white;
+ @apply rounded-b-8;
+ }
+ }
+}
diff --git a/app/css/bootcamp/components/exercise-widget.css b/app/css/bootcamp/components/exercise-widget.css
new file mode 100644
index 0000000000..e1a7897603
--- /dev/null
+++ b/app/css/bootcamp/components/exercise-widget.css
@@ -0,0 +1,63 @@
+.c-exercise-widget {
+ @apply py-12 px-12 rounded-8 border-1 block;
+ @apply flex flex-col items-stretch;
+ @apply bg-white;
+
+ &.available,
+ &.in_progress {
+ @apply shadow-base;
+
+ .tag {
+ @apply border-gray-300;
+ background-image: url("icons/available.svg");
+ }
+ }
+ &.completed {
+ }
+ &.locked {
+ @apply bg-gray-200;
+ @apply opacity-[0.5] cursor-not-allowed;
+
+ .tag {
+ @apply border-gray-400;
+ background-image: url("icons/lock.svg");
+ }
+ }
+
+ img {
+ @apply w-[80px] h-[80px] mr-12;
+ }
+
+ .title {
+ @apply mr-12;
+ }
+ .project-title {
+ @apply text-14 leading-150;
+ @apply font-semibold text-gray-900;
+ }
+ .exercise-title {
+ @apply text-17 leading-140;
+ @apply font-semibold;
+ @apply mb-2;
+ }
+ .description {
+ @apply text-15 leading-140;
+ }
+ .tag {
+ width: 20px;
+ height: 20px;
+ flex-shrink: 0;
+ background-position: center center;
+ background-size: 12px;
+ background-repeat: no-repeat;
+
+ @apply ml-auto;
+ @apply border-1 rounded-circle;
+
+ &.completed {
+ background: #e7fdf6;
+ border-color: #43b593;
+ color: #43b593;
+ }
+ }
+}
diff --git a/app/css/bootcamp/components/nav.css b/app/css/bootcamp/components/nav.css
new file mode 100644
index 0000000000..b9c10312a3
--- /dev/null
+++ b/app/css/bootcamp/components/nav.css
@@ -0,0 +1,7 @@
+.c-nav {
+ @apply flex flex-row items-center gap-10;
+ @apply mb-12;
+ a {
+ @apply font-semibold text-16 text-[blue];
+ }
+}
diff --git a/app/css/bootcamp/components/project-widget.css b/app/css/bootcamp/components/project-widget.css
new file mode 100644
index 0000000000..f8f5533ba6
--- /dev/null
+++ b/app/css/bootcamp/components/project-widget.css
@@ -0,0 +1,56 @@
+.c-project-widget {
+ @apply py-12 px-16 rounded-8 border-1 block;
+ @apply flex flex-col items-stretch;
+ @apply bg-white;
+ @apply relative;
+
+ &.available {
+ @apply shadow-base;
+
+ .tag {
+ @apply border-gray-300;
+ background-image: url("icons/available.svg");
+ }
+ }
+ &.completed {
+ }
+ &.locked {
+ @apply bg-gray-200;
+ @apply opacity-[0.5] cursor-not-allowed;
+
+ .tag {
+ @apply border-gray-400;
+ background-image: url("icons/lock.svg");
+ }
+ }
+
+ img {
+ @apply w-[80px] h-[80px] mr-16;
+ }
+
+ .title {
+ @apply text-17 leading-140;
+ @apply font-semibold;
+ @apply mb-2;
+ }
+ .description {
+ @apply text-15 leading-140;
+ }
+ .tag {
+ width: 20px;
+ height: 20px;
+ flex-shrink: 0;
+ background-position: center center;
+ background-size: 12px;
+ background-repeat: no-repeat;
+
+ @apply ml-auto;
+ @apply border-1 rounded-circle;
+
+ &.completed {
+ background: #e7fdf6;
+ border-color: #43b593;
+ color: #43b593;
+ }
+ }
+}
diff --git a/app/css/bootcamp/components/prose.css b/app/css/bootcamp/components/prose.css
new file mode 100644
index 0000000000..7adcf39a1c
--- /dev/null
+++ b/app/css/bootcamp/components/prose.css
@@ -0,0 +1,87 @@
+.c-prose {
+ --prose-base-text-size: 19px;
+ &.c-prose-small {
+ --prose-base-text-size: 18px;
+ }
+ h3 {
+ font-size: calc(var(--prose-base-text-size) + 3px);
+ @apply leading-140;
+ @apply font-semibold;
+ }
+
+ h4 {
+ font-size: calc(var(--prose-base-text-size) + 1px);
+ @apply leading-140;
+ @apply font-semibold;
+ }
+
+ p,
+ li {
+ font-size: var(--prose-base-text-size);
+ @apply leading-150;
+ }
+ ul {
+ @apply list-disc;
+ }
+ ol {
+ @apply list-decimal;
+ }
+ ul,
+ ol {
+ @apply ml-20;
+ }
+ li {
+ &:not(:last-child) {
+ @apply mb-4;
+ }
+ }
+ pre {
+ @apply bg-blue-100;
+ @apply px-12 py-12;
+ @apply rounded-8;
+ code {
+ font-size: calc(var(--prose-base-text-size) - 1px);
+ }
+ }
+ *:not(pre) > code {
+ @apply bg-blue-100;
+ @apply py-2 px-4;
+ @apply rounded-5;
+ font-size: calc(var(--prose-base-text-size) - 3px);
+ @apply whitespace-nowrap;
+ }
+ strong {
+ @apply font-semibold;
+ }
+
+ h3 + p,
+ h3 + pre,
+ h3 + ul,
+ h3 + ol,
+ h3 + div,
+ h4 + p,
+ h4 + pre,
+ h4 + ul,
+ h4 + ol,
+ h4 + div {
+ @apply mt-6;
+ }
+ p + ul {
+ @apply mt-4;
+ }
+ p + p,
+ ul + p {
+ @apply mt-12;
+ }
+ p + pre {
+ @apply my-8;
+ }
+ pre + p {
+ @apply mt-12;
+ }
+
+ * + h3,
+ * + h4 {
+ @apply mt-20;
+ }
+}
diff --git a/app/css/bootcamp/components/rhs-list.css b/app/css/bootcamp/components/rhs-list.css
new file mode 100644
index 0000000000..760a7ca3c3
--- /dev/null
+++ b/app/css/bootcamp/components/rhs-list.css
@@ -0,0 +1,19 @@
+.c-rhs-list {
+ h2 {
+ @apply text-22 leading-140;
+
+ @apply font-semibold;
+ @apply mb-4;
+ }
+ p {
+ @apply text-16 leading-140;
+ @apply mb-16;
+ }
+
+ ul {
+ @apply flex flex-col gap-16;
+ }
+}
+.c-rhs-list + .c-rhs-list {
+ @apply mt-20;
+}
diff --git a/app/css/bootcamp/components/scrubber.css b/app/css/bootcamp/components/scrubber.css
new file mode 100644
index 0000000000..285806d9b3
--- /dev/null
+++ b/app/css/bootcamp/components/scrubber.css
@@ -0,0 +1,61 @@
+#scrubber {
+ background: white;
+ padding: 8px;
+ @apply border-t-2 border-t-jiki-purple border-opacity-50;
+ @apply flex justify-center items-center gap-8;
+
+ input[type="range"] {
+ -webkit-appearance: none;
+ @apply flex-grow;
+ height: 9px;
+ border-radius: 100px;
+ border: 1px solid;
+ @apply border-jiki-purple;
+ box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 0.12) inset;
+
+ &::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 24px;
+ height: 24px;
+ border-radius: 100%;
+ @apply outline-2 outline-white;
+ @apply bg-jiki-purple;
+ box-shadow: 0 0 1px 1px white;
+ cursor: pointer;
+ background-image: url("/scrubber.svg");
+ background-size: 13px;
+ @apply bg-no-repeat bg-center;
+ &:hover {
+ @apply bg-jiki-purple;
+ }
+ }
+ &::-moz-range-thumb {
+ @apply bg-jiki-purple;
+ width: 35px;
+ width: 18px;
+ border-radius: 20px;
+ cursor: pointer;
+ }
+ }
+
+ .frame-stepper-buttons,
+ .play-button {
+ img {
+ width: 24px;
+ height: 24px;
+ filter: invert(27%) sepia(88%) saturate(6980%) hue-rotate(260deg)
+ brightness(95%) contrast(102%);
+ }
+ }
+
+ .frame-stepper-buttons {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 8px;
+
+ button {
+ @apply border-1 border-indigo-300 rounded-5;
+ }
+ }
+}
diff --git a/app/css/bootcamp/components/site-header.css b/app/css/bootcamp/components/site-header.css
new file mode 100644
index 0000000000..ff0bdb9f64
--- /dev/null
+++ b/app/css/bootcamp/components/site-header.css
@@ -0,0 +1,20 @@
+.c-site-header {
+ @apply bg-white;
+ @apply border-b-1 border-[#ccc];
+ .container {
+ @apply py-16;
+ @apply flex flex-row gap-8 items-center;
+ img {
+ filter: invert(1);
+ }
+ .content {
+ @apply text-18;
+ }
+ }
+}
+
+body.controller-exercises.action-edit {
+ .c-site-header {
+ display: none;
+ }
+}
diff --git a/app/css/bootcamp/components/solve-exercise-page.css b/app/css/bootcamp/components/solve-exercise-page.css
new file mode 100644
index 0000000000..506c726870
--- /dev/null
+++ b/app/css/bootcamp/components/solve-exercise-page.css
@@ -0,0 +1,6 @@
+#solve-exercise-page {
+ .scenario-lhs {
+ @apply w-[50%] max-w-[350px];
+ @apply flex-shrink-0;
+ }
+}
diff --git a/app/css/bootcamp/components/tasks.css b/app/css/bootcamp/components/tasks.css
new file mode 100644
index 0000000000..2c07a9bb83
--- /dev/null
+++ b/app/css/bootcamp/components/tasks.css
@@ -0,0 +1,70 @@
+#tasks {
+ @apply bg-background-purple;
+ @apply h-[40%] py-16 px-24;
+ @apply flex-grow-0 flex-shrink-0;
+ @apply border-t-1 border-thin-border-blue;
+
+ h2 {
+ @apply mb-8;
+ }
+
+ .list {
+ @apply flex flex-col gap-[12px];
+ }
+
+ .task {
+ @apply flex gap-12 items-center;
+ @apply relative;
+ @apply text-15;
+
+ .imgs {
+ @apply block w-[18px] h-[18px];
+ @apply flex-shrink-0;
+ @apply relative;
+
+ img {
+ @apply absolute inset-0;
+ @apply block w-full h-full;
+ @apply opacity-0;
+ transition: opacity ease-in 0.5s;
+ }
+ }
+ .confetti {
+ height: 120px;
+ width: 120px;
+ position: absolute;
+ left: -50px;
+ top: -50px;
+ @apply z-overlay;
+ }
+
+ &.hidden {
+ display: none;
+ opacity: 0;
+ max-height: 0;
+ }
+
+ &.inactive {
+ opacity: 0.5;
+ /* transition: all 0.8s ease-in 1s; */
+ }
+
+ &.active {
+ opacity: 1;
+ max-height: 70px;
+ transition: all 0.3s ease-in 0.3s, opacity 0.1s ease-in 0.4s;
+ .pending-icon {
+ @apply opacity-100;
+ }
+ }
+
+ &.completed {
+ filter: grayscale(1);
+ opacity: 0.5;
+ transition: all 0.3s ease-in 300ms;
+ .completed-icon {
+ opacity: 1;
+ }
+ }
+ }
+}
diff --git a/app/css/bootcamp/components/test-buttons.css b/app/css/bootcamp/components/test-buttons.css
new file mode 100644
index 0000000000..2cd698ea48
--- /dev/null
+++ b/app/css/bootcamp/components/test-buttons.css
@@ -0,0 +1,16 @@
+.test-button {
+ @apply relative p-4 w-[32px] h-[32px] grid place-content-center rounded-5;
+ transition: background-color 0.3s ease-out;
+
+ &.idle {
+ background-color: #f59e0b;
+ }
+
+ &.pass {
+ background-color: #22c55e;
+ }
+
+ &.fail {
+ background-color: #e11d48;
+ }
+}
diff --git a/app/css/bootcamp/components/toggle-switch.css b/app/css/bootcamp/components/toggle-switch.css
new file mode 100644
index 0000000000..6808e3b8aa
--- /dev/null
+++ b/app/css/bootcamp/components/toggle-switch.css
@@ -0,0 +1,75 @@
+:root {
+ --switch-width: 40px;
+ --switch-height: calc(var(--switch-width) / 2);
+ --slider-background-color: #ccc;
+ --slider-checked-background-color: #7128f5;
+ --slider-focus-box-shadow: 0 0 1px var(--slider-checked-background-color);
+ --thumb-size: calc(var(--switch-height) - 8px);
+ --thumb-left-offset: 4px;
+ --thumb-bottom-offset: 4px;
+ --thumb-background-color: white;
+ --transition-duration: 100ms;
+}
+
+.switch {
+ position: relative;
+ display: inline-block;
+ width: var(--switch-width);
+ height: var(--switch-height);
+}
+
+/* Hide default HTML checkbox */
+.switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+/* The slider */
+.slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: var(--slider-background-color);
+ transition: var(--transition-duration);
+}
+
+.slider:before {
+ position: absolute;
+ content: "";
+ height: var(--thumb-size);
+ width: var(--thumb-size);
+ left: var(--thumb-left-offset);
+ bottom: var(--thumb-bottom-offset);
+ background-color: var(--thumb-background-color);
+ transition: var(--transition-duration);
+}
+
+input:checked + .slider {
+ background-color: var(--slider-checked-background-color);
+}
+
+input:focus + .slider {
+ box-shadow: var(--slider-focus-box-shadow);
+}
+
+input:checked + .slider:before {
+ transform: translateX(
+ calc(
+ var(--switch-width) - var(--thumb-size) - var(--thumb-left-offset) *
+ 2
+ )
+ );
+}
+
+/* Rounded sliders */
+.slider.round {
+ border-radius: calc(var(--switch-height) / 2);
+}
+
+.slider.round:before {
+ border-radius: 50%;
+}
diff --git a/app/css/bootcamp/exercises/draw.css b/app/css/bootcamp/exercises/draw.css
new file mode 100644
index 0000000000..ffcb93f8ae
--- /dev/null
+++ b/app/css/bootcamp/exercises/draw.css
@@ -0,0 +1,13 @@
+#solve-exercise-page {
+ .exercise-draw {
+ @apply border-1 border-gray-500;
+ .bg-grid {
+ @apply absolute inset-0;
+ }
+ .canvas {
+ @apply absolute inset-0;
+ width: 100%;
+ height: 100%;
+ }
+ }
+}
diff --git a/app/css/bootcamp/exercises/maze.css b/app/css/bootcamp/exercises/maze.css
new file mode 100644
index 0000000000..061b382239
--- /dev/null
+++ b/app/css/bootcamp/exercises/maze.css
@@ -0,0 +1,136 @@
+#view-container {
+ position: relative;
+ padding: 10px;
+ background: white;
+ container-type: size;
+
+ aspect-ratio: 1;
+ /* min-width: 50%;
+ max-width: 70%; */
+ flex-shrink: 1;
+ /* flex-grow: 1; */
+ /*
+ & > *:first-child {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ &:before {
+ content: "";
+ padding-top: 100%;
+ }
+ }
+ */
+ .exercise-container {
+ aspect-ratio: 1;
+ max-height: 100cqh;
+ max-width: 100cqw;
+ background: red;
+ position: relative;
+ }
+}
+
+.exercise-maze {
+ --cellWidth: calc(100% / var(--gridSize));
+
+ .cells {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ border: 0;
+
+ display: grid;
+ grid-template-columns: repeat(var(--gridSize), var(--cellWidth));
+ grid-template-rows: repeat(var(--gridSize), var(--cellWidth));
+ gap: 0px;
+
+ .cell {
+ width: 100%;
+ height: 100%;
+ /* width: var(--cellWidth);
+ height: var(--cellWidth); */
+ background-color: white;
+ border: 0.5px solid #000;
+ &.blocked {
+ background-color: red;
+ }
+ &.start {
+ background-color: lightblue;
+ }
+ &.target {
+ background-color: lightgreen;
+ }
+ &.bomb {
+ background-color: purple;
+ }
+ }
+
+ .character {
+ width: calc(var(--cellWidth) * 0.7);
+ height: calc(var(--cellWidth) * 0.7);
+ background-color: lightblue;
+ border: 1px solid green;
+ border-radius: 50%;
+ position: absolute;
+ margin-left: 5px;
+ margin-top: 4px;
+ top: 0;
+ left: 0;
+
+ &::before,
+ &::after {
+ content: "";
+ position: absolute;
+ width: 7px;
+ height: 7px;
+ background-color: green;
+ border-radius: 50%;
+ bottom: 80%;
+ }
+
+ &::before {
+ left: 55%;
+ }
+
+ &::after {
+ right: 55%;
+ }
+ }
+ }
+
+ .character {
+ width: calc(var(--cellWidth) * 0.7);
+ height: calc(var(--cellWidth) * 0.7);
+ background-color: lightblue;
+ border: 1px solid green;
+ border-radius: 50%;
+ position: absolute;
+ margin-left: 5px;
+ margin-top: 4px;
+ top: 0;
+ left: 0;
+
+ &::before,
+ &::after {
+ content: "";
+ position: absolute;
+ width: 7px;
+ height: 7px;
+ background-color: green;
+ border-radius: 50%;
+ bottom: 70%;
+ }
+
+ &::before {
+ left: 55%;
+ }
+
+ &::after {
+ right: 55%;
+ }
+ }
+ .canvas {
+ }
+}
diff --git a/app/css/bootcamp/exercises/wordle.css b/app/css/bootcamp/exercises/wordle.css
new file mode 100644
index 0000000000..ea538befbd
--- /dev/null
+++ b/app/css/bootcamp/exercises/wordle.css
@@ -0,0 +1,51 @@
+.exercise-wordle {
+ .board {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ border: 0;
+
+ display: grid;
+ gap: 1%;
+ grid-template-rows: repeat(6, 1fr);
+ }
+
+ .guess {
+ display: grid;
+ gap: 1%;
+ grid-template-columns: repeat(5, 1fr);
+ container-type: size;
+ }
+
+ .letter {
+ height: 100%;
+ font-size: 70cqh;
+ background-color: #d3d3d3;
+ border: 1px solid #ccc;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-weight: bold;
+ text-transform: uppercase;
+ color: #333;
+ position: relative;
+ }
+
+ .letter[data-state="correct"] {
+ background-color: #6aaa64;
+ color: white;
+ }
+
+ .letter[data-state="present"] {
+ background-color: #c9b458;
+ color: white;
+ }
+
+ .letter[data-state="absent"] {
+ background-color: #787c7e;
+ color: white;
+ }
+}
diff --git a/app/css/bootcamp/pages/admin/base.css b/app/css/bootcamp/pages/admin/base.css
new file mode 100644
index 0000000000..1ce13b77b6
--- /dev/null
+++ b/app/css/bootcamp/pages/admin/base.css
@@ -0,0 +1,8 @@
+.page-admin {
+ h1 {
+ @apply font-semibold text-22 mb-20;
+ }
+ h2 {
+ @apply font-semibold text-18 mb-8;
+ }
+}
diff --git a/app/css/bootcamp/pages/admin/exercises.css b/app/css/bootcamp/pages/admin/exercises.css
new file mode 100644
index 0000000000..286a37dd27
--- /dev/null
+++ b/app/css/bootcamp/pages/admin/exercises.css
@@ -0,0 +1,76 @@
+#page-admin-exercises {
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-bottom: 20px;
+ thead {
+ background-color: #f5f5f5;
+ }
+ th {
+ text-align: left;
+ }
+ thead th {
+ border-bottom: 1px solid #ddd;
+ }
+ tbody {
+ td {
+ border-bottom: 1px solid #ddd;
+ a {
+ @apply text-blue-500 font-semibold;
+ }
+ }
+ }
+ th,
+ td {
+ padding: 10px;
+ border: 1px solid #eee;
+
+ &:first-child {
+ width: 1px;
+ }
+ &:nth-child(2) {
+ width: 250px;
+ }
+ &:nth-child(3) {
+ width: 20px;
+ @apply relative;
+ }
+
+ &:nth-child(4) {
+ }
+ &:nth-child(5),
+ &:nth-child(6) {
+ width: 1px;
+ @apply whitespace-nowrap;
+ @apply text-right;
+ }
+ }
+ .level {
+ @apply absolute inset-0 p-10;
+ @apply flex flex-col justify-center;
+ @apply font-semibold text-right;
+ &.positive {
+ @apply bg-green-100 text-green-700;
+ }
+ &.negative {
+ @apply bg-red-100 text-red-700;
+ }
+ }
+ .bubbles {
+ @apply flex flex-wrap gap-6;
+ }
+ .bubble {
+ @apply text-gray-600;
+ @apply border-1 border-gray-400;
+ @apply rounded-5;
+ @apply py-2 px-4;
+ @apply text-13 font-semibold;
+ &.locked {
+ @apply bg-red-100 text-red-500 border-red-300;
+ }
+ &.unlocked {
+ @apply bg-green-100 text-green-500 border-green-300;
+ }
+ }
+ }
+}
diff --git a/app/css/bootcamp/pages/concept.css b/app/css/bootcamp/pages/concept.css
new file mode 100644
index 0000000000..e800c29e3f
--- /dev/null
+++ b/app/css/bootcamp/pages/concept.css
@@ -0,0 +1,44 @@
+body.controller-concepts.action-show {
+ @apply bg-grad-basic;
+}
+#page-concept {
+ header {
+ @apply pb-16;
+ .details {
+ @apply flex items-center gap-20;
+ }
+ /* img {
+ @apply w-[128px] h-[128px];
+ } */
+ h1 {
+ @apply text-[39px] leading-140;
+ @apply font-bold;
+ }
+ }
+ .nav {
+ @apply flex flex-row items-center gap-10;
+ @apply mb-12;
+ a {
+ @apply font-semibold text-16 text-[blue];
+ }
+ }
+
+ .lhs {
+ @apply max-w-[800px];
+ }
+ .rhs {
+ @apply flex-grow w-full max-w-[380px] ml-auto;
+ }
+
+ ul.descendants {
+ @apply list-disc pl-6 mt-10;
+ li {
+ @apply text-16;
+ @apply mb-4;
+ a {
+ display: inline-flex;
+ flex-direction: column;
+ }
+ }
+ }
+}
diff --git a/app/css/bootcamp/pages/dashboard.css b/app/css/bootcamp/pages/dashboard.css
new file mode 100644
index 0000000000..f0d7e9918d
--- /dev/null
+++ b/app/css/bootcamp/pages/dashboard.css
@@ -0,0 +1,44 @@
+#page-dashboard {
+ h2 {
+ @apply text-20 leading-150;
+ @apply font-semibold;
+ @apply mb-4;
+ }
+ p {
+ @apply text-16 leading-150;
+ @apply mb-8;
+ }
+
+ .exercise {
+ @apply py-12 px-16 rounded-8 border-1 block;
+ @apply shadow-base flex flex-col items-stretch;
+ @apply bg-white;
+
+ 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;
+
+ &.completed {
+ background: #e7fdf6;
+ border-color: #43b593;
+ color: #43b593;
+ }
+ }
+ p {
+ @apply text-15 leading-140;
+ }
+ }
+}
diff --git a/app/css/bootcamp/pages/level.css b/app/css/bootcamp/pages/level.css
new file mode 100644
index 0000000000..71ae9a61f1
--- /dev/null
+++ b/app/css/bootcamp/pages/level.css
@@ -0,0 +1,35 @@
+#page-level {
+ header {
+ @apply bg-grad-basic;
+ @apply pb-16;
+
+ .details {
+ @apply flex flex-col items-start;
+ .status {
+ @apply border-1 rounded-100;
+ @apply font-semibold leading-170;
+ @apply px-12 py-4;
+ @apply whitespace-nowrap;
+ &.completed {
+ @apply bg-green-100 text-green-700 border-green-400;
+ }
+ &.in-progress {
+ @apply bg-blue-100 text-blue-700 border-blue-400;
+ }
+ }
+ }
+ /* img {
+ @apply w-[128px] h-[128px];
+ } */
+ h1 {
+ @apply text-[39px] leading-140;
+ @apply font-bold;
+ }
+ }
+ .lhs {
+ /* @apply flex-grow; */
+ }
+ .rhs {
+ @apply flex-grow w-full max-w-[400px];
+ }
+}
diff --git a/app/css/bootcamp/pages/project.css b/app/css/bootcamp/pages/project.css
new file mode 100644
index 0000000000..a12da8e746
--- /dev/null
+++ b/app/css/bootcamp/pages/project.css
@@ -0,0 +1,44 @@
+body.controller-projects.action-show {
+ @apply bg-grad-basic;
+}
+#page-project {
+ header {
+ .project-bar {
+ @apply flex items-center gap-20 mb-20;
+ img {
+ @apply w-[128px] h-[128px];
+ }
+ h1 {
+ @apply text-[39px] leading-140;
+ @apply font-bold;
+ }
+ .tags {
+ @apply flex;
+ .tag {
+ @apply flex items-center;
+ @apply border-1 rounded-100;
+ @apply font-semibold leading-170;
+ @apply px-12 py-4;
+ @apply whitespace-nowrap;
+
+ &.completed {
+ background: #e7fdf6;
+ border-color: #43b593;
+ color: #43b593;
+ }
+ }
+ }
+ }
+ }
+ .lhs {
+ .introduction {
+ @apply max-w-[700px];
+ }
+ }
+ .rhs {
+ @apply w-[400px] flex-shrink-0;
+ @apply ml-auto;
+ }
+ .introduction {
+ }
+}
diff --git a/app/css/bootcamp/pages/projects.css b/app/css/bootcamp/pages/projects.css
new file mode 100644
index 0000000000..549cdbb893
--- /dev/null
+++ b/app/css/bootcamp/pages/projects.css
@@ -0,0 +1,29 @@
+body.controller-projects.action-index {
+ @apply bg-grad-basic;
+}
+#page-projects {
+ header {
+ .title-bar {
+ @apply pb-20;
+ h1 {
+ @apply text-[39px] leading-140;
+ @apply font-bold;
+ @apply mb-4;
+ }
+ p {
+ @apply text-18 leading-140;
+ }
+ }
+ }
+ .lhs {
+ .introduction {
+ @apply max-w-[700px];
+ }
+ }
+ .rhs {
+ @apply w-[400px] flex-shrink-0;
+ @apply ml-auto;
+ }
+ .introduction {
+ }
+}
diff --git a/app/css/bootcamp/variables.css b/app/css/bootcamp/variables.css
new file mode 100644
index 0000000000..6ded0b84ce
--- /dev/null
+++ b/app/css/bootcamp/variables.css
@@ -0,0 +1,5 @@
+.bg-grad-basic {
+ background-image: linear-gradient(#e1ebff, rgba(225, 235, 255, 0));
+ background-size: 20%;
+ background-repeat: repeat-x;
+}
diff --git a/app/css/packs/bootcamp-ui.css b/app/css/packs/bootcamp-ui.css
new file mode 100644
index 0000000000..70174c68fe
--- /dev/null
+++ b/app/css/packs/bootcamp-ui.css
@@ -0,0 +1,36 @@
+@import "../tailwind";
+@import "../defaults";
+@import "../fonts";
+@import "../ui-kit/all";
+
+@import "../bootcamp/variables";
+
+@import "../bootcamp/components/solve-exercise-page";
+@import "../bootcamp/components/rhs-list";
+@import "../bootcamp/components/breadcrumbs";
+@import "../bootcamp/components/completed-bar";
+@import "../bootcamp/components/site-header";
+@import "../bootcamp/components/codemirror";
+@import "../bootcamp/components/scrubber";
+@import "../bootcamp/components/prose";
+@import "../bootcamp/components/editor-information-tooltip";
+@import "../bootcamp/components/tasks";
+@import "../bootcamp/components/toggle-switch";
+@import "../bootcamp/components/test-buttons";
+
+@import "../bootcamp/components/project-widget";
+@import "../bootcamp/components/exercise-widget";
+@import "../bootcamp/components/concept-widget";
+
+@import "../bootcamp/pages/dashboard";
+@import "../bootcamp/pages/projects";
+@import "../bootcamp/pages/project";
+@import "../bootcamp/pages/concept";
+@import "../bootcamp/pages/level";
+
+@import "../bootcamp/pages/admin/base";
+@import "../bootcamp/pages/admin/exercises";
+
+@import "../bootcamp/exercises/draw.css";
+@import "../bootcamp/exercises/maze.css";
+@import "../bootcamp/exercises/wordle.css";
diff --git a/app/css/pages/bootcamp/admin/base.css b/app/css/pages/bootcamp/admin/base.css
new file mode 100644
index 0000000000..a2328cc1a8
--- /dev/null
+++ b/app/css/pages/bootcamp/admin/base.css
@@ -0,0 +1,8 @@
+.page-bootcamp-admin {
+ h1 {
+ @apply font-semibold text-22 mb-20;
+ }
+ h2 {
+ @apply font-semibold text-18 mb-8;
+ }
+}
diff --git a/app/css/pages/bootcamp/admin/exercises.css b/app/css/pages/bootcamp/admin/exercises.css
new file mode 100644
index 0000000000..907f118b35
--- /dev/null
+++ b/app/css/pages/bootcamp/admin/exercises.css
@@ -0,0 +1,76 @@
+#page-bootcamp-admin-exercises {
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-bottom: 20px;
+ thead {
+ background-color: #f5f5f5;
+ }
+ th {
+ text-align: left;
+ }
+ thead th {
+ border-bottom: 1px solid #ddd;
+ }
+ tbody {
+ td {
+ border-bottom: 1px solid #ddd;
+ a {
+ @apply text-blue-500 font-semibold;
+ }
+ }
+ }
+ th,
+ td {
+ padding: 10px;
+ border: 1px solid #eee;
+
+ &:first-child {
+ width: 1px;
+ }
+ &:nth-child(2) {
+ width: 250px;
+ }
+ &:nth-child(3) {
+ width: 20px;
+ @apply relative;
+ }
+
+ &:nth-child(4) {
+ }
+ &:nth-child(5),
+ &:nth-child(6) {
+ width: 1px;
+ @apply whitespace-nowrap;
+ @apply text-right;
+ }
+ }
+ .level {
+ @apply absolute inset-0 p-10;
+ @apply flex flex-col justify-center;
+ @apply font-semibold text-right;
+ &.positive {
+ @apply bg-green-100 text-green-700;
+ }
+ &.negative {
+ @apply bg-red-100 text-red-700;
+ }
+ }
+ .bubbles {
+ @apply flex flex-wrap gap-6;
+ }
+ .bubble {
+ @apply text-gray-600;
+ @apply border-1 border-gray-400;
+ @apply rounded-5;
+ @apply py-2 px-4;
+ @apply text-13 font-semibold;
+ &.locked {
+ @apply bg-red-100 text-red-500 border-red-300;
+ }
+ &.unlocked {
+ @apply bg-green-100 text-green-500 border-green-300;
+ }
+ }
+ }
+}
diff --git a/app/css/pages/bootcamp/concept.css b/app/css/pages/bootcamp/concept.css
new file mode 100644
index 0000000000..b5032e7f73
--- /dev/null
+++ b/app/css/pages/bootcamp/concept.css
@@ -0,0 +1,45 @@
+body.namespace-bootcamp.controller-concepts.action-show {
+ @apply bg-grad-basic;
+}
+#page-bootcamp-concept {
+ header {
+ @apply pb-16;
+ .details {
+ @apply flex items-center gap-20;
+ }
+ /* img {
+ @apply w-[128px] h-[128px];
+ } */
+ h1 {
+ @apply text-[39px] leading-140;
+ @apply font-bold;
+ }
+ }
+
+ .nav {
+ @apply flex flex-row items-center gap-10;
+ @apply mb-12;
+ a {
+ @apply font-semibold text-16 text-[blue];
+ }
+ }
+
+ .lhs {
+ @apply max-w-[800px];
+ }
+ .rhs {
+ @apply flex-grow w-full max-w-[380px] ml-auto;
+ }
+
+ ul.descendants {
+ @apply list-disc pl-6 mt-10;
+ li {
+ @apply text-16;
+ @apply mb-4;
+ a {
+ display: inline-flex;
+ flex-direction: column;
+ }
+ }
+ }
+}
diff --git a/app/css/pages/bootcamp/dashboard.css b/app/css/pages/bootcamp/dashboard.css
new file mode 100644
index 0000000000..6c63d865c7
--- /dev/null
+++ b/app/css/pages/bootcamp/dashboard.css
@@ -0,0 +1,44 @@
+#page-bootcamp-dashboard {
+ h2 {
+ @apply text-20 leading-150;
+ @apply font-semibold;
+ @apply mb-4;
+ }
+ p {
+ @apply text-16 leading-150;
+ @apply mb-8;
+ }
+
+ .exercise {
+ @apply py-12 px-16 rounded-8 border-1 block;
+ @apply shadow-base flex flex-col items-stretch;
+ @apply bg-white;
+
+ 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;
+
+ &.completed {
+ background: #e7fdf6;
+ border-color: #43b593;
+ color: #43b593;
+ }
+ }
+ p {
+ @apply text-15 leading-140;
+ }
+ }
+}
diff --git a/app/css/pages/bootcamp/level.css b/app/css/pages/bootcamp/level.css
new file mode 100644
index 0000000000..e0455e358e
--- /dev/null
+++ b/app/css/pages/bootcamp/level.css
@@ -0,0 +1,35 @@
+#page-bootcamp-level {
+ header {
+ @apply bg-grad-basic;
+ @apply pb-16;
+
+ .details {
+ @apply flex flex-col items-start;
+ .status {
+ @apply border-1 rounded-100;
+ @apply font-semibold leading-170;
+ @apply px-12 py-4;
+ @apply whitespace-nowrap;
+ &.completed {
+ @apply bg-green-100 text-green-700 border-green-400;
+ }
+ &.in-progress {
+ @apply bg-blue-100 text-blue-700 border-blue-400;
+ }
+ }
+ }
+ /* img {
+ @apply w-[128px] h-[128px];
+ } */
+ h1 {
+ @apply text-[39px] leading-140;
+ @apply font-bold;
+ }
+ }
+ .lhs {
+ /* @apply flex-grow; */
+ }
+ .rhs {
+ @apply flex-grow w-full max-w-[400px];
+ }
+}
diff --git a/app/css/pages/bootcamp/project.css b/app/css/pages/bootcamp/project.css
new file mode 100644
index 0000000000..1d413812ba
--- /dev/null
+++ b/app/css/pages/bootcamp/project.css
@@ -0,0 +1,44 @@
+body.namespace-bootcamp.controller-projects.action-show {
+ @apply bg-grad-basic;
+}
+#page-bootcamp-project {
+ header {
+ .project-bar {
+ @apply flex items-center gap-20 mb-20;
+ img {
+ @apply w-[128px] h-[128px];
+ }
+ h1 {
+ @apply text-[39px] leading-140;
+ @apply font-bold;
+ }
+ .tags {
+ @apply flex;
+ .tag {
+ @apply flex items-center;
+ @apply border-1 rounded-100;
+ @apply font-semibold leading-170;
+ @apply px-12 py-4;
+ @apply whitespace-nowrap;
+
+ &.completed {
+ background: #e7fdf6;
+ border-color: #43b593;
+ color: #43b593;
+ }
+ }
+ }
+ }
+ }
+ .lhs {
+ .introduction {
+ @apply max-w-[700px];
+ }
+ }
+ .rhs {
+ @apply w-[400px] flex-shrink-0;
+ @apply ml-auto;
+ }
+ .introduction {
+ }
+}
diff --git a/app/css/pages/bootcamp/projects.css b/app/css/pages/bootcamp/projects.css
new file mode 100644
index 0000000000..e3878b59c3
--- /dev/null
+++ b/app/css/pages/bootcamp/projects.css
@@ -0,0 +1,29 @@
+body.namespace-bootcamp.controller-projects.action-index {
+ @apply bg-grad-basic;
+}
+#page-bootcamp-projects {
+ header {
+ .title-bar {
+ @apply pb-20;
+ h1 {
+ @apply text-[39px] leading-140;
+ @apply font-bold;
+ @apply mb-4;
+ }
+ p {
+ @apply text-18 leading-140;
+ }
+ }
+ }
+ .lhs {
+ .introduction {
+ @apply max-w-[700px];
+ }
+ }
+ .rhs {
+ @apply w-[400px] flex-shrink-0;
+ @apply ml-auto;
+ }
+ .introduction {
+ }
+}
diff --git a/app/helpers/view_components/bootcamp/concept_widget.rb b/app/helpers/view_components/bootcamp/concept_widget.rb
new file mode 100644
index 0000000000..c42fee585c
--- /dev/null
+++ b/app/helpers/view_components/bootcamp/concept_widget.rb
@@ -0,0 +1,11 @@
+module ViewComponents
+ class Bootcamp::ConceptWidget < ViewComponent
+ initialize_with :concept
+
+ def to_s
+ render template: "components/bootcamp/concept_widget", locals: {
+ concept:
+ }
+ end
+ end
+end
diff --git a/app/helpers/view_components/bootcamp/exercise_widget.rb b/app/helpers/view_components/bootcamp/exercise_widget.rb
new file mode 100644
index 0000000000..25b9e85012
--- /dev/null
+++ b/app/helpers/view_components/bootcamp/exercise_widget.rb
@@ -0,0 +1,29 @@
+module ViewComponents
+ class Bootcamp::ExerciseWidget < ViewComponent
+ initialize_with :exercise, solution: nil, user_project: nil
+
+ def to_s
+ render template: "components/bootcamp/exercise_widget", locals: {
+ exercise:,
+ project: exercise.project,
+ solution:,
+ status:
+ }
+ end
+
+ private
+ def status
+ user_project.exercise_status(exercise, solution)
+ end
+
+ memoize
+ def solution
+ @solution || current_user.solutions.find_by(exercise:)
+ end
+
+ memoize
+ def user_project
+ @user_project || UserProject.for!(current_user, exercise.project)
+ end
+ end
+end
diff --git a/app/helpers/view_components/bootcamp/project_widget.rb b/app/helpers/view_components/bootcamp/project_widget.rb
new file mode 100644
index 0000000000..97f160569b
--- /dev/null
+++ b/app/helpers/view_components/bootcamp/project_widget.rb
@@ -0,0 +1,20 @@
+module ViewComponents
+ class Bootcamp::ProjectWidget < ViewComponent
+ initialize_with :project, user_project: nil
+
+ def to_s
+ render template: "components/bootcamp/project_widget", locals: {
+ project:,
+ user_project:,
+ status:
+ }
+ end
+
+ private
+ def status
+ return :locked unless user_project
+
+ user_project.status
+ end
+ end
+end
diff --git a/app/views/bootcamp/concepts/index.html.haml b/app/views/bootcamp/concepts/index.html.haml
new file mode 100644
index 0000000000..4ce55cebdc
--- /dev/null
+++ b/app/views/bootcamp/concepts/index.html.haml
@@ -0,0 +1,5 @@
+#page-bootcamp-concepts
+ - @concepts.each do |concept|
+ = link_to concept do
+ %h2.font-semibold= concept.title
+ %p= concept.description
diff --git a/app/views/bootcamp/concepts/show.html.haml b/app/views/bootcamp/concepts/show.html.haml
new file mode 100644
index 0000000000..e8e50323e2
--- /dev/null
+++ b/app/views/bootcamp/concepts/show.html.haml
@@ -0,0 +1,39 @@
+#page-bootcamp-concept
+ %header
+ .c-breadcrumbs
+ .lg-container.container
+ = link_to concepts_path do
+ = graphical_icon 'concepts'
+ .span All Concepts
+ .seperator /
+
+ - @concept.parents.each do |parent|
+ = link_to parent.title, concept_path(parent)
+ .seperator /
+
+ .title= @concept.title
+
+ .lg-container.details
+ %h1.font-semibold= @concept.title
+
+ .lg-container
+ .flex
+ .lhs.pr-40
+ .c-prose
+ = raw @concept.content_html
+
+ - if @concept.descendants.any?
+ %h3 Subconcepts
+ %ul.descendants
+ - @concept.descendants.each do |concept|
+ %li
+ = link_to concept do
+ .font-semibold.text-primary-blue.text-18= concept.title
+ .text-16.leading-150= concept.description
+ .rhs
+ .c-rhs-list
+ %h2.mb-12 Exercises
+ %p These exercises have been designed to help you practice this concept.
+ %ul
+ - @concept.exercises.each do |exercise|
+ %li= render ViewComponents::Bootcamp::ExerciseWidget.new(exercise)
diff --git a/app/views/bootcamp/dashboard/_exercise.html.haml b/app/views/bootcamp/dashboard/_exercise.html.haml
new file mode 100644
index 0000000000..0aa0045ede
--- /dev/null
+++ b/app/views/bootcamp/dashboard/_exercise.html.haml
@@ -0,0 +1,13 @@
+- solution ||= nil
+- project = exercise.project
+- status = solution ? solution.status : "available"
+= link_to [project, exercise], class: "exercise #{status}" do
+ .flex.items-center
+ .text
+ %h3
+ = project.title
+ %h4
+ Part #{exercise.idx + 1}.
+ = exercise.title
+ .tag= status.to_s.titleize
+ %p= exercise.description
diff --git a/app/views/bootcamp/dashboard/index.html.haml b/app/views/bootcamp/dashboard/index.html.haml
new file mode 100644
index 0000000000..5e13eb5612
--- /dev/null
+++ b/app/views/bootcamp/dashboard/index.html.haml
@@ -0,0 +1,15 @@
+#page-bootcamp-dashboard.pt-20
+ .lg-container
+ .grid.grid-cols-2
+ .lhs
+ - if @exercise
+ - if @solution
+ %h2 Continue Where You Left Off
+ %p You have an exercise in progress.
+ = render "exercise", exercise: @exercise, solution: @solution
+ - else
+ %h2 Start new exercise
+ %p You have a new exercise available to work on.
+ = render "exercise", exercise: @exercise
+ - else
+ You have no exercises available.
diff --git a/app/views/bootcamp/exercises/edit.html.haml b/app/views/bootcamp/exercises/edit.html.haml
new file mode 100644
index 0000000000..3d210038f7
--- /dev/null
+++ b/app/views/bootcamp/exercises/edit.html.haml
@@ -0,0 +1,21 @@
+- content_for :head do
+ %meta{ name: "turbo-visit-control", content: "reload" }
+= render ReactComponents::SolveExercisePage.new(@solution)
+
+-#
+ This is the entry point for the exercise editor...
+
+ .p-10
+ .meh
+ .font-semibold.mb-2 Receive a name, return a sentence.
+ %table.mb-2
+ %tr
+ %th Input
+ %td "Susan"
+ %tr
+ %th Expected Output
+ %td "One for Susan, one for me!"
+ %tr
+ %th Actual Output
+ %td "One for Frank, one for me!
+ %button Show scenario
diff --git a/app/views/bootcamp/exercises/index.html.haml b/app/views/bootcamp/exercises/index.html.haml
new file mode 100644
index 0000000000..b720e4dad6
--- /dev/null
+++ b/app/views/bootcamp/exercises/index.html.haml
@@ -0,0 +1,4 @@
+- @exercises.each do |exercise|
+ = link_to exercise do
+ %h2.font-semibold= exercise.title
+ %p= exercise.description
diff --git a/app/views/bootcamp/exercises/show.html.haml b/app/views/bootcamp/exercises/show.html.haml
new file mode 100644
index 0000000000..7b8181c336
--- /dev/null
+++ b/app/views/bootcamp/exercises/show.html.haml
@@ -0,0 +1,3 @@
+%h1.font-semibold= @exercise.title
+
+= raw @exercise.instructions_html
diff --git a/app/views/bootcamp/index.html.haml b/app/views/bootcamp/index.html.haml
index 9fbe709fac..b076a961cc 100644
--- a/app/views/bootcamp/index.html.haml
+++ b/app/views/bootcamp/index.html.haml
@@ -282,7 +282,7 @@
%strong Build a portfolio of projects.
Create projects to showcase to potential employers while practicing your skills.
.rhs
- .dates.h3-sideheading.z-10
+ .dates.h3-sideheading.z-overlay
= image_tag "bootcamp/calendar.svg"
April - June 2025
= image_tag "bootcamp/part-2.png", class: 'w-[350px] -mr-32 -mt-[60px]'
diff --git a/app/views/bootcamp/levels/index.html.haml b/app/views/bootcamp/levels/index.html.haml
new file mode 100644
index 0000000000..304dc0052a
--- /dev/null
+++ b/app/views/bootcamp/levels/index.html.haml
@@ -0,0 +1,13 @@
+#page-bootcamp-levels
+ .lg-container
+ %h1.font-semibold.text-20.mb-8 Levels
+ .grid.grid-cols-3.gap-8
+ - (1..27).each do |idx|
+ - level = @levels[idx]
+ - if level&.unlocked?
+ = link_to level, class: "block border-1 p-8 rounded-8" do
+ %h2.font-semibold.mb-4= level.title
+ -# %p= level.status
+ %p= level.description
+ - else
+ .block.border-1.p-8.rounded-8.bg-gray-300{ class: "h-[100px]" }
diff --git a/app/views/bootcamp/levels/show.html.haml b/app/views/bootcamp/levels/show.html.haml
new file mode 100644
index 0000000000..9601f61043
--- /dev/null
+++ b/app/views/bootcamp/levels/show.html.haml
@@ -0,0 +1,43 @@
+#page-bootcamp-level
+ %header
+ .c-breadcrumbs
+ .lg-container.container
+ = link_to levels_path do
+ = graphical_icon 'levels'
+ .span Levels
+ .seperator /
+ .title
+ Level 1:
+ = @level.title
+
+ .lg-container
+ %section.c-completed-bar
+ = image_tag 'completed-check-circle.svg', class: "check-mark-icon"
+ .text You've completed Level #{@level.idx}
+ .stat
+ All #{@level.exercises.count} exercises completed
+
+ .details
+ %h1
+ Level 1:
+ = @level.title
+
+ .lg-container
+ .flex
+ .lhs.pr-40
+ .content.c-prose
+ = raw @level.content_html
+ .rhs
+ .c-rhs-list
+ %h2 Concepts
+ %p Make sure you understand these concepts before moving on from this level.
+ %ul
+ - @level.concepts.non_apex.each do |concept|
+ %li= render ViewComponents::Bootcamp::ConceptWidget.new(concept)
+
+ .c-rhs-list
+ %h2 Exercises
+ %p Complete these exercises to complete this level.
+ %ul
+ - @level.exercises.each do |exercise|
+ %li= render ViewComponents::Bootcamp::ExerciseWidget.new(exercise)
diff --git a/app/views/bootcamp/projects/index.html.haml b/app/views/bootcamp/projects/index.html.haml
new file mode 100644
index 0000000000..d922598285
--- /dev/null
+++ b/app/views/bootcamp/projects/index.html.haml
@@ -0,0 +1,15 @@
+#page-bootcamp-projects
+ %header
+ .c-breadcrumbs
+ .lg-container.container
+ .title Projects
+
+ .lg-container
+ .title-bar
+ %h1.font-semibold.text-20.mb-8 Projects
+ %p More projects will unlock as you progress through the Bootcamp.
+
+ .lg-container
+ .grid.grid-cols-3.gap-8
+ - @user_projects.each do |user_project|
+ = render ViewComponents::Bootcamp::ProjectWidget.new(user_project.project, user_project:)
diff --git a/app/views/bootcamp/projects/show.html.haml b/app/views/bootcamp/projects/show.html.haml
new file mode 100644
index 0000000000..fad6c629f7
--- /dev/null
+++ b/app/views/bootcamp/projects/show.html.haml
@@ -0,0 +1,41 @@
+#page-bootcamp-project
+ %header
+ .c-breadcrumbs
+ .lg-container.container
+ = link_to bootcamp_projects_path do
+ = graphical_icon 'concepts'
+ .span Projects
+ .seperator /
+
+ .title= @project.title
+
+ .lg-container
+ .project-bar
+ = image_tag @project.icon_url, width: 64, height: 64
+ .flex.flex-col.gap-4
+ %h1= @project.title
+ .tags
+ .tag.completed Completed
+
+ -#
+ - if @project.concepts.any?
+ .flex
+ - @project.concepts.each do |concept|
+ = link_to concept.title, concept, class: 'bubble'
+
+ .lg-container
+ .flex.gap-48
+ .lhs
+ .introduction.c-prose
+ = raw @project.introduction_html
+
+ .rhs
+ - num_completed = @project.exercises.to_a.count { |exercise| @solutions[exercise.id]&.completed? }
+ - num_unlocked = @project.exercises.to_a.count(&:unlocked?)
+ %h2.font-semibold.text-22.mb-4 Exercises (#{num_completed} / #{num_unlocked})
+ %p.text-16.leading-150.mb-12
+ You have completed #{num_completed} of #{num_unlocked} unlockable exercises in this project.
+ New exercises will unlock as the Bootcamp progresses.
+ .exercises.flex.flex-col.gap-16
+ - @project.exercises.each do |exercise|
+ = render ViewComponents::Bootcamp::ExerciseWidget.new(exercise, solution: @solutions[exercise.id], user_project: @user_project)
diff --git a/app/views/components/bootcamp/concept_widget.html.haml b/app/views/components/bootcamp/concept_widget.html.haml
new file mode 100644
index 0000000000..bb7f0a0943
--- /dev/null
+++ b/app/views/components/bootcamp/concept_widget.html.haml
@@ -0,0 +1,3 @@
+= link_to concept, class: "c-concept-widget" do
+ .title= concept.title
+ .description= concept.description
diff --git a/app/views/components/bootcamp/exercise_widget.html.haml b/app/views/components/bootcamp/exercise_widget.html.haml
new file mode 100644
index 0000000000..42fa89de10
--- /dev/null
+++ b/app/views/components/bootcamp/exercise_widget.html.haml
@@ -0,0 +1,13 @@
+- tag_type = status == :locked ? :div : :a
+= content_tag tag_type, href: bootcamp_project_exercise_url(project, exercise), class: "c-exercise-widget #{status}" do
+ .flex.items-start
+ = image_tag exercise.icon_url
+ .flex-grow.flex.flex-col
+ .flex.items-start
+ .title
+ .project-title= project.title
+ .tag{ class: status.to_s }
+ .exercise-title
+ #{exercise.idx}.
+ = exercise.title
+ .description= exercise.description
diff --git a/app/views/components/bootcamp/project_widget.html.haml b/app/views/components/bootcamp/project_widget.html.haml
new file mode 100644
index 0000000000..4ab1bbfe77
--- /dev/null
+++ b/app/views/components/bootcamp/project_widget.html.haml
@@ -0,0 +1,8 @@
+= link_to project, class: "c-project-widget #{status}" do
+ .flex.items-start
+ = image_tag project.icon_url
+ .flex.flex-col.flex-grow
+ .flex.items-start
+ .title= project.title
+ .tag{ class: status.to_s }
+ .description= project.description
diff --git a/app/views/layouts/bootcamp-ui.haml b/app/views/layouts/bootcamp-ui.haml
new file mode 100644
index 0000000000..202588d1a8
--- /dev/null
+++ b/app/views/layouts/bootcamp-ui.haml
@@ -0,0 +1,42 @@
+!!!
+%html
+ %head
+ -# Fallback fonts first
+ %link{ rel: "preload", href: asset_path('poppins-v20-latin-regular.woff2'), as: "font", type: "font/woff2", crossorigin: :anonymous }
+ %link{ rel: "preload", href: asset_path('poppins-v20-latin-600.woff2'), as: "font", type: "font/woff2", crossorigin: :anonymous }
+
+ -# Then the main stylesheet
+ = stylesheet_link_tag "bootcamp-ui", "data-turbo-track": "reload"
+
+ -# Then other critical fonts
+ %link{ rel: "preload", href: asset_path('poppins-v20-latin-500.woff2'), as: "font", type: "font/woff2", crossorigin: :anonymous }
+ %link{ rel: "preload", href: asset_path('poppins-v20-latin-700.woff2'), as: "font", type: "font/woff2", crossorigin: :anonymous }
+ %link{ rel: "preload", href: asset_path('source-code-pro-v22-latin_latin-ext-regular.woff2'), as: "font", type: "font/woff2", crossorigin: :anonymous }
+
+ %meta{ content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type" }
+ %title= content_for(:title) || "Bootcamp"
+ %meta{ content: "width=device-width,initial-scale=1", name: "viewport" }
+ %meta{ content: "yes", name: "apple-mobile-web-app-capable" }
+ = csrf_meta_tags
+ = csp_meta_tag
+ = yield :head
+
+ %meta{ name: "turbo-cache-control", content: "no-cache" }
+ %meta{ name: "turbo-prefetch", content: "false" }
+ %meta{ name: "user-id", content: current_user&.id }
+
+ %link{ href: "/manifest.json", rel: "manifest" }
+ %link{ href: "/icon.png", rel: "icon", type: "image/png" }
+ %link{ href: "/icon.svg", rel: "icon", type: "image/svg+xml" }
+ %link{ href: "/icon.png", rel: "apple-touch-icon" }
+ = javascript_include_tag "application", "data-turbo-track": "reload", type: "module", crossorigin: :anonymous
+
+ %body{ class: body_class }
+ %header.c-site-header
+ .lg-container
+ .container
+ %img{ src: "https://assets.exercism.org/assets/bootcamp/exercism-face-light-2fc4ffad44f295d2e900ab2d2198d2280128dfcd.svg" }
+ .content
+ %strong.font-semibold Exercism
+ Bootcamp
+ = yield
diff --git a/bootcamp_content/concepts/arrays.md b/bootcamp_content/concepts/arrays.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bootcamp_content/concepts/conditionals.md b/bootcamp_content/concepts/conditionals.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bootcamp_content/concepts/config.json b/bootcamp_content/concepts/config.json
new file mode 100644
index 0000000000..b4467ce259
--- /dev/null
+++ b/bootcamp_content/concepts/config.json
@@ -0,0 +1,63 @@
+[
+ {
+ "slug": "data-types",
+ "title": "Data Types",
+ "description": "Data types are a fundamental concept in programming. They are used to represent different types of data.",
+ "level": 1,
+ "apex": true
+ },
+ {
+ "slug": "strings",
+ "parent": "data-types",
+ "title": "Strings",
+ "description": "Strings are a fundamental concept in programming. They are used to represent text, words, and characters.",
+ "level": 1,
+ "apex": true
+ },
+ {
+ "slug": "strings-using",
+ "parent": "strings",
+ "title": "Using Strings",
+ "description": "Learn how to use strings in your code.",
+ "level": 1
+ },
+ {
+ "slug": "strings-concatenation",
+ "parent": "strings",
+ "title": "Concatenating Strings",
+ "description": "",
+ "level": 2
+ },
+ {
+ "slug": "functions",
+ "title": "Functions",
+ "description": "",
+ "level": 2,
+ "apex": true
+ },
+ {
+ "slug": "functions-using",
+ "parent": "functions",
+ "title": "Using Functions",
+ "description": "Learn how to use basic functions.",
+ "level": 1
+ },
+ {
+ "slug": "conditionals",
+ "title": "Conditionals",
+ "description": "",
+ "level": 2
+ },
+ {
+ "slug": "loops-repeat",
+ "title": "Repeat Loop",
+ "description": "",
+ "level": 3
+ },
+ {
+ "slug": "arrays",
+ "title": "Arrays",
+ "description": "",
+ "level": 4
+ }
+]
diff --git a/bootcamp_content/concepts/data-types.md b/bootcamp_content/concepts/data-types.md
new file mode 100644
index 0000000000..a15f20e1ee
--- /dev/null
+++ b/bootcamp_content/concepts/data-types.md
@@ -0,0 +1,8 @@
+# Data Types
+
+"Data" means the different values that we use in coding.
+There are different types of data ("data types").
+The most basic are things like numbers, booleans (true/false) or strings.
+And then we have data types that contain multiple different values like lists and dictionaries.
+
+Use the links below to build a comprehensive understanding of lots of different data types.
diff --git a/bootcamp_content/concepts/functions-using.md b/bootcamp_content/concepts/functions-using.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bootcamp_content/concepts/functions.md b/bootcamp_content/concepts/functions.md
new file mode 100644
index 0000000000..ac78031d1b
--- /dev/null
+++ b/bootcamp_content/concepts/functions.md
@@ -0,0 +1,5 @@
+# Functions
+
+Functions are very important.
+
+Use the links below to build a comprehensive understanding of how they work.
diff --git a/bootcamp_content/concepts/loops-repeat.md b/bootcamp_content/concepts/loops-repeat.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bootcamp_content/concepts/strings-concatenation.md b/bootcamp_content/concepts/strings-concatenation.md
new file mode 100644
index 0000000000..83ca8fa1ab
--- /dev/null
+++ b/bootcamp_content/concepts/strings-concatenation.md
@@ -0,0 +1,113 @@
+# Let’s Look at How We Can Join Strings Using Functions, Methods, and Operators
+
+Now that you know what strings are, let’s take it up a notch and explore how you can **combine strings**—a process often called **string concatenation**. It’s like putting puzzle pieces together to create a bigger picture, and there are plenty of ways to do it in programming.
+
+Ready? Let’s dive into the tools and techniques!
+
+---
+
+## 1. **Joining Strings with Operators**
+
+The simplest and most common way to join strings is by using an **operator**—specifically, the **`+` operator**. Think of it as gluing two or more strings together.
+
+### Example:
+
+```python
+greeting = "Hello, "
+name = "Alex"
+message = greeting + name
+print(message) # Output: Hello, Alex
+```
+
+Here, the + operator combines "Hello, " with "Alex" to form a single string: "Hello, Alex".
+
+Why Use It?
+
+• It’s straightforward and intuitive.
+• Great for small-scale operations.
+
+But keep in mind: most programming languages don’t automatically add spaces between strings. So if you need a space, include it yourself, like in "Hello, ".
+
+## 2. Using String Methods
+
+Strings come with built-in methods (kind of like tools), and one of the most useful for joining strings is .join().
+
+### Example:
+
+```python
+words = ["Learning", "to", "join", "strings", "is", "fun!"]
+sentence = " ".join(words)
+print(sentence) # Output: Learning to join strings is fun!
+```
+
+Here’s how .join() works:
+
+• Start with the separator (in this case, a single space: " ").
+• Use .join() to combine all the strings in the list (words) with the separator between them.
+
+Why Use .join()?
+
+• Perfect for combining multiple strings in a list or array.
+• Allows you to control how strings are joined (e.g., with commas, spaces, or even dashes).
+
+## 3. String Formatting and f-Strings
+
+Sometimes, you don’t just want to join strings—you want to insert variables or values into them. This is where string formatting shines. There are a few ways to do this, depending on the programming language, but let’s focus on f-strings (Python’s modern and super-convenient approach).
+
+### Example:
+
+```python
+name = "Alex"
+age = 25
+message = f"My name is {name} and I am {age} years old."
+print(message) # Output: My name is Alex and I am 25 years old.
+```
+
+Here, the f before the string lets you embed variables directly inside {}. It’s quick, readable, and avoids a lot of extra concatenation work.
+
+Why Use It?
+
+• Ideal for creating strings with dynamic content.
+• Clean and easy to read.
+
+## 4. Using Functions
+
+If you find yourself joining strings repeatedly in the same way, why not write a function to handle it? Functions let you encapsulate logic and reuse it whenever you need.
+
+### Example:
+
+```python
+def create_greeting(first_name, last_name):
+ return f"Hello, {first_name} {last_name}!"
+
+greeting = create_greeting("Alex", "Smith")
+print(greeting) # Output: Hello, Alex Smith!
+```
+
+By using a function, you can organize your code and make it reusable. Plus, it keeps things neat when working on larger projects.
+
+## 5. Combining Techniques
+
+You’re not limited to just one method—you can mix and match techniques depending on the situation.
+
+### Example:
+
+```python
+names = ["Alice", "Bob", "Charlie"]
+greeting = "Welcome, " + ", ".join(names) + "!"
+print(greeting) # Output: Welcome, Alice, Bob, Charlie!
+```
+
+Here, we combined the + operator and .join() to create a dynamic and friendly message.
+
+Key Tips for Joining Strings
+
+• Pay attention to spaces: Strings don’t magically add spaces, so you’ll need to include them manually or use methods like .join() thoughtfully.
+• Use the right tool for the job: For simple tasks, + works great. For more complex scenarios, consider .join(), formatting, or functions.
+• Readability matters: Clear, easy-to-read code is always better, especially when working with others.
+
+## Wrapping Up
+
+Joining strings might seem simple, but it’s a fundamental skill that unlocks countless possibilities in programming. Whether you’re displaying a message, building a URL, or generating dynamic content, knowing how to combine strings effectively is key.
+
+Now it’s your turn! Try experimenting with the methods we’ve covered and see how creative you can get. Who knew strings could be so fun?
diff --git a/bootcamp_content/concepts/strings-using.md b/bootcamp_content/concepts/strings-using.md
new file mode 100644
index 0000000000..64c4a116be
--- /dev/null
+++ b/bootcamp_content/concepts/strings-using.md
@@ -0,0 +1,46 @@
+# Introduction to Strings
+
+Let’s talk about one of the most fundamental concepts in programming: **strings**. And no, we’re not talking about the strings on a guitar or a piece of thread! In the world of coding, strings are all about text—words, sentences, symbols, even a single character.
+
+## What is a String?
+
+Imagine you’re writing a sentence, like _"Hello, world!"_. That entire piece of text is a string in programming. Essentially, a string is a sequence of characters grouped together. Characters could be letters, numbers, punctuation marks, or even spaces.
+
+Here’s the best part: strings are everywhere. They’re the text in a website button that says “Click me.” They’re the labels on a shopping app that show product names. They’re even the messages you send to your friends. If it’s text, it’s probably a string in disguise.
+
+## Strings in Everyday Life
+
+Think of strings as the digital equivalent of sticky notes. You can write anything on them: a name, a password, or even a silly joke. Once you’ve got a string, you can save it, modify it, and use it in all sorts of creative ways.
+
+For example:
+
+- A string might store your name: `"Alex"`
+- It could hold a question: `"How are you?"`
+- Or even act as a secret code: `"xyz123"`
+
+Strings help computers work with text just like you do, but they need clear instructions. That’s where you—the programmer—come in.
+
+## Fun Facts About Strings
+
+1. **Strings are “quoted.”** In most programming languages, strings are wrapped in quotes so the computer knows where the text begins and ends. For example:
+
+ - `"I love coding!"` (double quotes)
+ - `'Single quotes work too!'`
+
+2. **Strings can be long or short.** A string could be one letter, like `"A"`, or an entire book’s worth of text!
+
+3. **You can manipulate strings.** Want to shout your message? You can convert `"hello"` to `"HELLO"`. Need to count how many letters are in a word? Strings can help with that too.
+
+4. **They’re versatile.** Strings can include emojis, special characters, and even numbers, like `"🎉 Party starts at 7pm!"`.
+
+## Why Strings Matter
+
+Strings make programs feel alive. Imagine a calculator without any labels, or a game without dialogue—it’d be impossible to use! Strings make apps and websites readable and user-friendly.
+
+As a beginner in programming, working with strings is one of the first ways you’ll interact with your code. You’ll practice printing text, combining strings, and even creating fun outputs like `"Hello, [Your Name]!"`.
+
+## Let’s Wrap This Up
+
+Strings might sound simple, but they’re incredibly powerful. They’re how you give your program a voice—whether it’s to say “Welcome!” or “Error: Something went wrong.” As you dive deeper into coding, you’ll see just how versatile and essential strings really are.
+
+Now, go ahead and say it: _"I’m ready to learn strings!"_
diff --git a/bootcamp_content/concepts/strings.md b/bootcamp_content/concepts/strings.md
new file mode 100644
index 0000000000..6403103cb6
--- /dev/null
+++ b/bootcamp_content/concepts/strings.md
@@ -0,0 +1,5 @@
+# Strings
+
+Strings are one of the most common and fundamental data types.
+
+Use the links below to build a comprehensive understanding of how they work.
diff --git a/bootcamp_content/exploration/maze/index.html b/bootcamp_content/exploration/maze/index.html
new file mode 100644
index 0000000000..dd536e4823
--- /dev/null
+++ b/bootcamp_content/exploration/maze/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ Maze Game
+
+
+
+
+
+
+
+
+
diff --git a/bootcamp_content/exploration/maze/script.js b/bootcamp_content/exploration/maze/script.js
new file mode 100644
index 0000000000..c803d34c33
--- /dev/null
+++ b/bootcamp_content/exploration/maze/script.js
@@ -0,0 +1,164 @@
+const maze = document.getElementById('maze')
+
+// Maze configuration: 0 = open, 1 = blocked
+let mazeLayout
+let characterPosition
+let direction
+
+function createMaze() {
+ maze.innerHTML = ''
+ maze.style.setProperty('--size', mazeLayout.length)
+
+ for (let y = 0; y < mazeLayout.length; y++) {
+ for (let x = 0; x < mazeLayout[y].length; x++) {
+ const cell = document.createElement('div')
+ cell.classList.add('cell')
+ if (mazeLayout[y][x] === 1) {
+ cell.classList.add('blocked')
+ }
+ if (x === characterPosition.x && y === characterPosition.y) {
+ const character = document.createElement('div')
+ character.classList.add('character')
+ character.classList.add('direction-' + direction)
+ cell.appendChild(character)
+ }
+ maze.appendChild(cell)
+ }
+ }
+}
+
+function moveCharacter(dx, dy) {
+ const newX = characterPosition.x + dx
+ const newY = characterPosition.y + dy
+ if (
+ newX >= 0 &&
+ newX < mazeLayout[0].length &&
+ newY >= 0 &&
+ newY < mazeLayout.length &&
+ mazeLayout[newY][newX] === 0
+ ) {
+ characterPosition.x = newX
+ characterPosition.y = newY
+ createMaze()
+ }
+}
+
+document.addEventListener('keydown', (e) => {
+ switch (e.key) {
+ case 'ArrowUp':
+ moveCharacter(0, -1)
+ break
+ case 'ArrowDown':
+ moveCharacter(0, 1)
+ break
+ case 'ArrowLeft':
+ moveCharacter(-1, 0)
+ break
+ case 'ArrowRight':
+ moveCharacter(1, 0)
+ break
+ }
+})
+function move() {
+ if (direction == 'up') {
+ moveCharacter(0, -1)
+ } else if (direction == 'down') {
+ moveCharacter(0, 1)
+ } else if (direction == 'right') {
+ moveCharacter(1, 0)
+ } else if (direction == 'left') {
+ moveCharacter(-1, 0)
+ }
+}
+
+function turn_left() {
+ if (direction == 'up') {
+ direction = 'left'
+ } else if (direction == 'down') {
+ direction = 'right'
+ } else if (direction == 'right') {
+ direction = 'up'
+ } else if (direction == 'left') {
+ direction = 'down'
+ }
+ createMaze()
+}
+
+function turn_right() {
+ if (direction == 'up') {
+ direction = 'right'
+ } else if (direction == 'down') {
+ direction = 'left'
+ } else if (direction == 'right') {
+ direction = 'down'
+ } else if (direction == 'left') {
+ direction = 'up'
+ }
+ createMaze()
+}
+
+instructions = [
+ move,
+ move,
+ turn_left,
+ move,
+ move,
+ turn_left,
+ move,
+ move,
+ turn_right,
+ move,
+]
+/*for (let i = 0; i < instructions.length; i++) {
+ setTimeout(instructions[i], i * 100)
+} */
+
+function gameLoop() {
+ const instruction = instructions.shift()
+ if (!instruction) {
+ return
+ }
+ instruction()
+ setTimeout(gameLoop, 100)
+}
+
+function setupDefault() {
+ mazeLayout = [
+ [0, 1, 0, 0, 0, 1, 0, 0, 0, 0],
+ [0, 1, 0, 1, 0, 1, 1, 1, 0, 1],
+ [0, 0, 0, 1, 0, 0, 0, 0, 0, 1],
+ [0, 1, 1, 1, 0, 1, 1, 1, 0, 1],
+ [0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
+ [1, 1, 1, 1, 0, 1, 1, 1, 1, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 1, 1, 1, 1, 1, 1, 1, 0, 1],
+ [0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
+ [1, 1, 1, 1, 1, 1, 0, 1, 0, 0],
+ ]
+
+ characterPosition = { x: 0, y: 0 }
+ direction = 'down'
+}
+
+function setupGrid(layout) {
+ mazeLayout = layout
+}
+function setupDirection(dir) {
+ direction = dir
+}
+function setupPosition(x, y) {
+ characterPosition = { x: x, y: y }
+}
+
+// This is what we want to call for an exercise where we're testing
+// the code runs as expected.
+setupDefault()
+createMaze()
+gameLoop()
+
+// setupGrid([[0,0,0],[0,0,0],[0,0,0]]);
+// setupDirection("up")
+// setupPosition(1,1)
+
+// move()
+console.log(characterPosition)
diff --git a/bootcamp_content/exploration/maze/style.css b/bootcamp_content/exploration/maze/style.css
new file mode 100644
index 0000000000..f0ab60ce46
--- /dev/null
+++ b/bootcamp_content/exploration/maze/style.css
@@ -0,0 +1,70 @@
+body {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ background-color: #f4f4f4;
+ margin: 0;
+
+ --cellWidth: 20px;
+}
+
+#maze {
+ display: grid;
+ grid-template-columns: repeat(var(--size), var(--cellWidth));
+ grid-template-rows: repeat(var(--size), var(--cellWidth));
+ gap: 0px;
+}
+
+.cell {
+ width: var(--cellWidth);
+ height: var(--cellWidth);
+ background-color: white;
+ border: 0.5px solid #000;
+}
+
+.cell.blocked {
+ background-color: red;
+}
+
+.character {
+ width: calc(var(--cellWidth) / 2);
+ height: calc(var(--cellWidth) / 2);
+ background-color: lightblue;
+ border: 1px solid green;
+ border-radius: 50%;
+ position: relative;
+ left: calc(var(--cellWidth) / 5);
+ top: calc(var(--cellWidth) / 5);
+}
+.direction-up {
+ transform: rotate(0deg);
+}
+.direction-right {
+ transform: rotate(-90deg);
+}
+.direction-down {
+ transform: rotate(180deg);
+}
+.direction-right {
+ transform: rotate(90deg);
+}
+
+.character::before,
+.character::after {
+ content: "";
+ position: absolute;
+ width: calc(var(--cellWidth) / 4);
+ height: calc(var(--cellWidth) / 4);
+ background-color: green;
+ border-radius: 50%;
+ bottom: 8px;
+}
+
+.character::before {
+ left: 6px;
+}
+
+.character::after {
+ right: 6px;
+}
diff --git a/bootcamp_content/levels/1.md b/bootcamp_content/levels/1.md
new file mode 100644
index 0000000000..6969ca68ef
--- /dev/null
+++ b/bootcamp_content/levels/1.md
@@ -0,0 +1,34 @@
+# Level 1
+
+## About Levels
+
+Welcome to Level One!
+
+Levels are the core of your learning experience on this Bootcamp.
+
+Each week we'll unlock a new level, with new concepts to learn and exercises to solve. Our teaching sessions appear here for you to watch live, and then stay for you to watch back on demand.
+
+I strongly recommend finishing all of the exercises on a level before moving onto the next one.
+However, if you get really stuck on one exercise, you might still like to continue on in the next level while waiting for help.
+If you feel like a level is too easy, then it shouldn't take you any time at all to solve the exercises within it, so use that for practice anyway.
+
+### How to Complete a Level
+
+To complete a level, you need to solve all the exercises on the right hand side.
+
+## Your first steps
+
+This week largely revolves around drawing using code.
+The objective is for you to be able to draw fun things without feeling like you've having to think too hard about getting the right syntax (ie what to write where).
+Your drawing ability should be what limits you - not your coding ability!
+
+We have students on this Bootcamp with a wide range of experience-levels, from total beginners to people in full-time dev jobs.
+
+For those just starting out, this first week will contain lots of new information and thinking for you to absorb.
+The most important thing is to take your time and build confidence.
+
+For those who are experienced, these first few weeks will feel familiar and probably quite easy.
+But I strongly advise you to still watch the videos and explore the mental models I'm building, as they will be different from the way you've approached things before.
+
+Most of all, have fun!
+And we'll dig into some more interesting things at Level 2!
diff --git a/bootcamp_content/levels/2.md b/bootcamp_content/levels/2.md
new file mode 100644
index 0000000000..6a13d568e1
--- /dev/null
+++ b/bootcamp_content/levels/2.md
@@ -0,0 +1,7 @@
+# Level 2
+
+Welcome to Level 2!
+
+Last week we touched on the absolute basics of coding - using functions. Hopefully you had lots of fun testing your drawing abilities, and getting creative.
+
+In Level 2, we're going to build on what we learned last week, and look at when code moves beyond a linear list of commands, exploring loops, conditionals, variables, and writing our own functions. This is the most full content-rich week of the whole Bootcamp, and we'll cover quite a lot of ground, so take it slowly and make sure you understand each part thoroughly.
diff --git a/bootcamp_content/levels/3.md b/bootcamp_content/levels/3.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bootcamp_content/levels/4.md b/bootcamp_content/levels/4.md
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bootcamp_content/levels/config.json b/bootcamp_content/levels/config.json
new file mode 100644
index 0000000000..32912b63a9
--- /dev/null
+++ b/bootcamp_content/levels/config.json
@@ -0,0 +1,18 @@
+[
+ {
+ "title": "Writing your first code",
+ "description": "Learn how to write your first lines of code"
+ },
+ {
+ "title": "Conditionals and Returns",
+ "description": ""
+ },
+ {
+ "title": "Loops",
+ "description": ""
+ },
+ {
+ "title": "Arrays",
+ "description": ""
+ }
+]
diff --git a/bootcamp_content/projects/drawing/config.json b/bootcamp_content/projects/drawing/config.json
new file mode 100644
index 0000000000..cd1f5b5254
--- /dev/null
+++ b/bootcamp_content/projects/drawing/config.json
@@ -0,0 +1,6 @@
+{
+ "slug": "drawing",
+ "title": "Drawing",
+ "description": "The world of drawing",
+ "exercises": ["loops"]
+}
diff --git a/bootcamp_content/projects/drawing/exercises/loops/config.json b/bootcamp_content/projects/drawing/exercises/loops/config.json
new file mode 100644
index 0000000000..7e8180e10b
--- /dev/null
+++ b/bootcamp_content/projects/drawing/exercises/loops/config.json
@@ -0,0 +1,49 @@
+{
+ "title": "Loops",
+ "description": "Create 5 rectangles",
+ "project_type": "draw",
+ "level": 3,
+ "concepts": ["loops-repeat"],
+ "tests_type": "state",
+ "tasks": [
+ {
+ "name": "Draw 4 rectangles",
+ "tests": [
+ {
+ "slug": "4-rectangles-20-20",
+ "name": "Draw 4 rectangles",
+ "function": "main",
+ "checks": [
+ {
+ "name": "numElements()",
+ "value": 4,
+ "descriptionHtml": "Expected 4 rectangles to be drawn, but only got %actual%."
+ },
+ {
+ "name": "getRectAt(20,20,20,20)",
+ "matcher": "toExist",
+ "descriptionHtml": "No rectangle at (20,20,20,20)"
+ }
+ ]
+ },
+ {
+ "slug": "4-rectangles-40-40",
+ "name": "Draw 4 rectangles",
+ "description": "Draw 4 rectangles at (40,40,20,20)",
+ "function": "main",
+ "checks": [
+ {
+ "name": "numElements()",
+ "value": 4
+ },
+ {
+ "name": "getRectAt(40,40,20,20)",
+ "matcher": "toExist",
+ "descriptionHtml": "We couldn't find a rectangle at (40,40,20,20). Check that you've got the co-ordinates, width and height all correct and try again!"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/bootcamp_content/projects/drawing/exercises/loops/example.jk b/bootcamp_content/projects/drawing/exercises/loops/example.jk
new file mode 100644
index 0000000000..802eb8f278
--- /dev/null
+++ b/bootcamp_content/projects/drawing/exercises/loops/example.jk
@@ -0,0 +1,7 @@
+function two_fer with name do
+ if name is "" do
+ return "One for you, one for me."
+ else do
+ return "One for " + name + ", one for me."
+ end
+end
diff --git a/bootcamp_content/projects/drawing/exercises/loops/introduction.md b/bootcamp_content/projects/drawing/exercises/loops/introduction.md
new file mode 100644
index 0000000000..a8c9ace4f3
--- /dev/null
+++ b/bootcamp_content/projects/drawing/exercises/loops/introduction.md
@@ -0,0 +1,15 @@
+# TwoFer Part 1
+
+Two Fer is a classic Exercism exercise.
+Through it, we'll explore a few ideas around using _Strings_ and _Conditionals_.
+
+In some English accents, when you say "two for" quickly, it sounds like "two fer". Two-for-one is a way of saying that if you buy one, you also get one for free. So the phrase "two-fer" often implies a two-for-one offer.
+
+Imagine a bakery that has a holiday offer where you can buy two cookies for the price of one ("two-fer one!"). You take the offer and (very generously) decide to give the extra cookie to someone else in the queue.
+
+As you give them the cookie, you one of two things.
+
+- If you know their name, you say: "One for <name>, one for me."
+- If you don't know their name, you say: "One for you, one for me."
+
+For example, you might say "One for Jeremy, one for me" if you know Jeremy's name.
diff --git a/bootcamp_content/projects/drawing/exercises/loops/stub.jk b/bootcamp_content/projects/drawing/exercises/loops/stub.jk
new file mode 100644
index 0000000000..20a0e40b8a
--- /dev/null
+++ b/bootcamp_content/projects/drawing/exercises/loops/stub.jk
@@ -0,0 +1,4 @@
+rect(rand(0,10),10,10,10)
+rect(rand(0,80),10,10,10)
+rect(rand(0,80),10,10,10)
+rect(rand(0,80),10,10,10)
\ No newline at end of file
diff --git a/bootcamp_content/projects/drawing/exercises/loops/task-1.md b/bootcamp_content/projects/drawing/exercises/loops/task-1.md
new file mode 100644
index 0000000000..1b03934503
--- /dev/null
+++ b/bootcamp_content/projects/drawing/exercises/loops/task-1.md
@@ -0,0 +1,5 @@
+We've given you a function skeleton that takes one input - `name`.
+
+It will either be an empty string (`""`) or it will be someone's name (`"Jeremy"`).
+
+Let's start off by just considering that empty version and always returning our default version `"One for me, one for you."`.
diff --git a/bootcamp_content/projects/drawing/exercises/loops/task-2.md b/bootcamp_content/projects/drawing/exercises/loops/task-2.md
new file mode 100644
index 0000000000..2a597efd6a
--- /dev/null
+++ b/bootcamp_content/projects/drawing/exercises/loops/task-2.md
@@ -0,0 +1,7 @@
+Nice work!
+
+Now we need to handle the situation where we **do** know the person's name.
+
+Sometime's the name will be empty (`""`) in which case we want to continue returning `"One for you, one for me."`, but other times `name` will contain a name, in which case we want to include it in the return value (e.g. `"One for Jeremy, one for me."`).
+
+Remember, you can join multiple strings together using the `join_strings(...)` function.
diff --git a/bootcamp_content/projects/drawing/introduction.md b/bootcamp_content/projects/drawing/introduction.md
new file mode 100644
index 0000000000..f3cf6682e4
--- /dev/null
+++ b/bootcamp_content/projects/drawing/introduction.md
@@ -0,0 +1,25 @@
+# Two Fer
+
+## Overview
+
+Two Fer is a classic Exercism exercise.
+Through it, we'll explore a few ideas around using _Strings_ and _Conditionals_.
+
+## Introduction
+
+In some English accents, when you say "two for" quickly, it sounds like "two fer". Two-for-one is a way of saying that if you buy one, you also get one for free. So the phrase "two-fer" often implies a two-for-one offer.
+
+Imagine a bakery that has a holiday offer where you can buy two cookies for the price of one ("two-fer one!"). You take the offer and (very generously) decide to give the extra cookie to someone else in the queue.
+
+As you give them the cookie, if you know their name (e.g. they're called "Jeremy"), you say:
+
+```text
+"One for Jeremy, one for me."
+```
+
+If you don't know their name (even more generous of you!), you say:
+"One for you, one for me."
+
+```
+
+```
diff --git a/bootcamp_content/projects/drawing/tree.jiki b/bootcamp_content/projects/drawing/tree.jiki
new file mode 100644
index 0000000000..342ca5fe90
--- /dev/null
+++ b/bootcamp_content/projects/drawing/tree.jiki
@@ -0,0 +1,14 @@
+
+change_fill_color("#aa95aa")
+rect(13,28,13,50)
+change_fill_color("#33ff33")
+change_pen_color("#33ff33")
+circle(4,5,8)
+circle(17,5,8)
+circle(10,10,10)
+circle(19,19,8)
+circle(4,16,8)
+
+change_pen_color("#885588")
+change_fill_color("#885588")
+circle(15.5,40,3.8)
\ No newline at end of file
diff --git a/bootcamp_content/projects/maze/config.json b/bootcamp_content/projects/maze/config.json
new file mode 100644
index 0000000000..28339524da
--- /dev/null
+++ b/bootcamp_content/projects/maze/config.json
@@ -0,0 +1,6 @@
+{
+ "slug": "maze",
+ "title": "Maze",
+ "description": "Use then implement a basic maze",
+ "exercises": ["manual-solve", "automated-solve"]
+}
diff --git a/bootcamp_content/projects/maze/exercises/automated-solve/config.json b/bootcamp_content/projects/maze/exercises/automated-solve/config.json
new file mode 100644
index 0000000000..b3c595d41a
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/automated-solve/config.json
@@ -0,0 +1,216 @@
+{
+ "title": "Programatically solve a maze",
+ "description": "Programatically solve a maze",
+ "level": 2,
+ "concepts": ["Conditionals", "loops-repeat"],
+ "project_type": "maze",
+ "tests_type": "state",
+ "avaliableFunctions": ["move", "turnLeft", "turnRight", "canMove"],
+ "tasks": [
+ {
+ "name": "A straight path",
+ "tests": [
+ {
+ "slug": "maze-1",
+ "name": "Guide person to the end of the maze",
+ "setupFunctions": [
+ [
+ "setupGrid",
+ [
+ [
+ [1, 1, 1, 1, 2, 1, 1],
+ [1, 1, 1, 1, 0, 1, 1],
+ [1, 1, 1, 1, 0, 1, 1],
+ [1, 1, 1, 1, 0, 1, 1],
+ [1, 1, 1, 1, 0, 1, 1],
+ [1, 1, 1, 1, 0, 1, 1],
+ [1, 1, 1, 1, 3, 1, 1]
+ ]
+ ]
+ ],
+ ["setupDirection", ["down"]],
+ ["setupPosition", [4, 0]]
+ ],
+ "checks": [
+ {
+ "name": "position",
+ "value": [4, 8]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Turn left if you can't move straight",
+ "tests": [
+ {
+ "slug": "left-turn",
+ "name": "A single left turn",
+ "setupFunctions": [
+ [
+ "setupGrid",
+ [
+ [
+ [2, 1, 1, 1, 1, 1, 1, 1, 1],
+ [0, 1, 1, 1, 1, 1, 1, 1, 1],
+ [0, 1, 1, 1, 1, 1, 1, 1, 1],
+ [0, 1, 1, 1, 1, 1, 1, 1, 1],
+ [0, 1, 1, 1, 1, 1, 1, 1, 1],
+ [0, 0, 0, 0, 0, 0, 0, 0, 3],
+ [1, 1, 1, 1, 1, 1, 1, 1, 1],
+ [1, 1, 1, 1, 1, 1, 1, 1, 1],
+ [1, 1, 1, 1, 1, 1, 1, 1, 1]
+ ]
+ ]
+ ],
+ ["setupDirection", ["down"]],
+ ["setupPosition", [0, 1]]
+ ],
+ "checks": [
+ {
+ "name": "position",
+ "value": [8, 5]
+ },
+ {
+ "name": "direction",
+ "value": "right"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Turn right if you can't move straight or left",
+ "tests": [
+ {
+ "slug": "right-turn",
+ "name": "A single right turn",
+ "setupFunctions": [
+ [
+ "setupGrid",
+ [
+ [
+ [1, 1, 1, 1, 1, 1, 1, 1, 2],
+ [1, 1, 1, 1, 1, 1, 1, 1, 0],
+ [1, 1, 1, 1, 1, 1, 1, 1, 0],
+ [1, 1, 1, 1, 1, 1, 1, 1, 0],
+ [1, 1, 1, 1, 1, 1, 1, 1, 0],
+ [3, 0, 0, 0, 0, 0, 0, 0, 0],
+ [1, 1, 1, 1, 1, 1, 1, 1, 1],
+ [1, 1, 1, 1, 1, 1, 1, 1, 1],
+ [1, 1, 1, 1, 1, 1, 1, 1, 1]
+ ]
+ ]
+ ],
+ ["setupDirection", ["down"]],
+ ["setupPosition", [8, 0]]
+ ],
+ "checks": [
+ {
+ "name": "position",
+ "value": [0, 5]
+ }
+ ]
+ },
+ {
+ "slug": "forks",
+ "name": "Choose left if you can, otherwise choose right",
+ "setupFunctions": [
+ [
+ "setupGrid",
+ [
+ [
+ [2, 1, 1, 1, 1, 1, 1, 1, 1],
+ [0, 1, 1, 1, 1, 1, 1, 1, 1],
+ [0, 1, 1, 1, 0, 0, 0, 0, 3],
+ [0, 1, 1, 1, 0, 1, 1, 1, 1],
+ [0, 1, 1, 1, 0, 1, 1, 1, 1],
+ [0, 0, 0, 0, 0, 1, 1, 1, 1],
+ [1, 1, 1, 1, 4, 1, 1, 1, 1],
+ [1, 1, 1, 1, 0, 1, 1, 1, 1],
+ [1, 1, 1, 1, 0, 1, 1, 1, 1]
+ ]
+ ]
+ ],
+ ["setupDirection", ["down"]],
+ ["setupPosition", [0, 0]]
+ ],
+ "function": "solve_maze",
+ "checks": [
+ {
+ "name": "position",
+ "value": [8, 2]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Turn around if needed",
+ "tests": [
+ {
+ "slug": "turn-around",
+ "name": "Turn around if you can't move straight, left, or right",
+ "setupFunctions": [
+ [
+ "setupGrid",
+ [
+ [
+ [1, 1, 1, 2, 1, 1, 1, 1, 1],
+ [1, 1, 1, 0, 1, 1, 1, 1, 1],
+ [1, 1, 1, 0, 1, 1, 1, 1, 1],
+ [1, 1, 1, 0, 1, 1, 0, 1, 1],
+ [1, 1, 1, 0, 1, 1, 0, 1, 1],
+ [1, 1, 4, 0, 0, 0, 0, 0, 1],
+ [1, 1, 1, 0, 1, 1, 1, 1, 1],
+ [3, 0, 0, 0, 1, 1, 1, 1, 1],
+ [1, 1, 1, 0, 1, 1, 1, 1, 1]
+ ]
+ ]
+ ],
+ ["setupDirection", ["down"]],
+ ["setupPosition", [3, 0]]
+ ],
+ "function": "solve_maze",
+ "checks": [
+ {
+ "name": "position",
+ "value": [0, 7]
+ }
+ ]
+ },
+ {
+ "slug": "forks",
+ "name": "Choose left if you can, otherwise choose right",
+ "setupFunctions": [
+ [
+ "setupGrid",
+ [
+ [
+ [2, 1, 1, 1, 1, 1, 1, 1, 1],
+ [0, 1, 1, 1, 1, 1, 1, 1, 1],
+ [0, 1, 1, 1, 0, 0, 0, 0, 1],
+ [0, 1, 1, 1, 0, 1, 1, 1, 1],
+ [0, 1, 1, 1, 0, 1, 1, 1, 1],
+ [0, 0, 0, 0, 0, 1, 1, 1, 1],
+ [1, 1, 1, 1, 0, 1, 1, 1, 1],
+ [1, 1, 1, 1, 0, 1, 1, 1, 1],
+ [1, 1, 1, 1, 3, 1, 1, 1, 1]
+ ]
+ ]
+ ],
+ ["setupDirection", ["down"]],
+ ["setupPosition", [0, 0]]
+ ],
+ "function": "solve_maze",
+ "checks": [
+ {
+ "name": "position",
+ "value": [8, 2]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/bootcamp_content/projects/maze/exercises/automated-solve/example.jk b/bootcamp_content/projects/maze/exercises/automated-solve/example.jk
new file mode 100644
index 0000000000..c22af95a40
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/automated-solve/example.jk
@@ -0,0 +1,14 @@
+repeat_until_game_over do
+ if can_turn_left() is true do
+ turn_left()
+ move()
+ else if can_move() is true do
+ move()
+ else if can_turn_right() is true do
+ turn_right()
+ move()
+ else do
+ turn_left()
+ turn_left()
+ end
+end
\ No newline at end of file
diff --git a/bootcamp_content/projects/maze/exercises/automated-solve/introduction.md b/bootcamp_content/projects/maze/exercises/automated-solve/introduction.md
new file mode 100644
index 0000000000..bf45496945
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/automated-solve/introduction.md
@@ -0,0 +1,11 @@
+# Solve the Maze
+
+Your task is to solve the following maze.
+
+You have three functions you can use:
+
+- `move()` which moves the character one step forward
+- `turn_left()` turns the character left (relative to the direction they're currently facing)
+- `turn_right()` turns the character right (relative to the direction they're currently facing)
+
+Remember to use one function per line.
diff --git a/bootcamp_content/projects/maze/exercises/automated-solve/stub.jk b/bootcamp_content/projects/maze/exercises/automated-solve/stub.jk
new file mode 100644
index 0000000000..27cc684a7b
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/automated-solve/stub.jk
@@ -0,0 +1,6 @@
+// You can use move(), turn_left() and turn_right()
+// Use the functions in the order you want your character
+// to use them to solve them maze. We'll start you off by
+// moving the character one step forward.
+
+move()
\ No newline at end of file
diff --git a/bootcamp_content/projects/maze/exercises/automated-solve/task-1.md b/bootcamp_content/projects/maze/exercises/automated-solve/task-1.md
new file mode 100644
index 0000000000..b470c01201
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/automated-solve/task-1.md
@@ -0,0 +1,8 @@
+# Task 1
+
+We've started by adding a single `move()` for you, which will move the character one step forward.
+
+Use the "Run code" button to see how close you're getting.
+
+It's good practice to get into the habit of running your code reguarly.
+In this exercise, we'd recommend running it after adding each new instruction, although you might like to try and challenge yourself to solve it all in one go instead!
diff --git a/bootcamp_content/projects/maze/exercises/automated-solve/task-2.md b/bootcamp_content/projects/maze/exercises/automated-solve/task-2.md
new file mode 100644
index 0000000000..b470c01201
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/automated-solve/task-2.md
@@ -0,0 +1,8 @@
+# Task 1
+
+We've started by adding a single `move()` for you, which will move the character one step forward.
+
+Use the "Run code" button to see how close you're getting.
+
+It's good practice to get into the habit of running your code reguarly.
+In this exercise, we'd recommend running it after adding each new instruction, although you might like to try and challenge yourself to solve it all in one go instead!
diff --git a/bootcamp_content/projects/maze/exercises/automated-solve/task-3.md b/bootcamp_content/projects/maze/exercises/automated-solve/task-3.md
new file mode 100644
index 0000000000..b470c01201
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/automated-solve/task-3.md
@@ -0,0 +1,8 @@
+# Task 1
+
+We've started by adding a single `move()` for you, which will move the character one step forward.
+
+Use the "Run code" button to see how close you're getting.
+
+It's good practice to get into the habit of running your code reguarly.
+In this exercise, we'd recommend running it after adding each new instruction, although you might like to try and challenge yourself to solve it all in one go instead!
diff --git a/bootcamp_content/projects/maze/exercises/automated-solve/task-4.md b/bootcamp_content/projects/maze/exercises/automated-solve/task-4.md
new file mode 100644
index 0000000000..b470c01201
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/automated-solve/task-4.md
@@ -0,0 +1,8 @@
+# Task 1
+
+We've started by adding a single `move()` for you, which will move the character one step forward.
+
+Use the "Run code" button to see how close you're getting.
+
+It's good practice to get into the habit of running your code reguarly.
+In this exercise, we'd recommend running it after adding each new instruction, although you might like to try and challenge yourself to solve it all in one go instead!
diff --git a/bootcamp_content/projects/maze/exercises/implement-move/config.json b/bootcamp_content/projects/maze/exercises/implement-move/config.json
new file mode 100644
index 0000000000..1875e15b06
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/implement-move/config.json
@@ -0,0 +1,124 @@
+{
+ "title": "Implement move",
+ "description": "Implement the move function",
+ "project_type": "maze",
+ "level": 5,
+ "tests_type": "state",
+ "tasks": [
+ {
+ "name": "Move up",
+ "tests": [
+ {
+ "slug": "move-up",
+ "name": "Moves up",
+ "setupFunctions": [
+ [
+ "setupGrid",
+ [
+ [0, 0, 0],
+ [0, 0, 0],
+ [0, 0, 0]
+ ]
+ ],
+ ["setupDirection", ["up"]],
+ ["setupPosition", [1, 1]]
+ ],
+ "function": "move",
+ "avaliableFunctions": ["moveCharacter"],
+ "checks": [
+ {
+ "name": "position",
+ "value": [0, -1]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Move down",
+ "tests": [
+ {
+ "slug": "move-down",
+ "name": "Moves down",
+ "setupFunctions": [
+ [
+ "setupGrid",
+ [
+ [0, 0, 0],
+ [0, 0, 0],
+ [0, 0, 0]
+ ]
+ ],
+ ["setupDirection", ["down"]],
+ ["setupPosition", [1, 1]]
+ ],
+ "function": "move",
+ "checks": [
+ {
+ "name": "position",
+ "value": [0, 1]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Move left",
+ "tests": [
+ {
+ "slug": "move-left",
+ "name": "Moves left",
+
+ "setupFunctions": [
+ [
+ "setupGrid",
+ [
+ [0, 0, 0],
+ [0, 0, 0],
+ [0, 0, 0]
+ ]
+ ],
+ ["setupDirection", ["left"]],
+ ["setupPosition", [1, 1]]
+ ],
+ "function": "move",
+ "checks": [
+ {
+ "name": "position",
+ "value": [-1, 0]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "Move right",
+ "tests": [
+ {
+ "slug": "move-right",
+ "name": "Moves right",
+
+ "setupFunctions": [
+ [
+ "setupGrid",
+ [
+ [0, 0, 0],
+ [0, 0, 0],
+ [0, 0, 0]
+ ]
+ ],
+ ["setupDirection", ["left"]],
+ ["setupPosition", [1, 1]]
+ ],
+ "function": "move",
+ "checks": [
+ {
+ "name": "position",
+ "value": [1, 0]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/bootcamp_content/projects/maze/exercises/implement-move/example.jk b/bootcamp_content/projects/maze/exercises/implement-move/example.jk
new file mode 100644
index 0000000000..a43e2c22f8
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/implement-move/example.jk
@@ -0,0 +1,21 @@
+// You have two tools at your disposal
+// - A variable called direction. It's been defined like this.
+// `set direction to "down"`
+// - A function called moveCharacter, that expects an x and y coordinate as its input, and moves the character accordingly.
+
+// Your job is to check the direction then call moveCharacter with the relative direction, so for example if you move up, you call moveCharacter(0,-1) - no change to the x, but a negative change to the y coordinate.
+
+function move do
+ if direction is "up" do
+ moveCharacter(0, -1)
+ end
+ else if direction is "down" do
+ moveCharacter(0, 1)
+ end
+ else if direction is "right" do
+ moveCharacter(1, 0)
+ end
+ else if direction is "left" do
+ moveCharacter(-1, 0)
+ end
+end
\ No newline at end of file
diff --git a/bootcamp_content/projects/maze/exercises/implement-move/introduction.md b/bootcamp_content/projects/maze/exercises/implement-move/introduction.md
new file mode 100644
index 0000000000..da2e8f32a2
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/implement-move/introduction.md
@@ -0,0 +1,19 @@
+# Even or odd
+
+Let's build a function that takes a _number_ as an input and returns a _string_ specifying whether it's `"Even"` (0, 2, 4, 6, 8, etc), or `"Odd"` (1, 3, 5, 7, etc) or `"Zero"`.
+
+To approach this problem, think about what it is that acutally makes a number odd or even.
+
+
+ Totally Stuck?
+ A good way of working out if a number is even or odd is to check whether it has a remainder when it's divided by 2.
+
+You probably remember from school that a remainder is what’s left over when you divide a number but can’t divide it evenly. In other words, it’s the part of the number that doesn’t fit into equal groups.
+
+For example, if you divide 7 by 3, you can fit two groups of 3 into 7 (since 3 + 3 = 6), but there’s 1 left over. That leftover 1 is the remainder. And that remainder makes it an odd number.
+
+So to solve this exercise, you might like to use the **[remainder operator]()**.
+
+```
+
+```
diff --git a/bootcamp_content/projects/maze/exercises/implement-move/stub.jk b/bootcamp_content/projects/maze/exercises/implement-move/stub.jk
new file mode 100644
index 0000000000..d358c9aab8
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/implement-move/stub.jk
@@ -0,0 +1,5 @@
+// Receives a number as its input
+// and should return "Positive", "Negative" or "Zero"
+function even_or_odd with num do
+
+end
\ No newline at end of file
diff --git a/bootcamp_content/projects/maze/exercises/implement-move/task-1.md b/bootcamp_content/projects/maze/exercises/implement-move/task-1.md
new file mode 100644
index 0000000000..ebd3b8b008
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/implement-move/task-1.md
@@ -0,0 +1,3 @@
+# Task 1
+
+Start with trying to get the `"Even"` check in place.
diff --git a/bootcamp_content/projects/maze/exercises/implement-move/task-2.md b/bootcamp_content/projects/maze/exercises/implement-move/task-2.md
new file mode 100644
index 0000000000..a5378b687d
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/implement-move/task-2.md
@@ -0,0 +1,3 @@
+# Task 2
+
+Great! Now get the `"Odd"` check in place.
diff --git a/bootcamp_content/projects/maze/exercises/implement-move/task-3.md b/bootcamp_content/projects/maze/exercises/implement-move/task-3.md
new file mode 100644
index 0000000000..da38a007cc
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/implement-move/task-3.md
@@ -0,0 +1,3 @@
+# Task 3
+
+Great! Finally, let's check that `0` is returned as `Even`.
diff --git a/bootcamp_content/projects/maze/exercises/manual-solve/config.json b/bootcamp_content/projects/maze/exercises/manual-solve/config.json
new file mode 100644
index 0000000000..00261c677e
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/manual-solve/config.json
@@ -0,0 +1,56 @@
+{
+ "title": "Manually solve a maze",
+ "description": "Solve a maze using some basic functions",
+ "project_type": "maze",
+ "level": 1,
+ "avaliableFunctions": ["move", "turnLeft", "turnRight"],
+ "tests_type": "state",
+ "concepts": ["functions-using"],
+ "tasks": [
+ {
+ "name": "Guide person to the end of the maze",
+ "tests": [
+ {
+ "slug": "maze-1",
+ "name": "Guide person to the end of the maze",
+ "setupFunctions": [
+ [
+ "setupGrid",
+ [
+ [
+ [2, 1, 0, 0, 0, 1, 0],
+ [0, 1, 0, 1, 0, 1, 1],
+ [0, 0, 0, 1, 0, 0, 0],
+ [0, 1, 1, 1, 0, 1, 1],
+ [0, 0, 1, 0, 0, 1, 0],
+ [1, 1, 1, 1, 0, 1, 1],
+ [0, 0, 0, 0, 0, 0, 3]
+ ]
+ ]
+ ],
+ ["setupDirection", ["down"]],
+ ["setupPosition", [0, 0]]
+ ],
+ "function": "runGame",
+ "checks": [
+ {
+ "name": "position",
+ "value": [6, 6],
+ "descriptionHtml": "Your position should be 6, 6, but it's %actual% ."
+ },
+ {
+ "name": "direction",
+ "value": "right",
+ "descriptionHtml": "You should be facing right, but you're facing %actual%."
+ },
+ {
+ "name": "getGameResult()",
+ "value": "win",
+ "descriptionHtml": "You didn't reach the end of the maze"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/bootcamp_content/projects/maze/exercises/manual-solve/example.jk b/bootcamp_content/projects/maze/exercises/manual-solve/example.jk
new file mode 100644
index 0000000000..7e4fc86905
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/manual-solve/example.jk
@@ -0,0 +1,6 @@
+move()
+move()
+turn_left()
+move()
+move()
+turn_right()
\ No newline at end of file
diff --git a/bootcamp_content/projects/maze/exercises/manual-solve/introduction.md b/bootcamp_content/projects/maze/exercises/manual-solve/introduction.md
new file mode 100644
index 0000000000..bf45496945
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/manual-solve/introduction.md
@@ -0,0 +1,11 @@
+# Solve the Maze
+
+Your task is to solve the following maze.
+
+You have three functions you can use:
+
+- `move()` which moves the character one step forward
+- `turn_left()` turns the character left (relative to the direction they're currently facing)
+- `turn_right()` turns the character right (relative to the direction they're currently facing)
+
+Remember to use one function per line.
diff --git a/bootcamp_content/projects/maze/exercises/manual-solve/stub.jk b/bootcamp_content/projects/maze/exercises/manual-solve/stub.jk
new file mode 100644
index 0000000000..27cc684a7b
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/manual-solve/stub.jk
@@ -0,0 +1,6 @@
+// You can use move(), turn_left() and turn_right()
+// Use the functions in the order you want your character
+// to use them to solve them maze. We'll start you off by
+// moving the character one step forward.
+
+move()
\ No newline at end of file
diff --git a/bootcamp_content/projects/maze/exercises/manual-solve/task-1.md b/bootcamp_content/projects/maze/exercises/manual-solve/task-1.md
new file mode 100644
index 0000000000..b470c01201
--- /dev/null
+++ b/bootcamp_content/projects/maze/exercises/manual-solve/task-1.md
@@ -0,0 +1,8 @@
+# Task 1
+
+We've started by adding a single `move()` for you, which will move the character one step forward.
+
+Use the "Run code" button to see how close you're getting.
+
+It's good practice to get into the habit of running your code reguarly.
+In this exercise, we'd recommend running it after adding each new instruction, although you might like to try and challenge yourself to solve it all in one go instead!
diff --git a/bootcamp_content/projects/maze/introduction.md b/bootcamp_content/projects/maze/introduction.md
new file mode 100644
index 0000000000..2d376d908f
--- /dev/null
+++ b/bootcamp_content/projects/maze/introduction.md
@@ -0,0 +1,7 @@
+# Maze
+
+## Overview
+
+This project contains the first exercise you ever do, but continues all the way through into Part 2.
+
+Your job is to solve, and then later create a maze that your character can explore through.
diff --git a/bootcamp_content/projects/number-puzzles/config.json b/bootcamp_content/projects/number-puzzles/config.json
new file mode 100644
index 0000000000..f320f28958
--- /dev/null
+++ b/bootcamp_content/projects/number-puzzles/config.json
@@ -0,0 +1,6 @@
+{
+ "slug": "number-puzzles",
+ "title": "Number Puzzles",
+ "description": "Implement the classic game",
+ "exercises": ["positive-negative-or-zero", "even-or-odd"]
+}
diff --git a/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/config.json b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/config.json
new file mode 100644
index 0000000000..870caa55d7
--- /dev/null
+++ b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/config.json
@@ -0,0 +1,63 @@
+{
+ "title": "Even or Odd",
+ "description": "Determine if a number is even or odd",
+ "concepts": ["strings-using", "conditionals"],
+ "level": 2,
+ "tasks": [
+ {
+ "name": "Correctly identify even numbers",
+ "tests": [
+ {
+ "slug": "number-2",
+ "type": "io",
+ "name": "Number 2",
+ "function": "even_or_odd",
+ "params": [2],
+ "expected": "Even"
+ },
+ {
+ "slug": "number-28",
+ "type": "io",
+ "name": "Number 28",
+ "function": "even_or_odd",
+ "params": [28],
+ "expected": "Odd"
+ }
+ ]
+ },
+ {
+ "name": "Correctly identify odd numbers",
+ "tests": [
+ {
+ "slug": "number-1",
+ "type": "io",
+ "name": "Number 1",
+ "function": "even_or_odd",
+ "params": [1],
+ "expected": "Odd"
+ },
+ {
+ "slug": "number-21",
+ "type": "io",
+ "name": "Number 21",
+ "function": "even_or_odd",
+ "params": [21],
+ "expected": "Odd"
+ }
+ ]
+ },
+ {
+ "name": "Correctly identify zero",
+ "tests": [
+ {
+ "slug": "number-0",
+ "type": "io",
+ "name": "Number 0",
+ "function": "even_or_odd",
+ "params": [0],
+ "expected": "Even"
+ }
+ ]
+ }
+ ]
+}
diff --git a/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/example.jk b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/example.jk
new file mode 100644
index 0000000000..f29ecc41b4
--- /dev/null
+++ b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/example.jk
@@ -0,0 +1,7 @@
+function even_or_odd with num do
+ if num % 2 equals 0 do
+ return "Even"
+ else do
+ return "Odd"
+ end
+end
\ No newline at end of file
diff --git a/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/introduction.md b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/introduction.md
new file mode 100644
index 0000000000..da2e8f32a2
--- /dev/null
+++ b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/introduction.md
@@ -0,0 +1,19 @@
+# Even or odd
+
+Let's build a function that takes a _number_ as an input and returns a _string_ specifying whether it's `"Even"` (0, 2, 4, 6, 8, etc), or `"Odd"` (1, 3, 5, 7, etc) or `"Zero"`.
+
+To approach this problem, think about what it is that acutally makes a number odd or even.
+
+
+ Totally Stuck?
+ A good way of working out if a number is even or odd is to check whether it has a remainder when it's divided by 2.
+
+You probably remember from school that a remainder is what’s left over when you divide a number but can’t divide it evenly. In other words, it’s the part of the number that doesn’t fit into equal groups.
+
+For example, if you divide 7 by 3, you can fit two groups of 3 into 7 (since 3 + 3 = 6), but there’s 1 left over. That leftover 1 is the remainder. And that remainder makes it an odd number.
+
+So to solve this exercise, you might like to use the **[remainder operator]()**.
+
+```
+
+```
diff --git a/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/stub.jk b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/stub.jk
new file mode 100644
index 0000000000..d358c9aab8
--- /dev/null
+++ b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/stub.jk
@@ -0,0 +1,5 @@
+// Receives a number as its input
+// and should return "Positive", "Negative" or "Zero"
+function even_or_odd with num do
+
+end
\ No newline at end of file
diff --git a/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/task-1.md b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/task-1.md
new file mode 100644
index 0000000000..ebd3b8b008
--- /dev/null
+++ b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/task-1.md
@@ -0,0 +1,3 @@
+# Task 1
+
+Start with trying to get the `"Even"` check in place.
diff --git a/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/task-2.md b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/task-2.md
new file mode 100644
index 0000000000..a5378b687d
--- /dev/null
+++ b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/task-2.md
@@ -0,0 +1,3 @@
+# Task 2
+
+Great! Now get the `"Odd"` check in place.
diff --git a/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/task-3.md b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/task-3.md
new file mode 100644
index 0000000000..da38a007cc
--- /dev/null
+++ b/bootcamp_content/projects/number-puzzles/exercises/even-or-odd/task-3.md
@@ -0,0 +1,3 @@
+# Task 3
+
+Great! Finally, let's check that `0` is returned as `Even`.
diff --git a/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/config.json b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/config.json
new file mode 100644
index 0000000000..aad53410b9
--- /dev/null
+++ b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/config.json
@@ -0,0 +1,63 @@
+{
+ "title": "Positive, Negative or Zero",
+ "description": "Determine if a number is positive, negative or zero",
+ "concepts": ["strings-using", "conditionals"],
+ "level": 2,
+ "tasks": [
+ {
+ "name": "Correctly identify positive numbers",
+ "tests": [
+ {
+ "slug": "number-1",
+ "type": "io",
+ "name": "Number 1",
+ "function": "positive_negative_or_zero",
+ "params": [1],
+ "expected": "Positive"
+ },
+ {
+ "slug": "number-20",
+ "type": "io",
+ "name": "Number 20",
+ "function": "positive_negative_or_zero",
+ "params": [20],
+ "expected": "Positive"
+ }
+ ]
+ },
+ {
+ "name": "Correctly identify negative numbers",
+ "tests": [
+ {
+ "slug": "negative-1",
+ "type": "io",
+ "name": "Number -1",
+ "function": "positive_negative_or_zero",
+ "params": [-1],
+ "expected": "Negative"
+ },
+ {
+ "slug": "negative-20",
+ "type": "io",
+ "name": "Number -20",
+ "function": "positive_negative_or_zero",
+ "params": [-20],
+ "expected": "Negative"
+ }
+ ]
+ },
+ {
+ "name": "Correctly identify zero",
+ "tests": [
+ {
+ "slug": "number-0",
+ "type": "io",
+ "name": "Number 0",
+ "function": "positive_negative_or_zero",
+ "params": [0],
+ "expected": "Zero"
+ }
+ ]
+ }
+ ]
+}
diff --git a/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/example.jk b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/example.jk
new file mode 100644
index 0000000000..3772cfa091
--- /dev/null
+++ b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/example.jk
@@ -0,0 +1,9 @@
+function positive_negative_or_zero with num do
+ if num > 0 do
+ return "Positive"
+ else if num < 0 do
+ return "Negative"
+ else do
+ return "Zero"
+ end
+end
\ No newline at end of file
diff --git a/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/introduction.md b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/introduction.md
new file mode 100644
index 0000000000..ef7a9cfb53
--- /dev/null
+++ b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/introduction.md
@@ -0,0 +1,3 @@
+# Positive, Negative or Zero
+
+Let's build a function that takes a _number_ as an input and returns a _string_ specifying whether it's `"Positive"` (greater than zero), `"Negative"` (less than zero) or `"Zero"`.
diff --git a/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/stub.jk b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/stub.jk
new file mode 100644
index 0000000000..95b3f0e31c
--- /dev/null
+++ b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/stub.jk
@@ -0,0 +1,5 @@
+// Receives a number as its input
+// and should return "Positive", "Negative" or "Zero"
+function positive_negative_or_zero with num do
+
+end
\ No newline at end of file
diff --git a/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/task-1.md b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/task-1.md
new file mode 100644
index 0000000000..5e43d9039d
--- /dev/null
+++ b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/task-1.md
@@ -0,0 +1,3 @@
+# Task 1
+
+Start with trying to get the `"Positive"` check in place.
diff --git a/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/task-2.md b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/task-2.md
new file mode 100644
index 0000000000..3746e6a983
--- /dev/null
+++ b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/task-2.md
@@ -0,0 +1,3 @@
+# Task 2
+
+Great! Now get the `"Negative"` check in place.
diff --git a/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/task-3.md b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/task-3.md
new file mode 100644
index 0000000000..b93c282445
--- /dev/null
+++ b/bootcamp_content/projects/number-puzzles/exercises/positive-negative-or-zero/task-3.md
@@ -0,0 +1,5 @@
+# Task 3
+
+Nice work!
+
+Finally let's get the `"Zero"` check in place.
diff --git a/bootcamp_content/projects/number-puzzles/introduction.md b/bootcamp_content/projects/number-puzzles/introduction.md
new file mode 100644
index 0000000000..025fa4b8a9
--- /dev/null
+++ b/bootcamp_content/projects/number-puzzles/introduction.md
@@ -0,0 +1,5 @@
+# Number Puzzles
+
+## Overview
+
+This project contains a series of miscellaneous puzzles related to manipulated numbers or determining things from them.
diff --git a/bootcamp_content/projects/rock-paper-scissors/config.json b/bootcamp_content/projects/rock-paper-scissors/config.json
new file mode 100644
index 0000000000..3915c85080
--- /dev/null
+++ b/bootcamp_content/projects/rock-paper-scissors/config.json
@@ -0,0 +1,6 @@
+{
+ "slug": "rock-paper-scissors",
+ "title": "Rock, Paper, Scissors",
+ "description": "Implement the classic game",
+ "exercises": ["basic"]
+}
diff --git a/bootcamp_content/projects/rock-paper-scissors/exercises/basic/config.json b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/config.json
new file mode 100644
index 0000000000..f97988c28e
--- /dev/null
+++ b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/config.json
@@ -0,0 +1,104 @@
+{
+ "title": "Rock Paper Scissors",
+ "description": "Calculate the correct result",
+ "concepts": ["Conditionals"],
+ "level": 2,
+ "tasks": [
+ {
+ "name": "Player 1 chooses paper",
+ "tests": [
+ {
+ "slug": "paper-vs-paper",
+ "type": "io",
+ "name": "Paper vs Paper",
+ "function": "who_wins",
+ "params": ["paper", "paper"],
+ "expected": "tie",
+ "image_slug": "rock-paper-scissors/paper-paper.png"
+ },
+ {
+ "slug": "paper-vs-rock",
+ "type": "io",
+ "name": "Paper vs Rock",
+ "function": "who_wins",
+ "params": ["paper", "rock"],
+ "expected": "player_1",
+ "image_slug": "rock-paper-scissors/paper-rock.png"
+ },
+ {
+ "slug": "paper-vs-scissors",
+ "type": "io",
+ "name": "Paper vs Scissors",
+ "function": "who_wins",
+ "params": ["paper", "scissors"],
+ "expected": "player_2",
+ "image_slug": "rock-paper-scissors/paper-scissors.png"
+ }
+ ]
+ },
+ {
+ "name": "Player 1 chooses rock",
+ "tests": [
+ {
+ "slug": "rock-vs-paper",
+ "type": "io",
+ "name": "Rock vs Paper",
+ "function": "who_wins",
+ "params": ["rock", "paper"],
+ "expected": "player_2",
+ "image_slug": "rock-paper-scissors/rock-paper.png"
+ },
+ {
+ "slug": "rock-vs-rock",
+ "type": "io",
+ "name": "Rock vs Rock",
+ "function": "who_wins",
+ "params": ["rock", "rock"],
+ "expected": "tie",
+ "image_slug": "rock-paper-scissors/rock-rock.png"
+ },
+ {
+ "slug": "rock-vs-scissors",
+ "type": "io",
+ "name": "Rock vs Scissors",
+ "function": "who_wins",
+ "params": ["rock", "scissors"],
+ "expected": "player_1",
+ "image_slug": "rock-paper-scissors/rock-scissors.png"
+ }
+ ]
+ },
+ {
+ "name": "Player 1 chooses scissors",
+ "tests": [
+ {
+ "slug": "scissors-vs-paper",
+ "type": "io",
+ "name": "Scissors vs Paper",
+ "function": "who_wins",
+ "params": ["scissors", "paper"],
+ "expected": "player_1",
+ "image_slug": "rock-paper-scissors/scissors-paper.png"
+ },
+ {
+ "slug": "scissors-vs-rock",
+ "type": "io",
+ "name": "Scissors vs Rock",
+ "function": "who_wins",
+ "params": ["scissors", "rock"],
+ "expected": "player_2",
+ "image_slug": "rock-paper-scissors/scissors-rock.png"
+ },
+ {
+ "slug": "scissors-vs-scissors",
+ "type": "io",
+ "name": "Scissors vs Scissors",
+ "function": "who_wins",
+ "params": ["scissors", "scissors"],
+ "expected": "tie",
+ "image_slug": "rock-paper-scissors/scissors-scissors.png"
+ }
+ ]
+ }
+ ]
+}
diff --git a/bootcamp_content/projects/rock-paper-scissors/exercises/basic/example.jk b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/example.jk
new file mode 100644
index 0000000000..2c0ef84106
--- /dev/null
+++ b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/example.jk
@@ -0,0 +1,27 @@
+function who_wins with player_1_choice, player_2_choice do
+ if player_1_choice is "rock" do
+ if player_2_choice is "rock" do
+ return "tie"
+ else if player_2_choice is "paper" do
+ return "player_2"
+ else do
+ return "player_1"
+ end
+ else if player_1_choice is "paper" do
+ if player_2_choice is "rock" do
+ return "player_1"
+ else if player_2_choice is "paper" do
+ return "tie"
+ else do
+ return "player_2"
+ end
+ else do
+ if player_2_choice is "rock" do
+ return "player_2"
+ else if player_2_choice is "paper" do
+ return "player_1"
+ else do
+ return "tie"
+ end
+ end
+end
\ No newline at end of file
diff --git a/bootcamp_content/projects/rock-paper-scissors/exercises/basic/introduction.md b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/introduction.md
new file mode 100644
index 0000000000..a531c32416
--- /dev/null
+++ b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/introduction.md
@@ -0,0 +1,11 @@
+# Part 1
+
+We're going to start off by determining who wins a game of rock-paper-scissors by creating the function `who_wins`.
+
+The function takes two inputs - `player_1_choice` and `player_2_choice`, which can be `"rock"`, `"paper"` or `"scissors"`.
+
+Your job is to return the winner (`"player_1"`, `"player_2"`, or `"tie`") based on the following rules:
+
+- Paper beats Rock
+- Rock beats Scissors
+- Scissors beat Paper
diff --git a/bootcamp_content/projects/rock-paper-scissors/exercises/basic/stub.jk b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/stub.jk
new file mode 100644
index 0000000000..cdfb9a2dac
--- /dev/null
+++ b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/stub.jk
@@ -0,0 +1,5 @@
+// The choices can be one of "rock", "paper" or "scissors"
+// Should return "player_1", "player_2" or "tie"
+function who_wins with player_1_choice, player_2_choice do
+
+end
\ No newline at end of file
diff --git a/bootcamp_content/projects/rock-paper-scissors/exercises/basic/task-1.md b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/task-1.md
new file mode 100644
index 0000000000..830e498063
--- /dev/null
+++ b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/task-1.md
@@ -0,0 +1,9 @@
+# Task 1
+
+There are a few different ways to approach this exercise, but let's solve it by breaking it down based on player 1's choice.
+
+To start with, return the correct value based on when player 1 choices "paper":
+
+- If player 2 also chooses paper, we should return `"tie"`
+- If player 2 chooses "rock", then the paper smoothers the rock so player 1 wins (return `"player_1"`)
+- If player 2 chooses "scissors", then the rock blunts the scissors so player 2 wins (return `"player_2"`)
diff --git a/bootcamp_content/projects/rock-paper-scissors/exercises/basic/task-2.md b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/task-2.md
new file mode 100644
index 0000000000..cfa6f86026
--- /dev/null
+++ b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/task-2.md
@@ -0,0 +1,7 @@
+# Task 2
+
+Great, now let's consider what happens if player 1 chooses "rock':
+
+- If player 2 also chooses rock, we should return `"tie"`
+- If player 2 chooses "paper", then the paper smoothers the rock so player 2 wins (return `"player_2"`)
+- If player 2 chooses "scissors", then the rock blunts the scissors so player 1 wins (return `"player_1"`)
diff --git a/bootcamp_content/projects/rock-paper-scissors/exercises/basic/task-3.md b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/task-3.md
new file mode 100644
index 0000000000..0ffe0edf7e
--- /dev/null
+++ b/bootcamp_content/projects/rock-paper-scissors/exercises/basic/task-3.md
@@ -0,0 +1,5 @@
+# Task 3
+
+Nice work. So we're two-thirds of the way there. The final situation is if player 1 chooses scissors.
+
+This time, we'll leave the logic for you to work out!
diff --git a/bootcamp_content/projects/rock-paper-scissors/introduction.md b/bootcamp_content/projects/rock-paper-scissors/introduction.md
new file mode 100644
index 0000000000..7abf4796cc
--- /dev/null
+++ b/bootcamp_content/projects/rock-paper-scissors/introduction.md
@@ -0,0 +1,21 @@
+# Rock Paper Scissors
+
+## Overview
+
+Rock Paper Scissors is a class game played around the world by people of all ages.
+
+We're going to build out this whole game, starting by implementing the logic of who wins using _Conditionals_, adding a way of keeping score using _Variables_, and eventually building out all the visuals in Part 2 of the course.
+
+## Introduction
+
+Rock, Paper, Scissors is one of those games everyone seems to know. It’s quick, simple, and surprisingly fun for how basic it is. Whether you’re trying to decide who gets the last slice of pizza or just passing time with a friend, this game has a way of making even small decisions feel epic. The best part? You don’t need any equipment—just your hands and a little competitive spirit.
+
+The rules are simple.
+
+Both players choose one of three options: Rock, Paper, or Scissors. Choices are revealed simultaneously and the winner is determined based on these rules:
+• Rock beats Scissors (Rock crushes Scissors).
+• Scissors beats Paper (Scissors cut Paper).
+• Paper beats Rock (Paper covers Rock).
+If both players choose the same option, it’s a tie, and the round is replayed (optional based on how you’re playing).
+
+You and your opponent pick one of three options—Rock, Paper, or Scissors—and reveal your choices at the same time. Rock beats Scissors (because it crushes it), Scissors beats Paper (because it cuts it), and Paper beats Rock (because it covers it). If you both pick the same thing, it’s a tie, and you go again. You can play a single round to settle something quickly or go best out of three for a proper showdown. It’s easy to learn, impossible to master, and always a good time.
diff --git a/bootcamp_content/projects/two-fer/config.json b/bootcamp_content/projects/two-fer/config.json
new file mode 100644
index 0000000000..0e383e2435
--- /dev/null
+++ b/bootcamp_content/projects/two-fer/config.json
@@ -0,0 +1,6 @@
+{
+ "slug": "two-fer",
+ "title": "TwoFer",
+ "description": "Cookies, strings, conditionals",
+ "exercises": ["basic"]
+}
diff --git a/bootcamp_content/projects/two-fer/exercises/basic/config.json b/bootcamp_content/projects/two-fer/exercises/basic/config.json
new file mode 100644
index 0000000000..cc539ce27b
--- /dev/null
+++ b/bootcamp_content/projects/two-fer/exercises/basic/config.json
@@ -0,0 +1,46 @@
+{
+ "title": "Empty Strings",
+ "description": "Return what you say when you hand the cookie other, using the person's name if you know it.",
+ "concepts": ["strings-concatenation", "conditionals"],
+ "level": 2,
+ "testsType": "io",
+ "readonly_ranges": [
+ { "from": 1, "to": 1 },
+ { "from": 3, "to": 3 }
+ ],
+ "tasks": [
+ {
+ "name": "Get things working with a single string",
+ "tests": [
+ {
+ "slug": "no_name",
+ "name": "no name given",
+ "function": "two_fer",
+ "params": [""],
+ "expected": "One for you, one for me."
+ }
+ ]
+ },
+ {
+ "name": "Get things working with a name",
+ "tests": [
+ {
+ "type": "io",
+ "slug": "alice",
+ "name": "A name is given",
+ "function": "two_fer",
+ "params": ["Alice"],
+ "expected": "One for Alice, one for me."
+ },
+ {
+ "type": "io",
+ "slug": "bob",
+ "name": "Another name is given",
+ "function": "two_fer",
+ "params": ["Bob"],
+ "expected": "One for Bob, one for me."
+ }
+ ]
+ }
+ ]
+}
diff --git a/bootcamp_content/projects/two-fer/exercises/basic/example.jk b/bootcamp_content/projects/two-fer/exercises/basic/example.jk
new file mode 100644
index 0000000000..802eb8f278
--- /dev/null
+++ b/bootcamp_content/projects/two-fer/exercises/basic/example.jk
@@ -0,0 +1,7 @@
+function two_fer with name do
+ if name is "" do
+ return "One for you, one for me."
+ else do
+ return "One for " + name + ", one for me."
+ end
+end
diff --git a/bootcamp_content/projects/two-fer/exercises/basic/introduction.md b/bootcamp_content/projects/two-fer/exercises/basic/introduction.md
new file mode 100644
index 0000000000..c8048e8dea
--- /dev/null
+++ b/bootcamp_content/projects/two-fer/exercises/basic/introduction.md
@@ -0,0 +1,15 @@
+# TwoFer Part 1
+
+Two Fer is a classic Exercism exercise.
+Through it, we'll explore a few ideas around using _Strings_ and _Conditionals_.
+
+In some English accents, when you say "two for" quickly, it sounds like "two fer". Two-for-one is a way of saying that if you buy one, you also get one for free. So the phrase "two-fer" often implies a two-for-one offer.
+
+Imagine a bakery that has a holiday offer where you can buy two cookies for the price of one ("two-fer one!"). You take the offer and (very generously) decide to give the extra cookie to someone else in the queue.
+
+As you give them the cookie, you one of two things.
+
+- If you know their name, you say: "One for , one for me."
+- If you don't know their name, you say: "One for you, one for me."
+
+For example, you might say "One for Jeremy, one for me" if you know Jeremy's name.
diff --git a/bootcamp_content/projects/two-fer/exercises/basic/stub.jk b/bootcamp_content/projects/two-fer/exercises/basic/stub.jk
new file mode 100644
index 0000000000..e871b02348
--- /dev/null
+++ b/bootcamp_content/projects/two-fer/exercises/basic/stub.jk
@@ -0,0 +1,3 @@
+function two_fer with name do
+
+end
diff --git a/bootcamp_content/projects/two-fer/exercises/basic/task-1.md b/bootcamp_content/projects/two-fer/exercises/basic/task-1.md
new file mode 100644
index 0000000000..1b03934503
--- /dev/null
+++ b/bootcamp_content/projects/two-fer/exercises/basic/task-1.md
@@ -0,0 +1,5 @@
+We've given you a function skeleton that takes one input - `name`.
+
+It will either be an empty string (`""`) or it will be someone's name (`"Jeremy"`).
+
+Let's start off by just considering that empty version and always returning our default version `"One for me, one for you."`.
diff --git a/bootcamp_content/projects/two-fer/exercises/basic/task-2.md b/bootcamp_content/projects/two-fer/exercises/basic/task-2.md
new file mode 100644
index 0000000000..2a597efd6a
--- /dev/null
+++ b/bootcamp_content/projects/two-fer/exercises/basic/task-2.md
@@ -0,0 +1,7 @@
+Nice work!
+
+Now we need to handle the situation where we **do** know the person's name.
+
+Sometime's the name will be empty (`""`) in which case we want to continue returning `"One for you, one for me."`, but other times `name` will contain a name, in which case we want to include it in the return value (e.g. `"One for Jeremy, one for me."`).
+
+Remember, you can join multiple strings together using the `join_strings(...)` function.
diff --git a/bootcamp_content/projects/two-fer/introduction.md b/bootcamp_content/projects/two-fer/introduction.md
new file mode 100644
index 0000000000..f3cf6682e4
--- /dev/null
+++ b/bootcamp_content/projects/two-fer/introduction.md
@@ -0,0 +1,25 @@
+# Two Fer
+
+## Overview
+
+Two Fer is a classic Exercism exercise.
+Through it, we'll explore a few ideas around using _Strings_ and _Conditionals_.
+
+## Introduction
+
+In some English accents, when you say "two for" quickly, it sounds like "two fer". Two-for-one is a way of saying that if you buy one, you also get one for free. So the phrase "two-fer" often implies a two-for-one offer.
+
+Imagine a bakery that has a holiday offer where you can buy two cookies for the price of one ("two-fer one!"). You take the offer and (very generously) decide to give the extra cookie to someone else in the queue.
+
+As you give them the cookie, if you know their name (e.g. they're called "Jeremy"), you say:
+
+```text
+"One for Jeremy, one for me."
+```
+
+If you don't know their name (even more generous of you!), you say:
+"One for you, one for me."
+
+```
+
+```
diff --git a/bootcamp_content/projects/wordle/config.json b/bootcamp_content/projects/wordle/config.json
new file mode 100644
index 0000000000..1bbcca61f0
--- /dev/null
+++ b/bootcamp_content/projects/wordle/config.json
@@ -0,0 +1,6 @@
+{
+ "slug": "wordle",
+ "title": "Wordle",
+ "description": "Create the modern Wordle game",
+ "exercises": ["process-guess"]
+}
diff --git a/bootcamp_content/projects/wordle/exercises/process-guess/config.json b/bootcamp_content/projects/wordle/exercises/process-guess/config.json
new file mode 100644
index 0000000000..e6580377bd
--- /dev/null
+++ b/bootcamp_content/projects/wordle/exercises/process-guess/config.json
@@ -0,0 +1,28 @@
+{
+ "title": "Process a guess",
+ "description": "Turn a guess into a valid response",
+ "project_type": "wordle",
+ "avaliableFunctions": [],
+ "concepts": ["conditionals", "arrays"],
+ "level": 3,
+ "testsType": "state",
+ "tasks": [
+ {
+ "name": "Deal with a correct guess",
+ "tests": [
+ {
+ "slug": "all-correct",
+ "name": "Deal with a fully correct guess",
+ "function": "process_guess",
+ "checks": [
+ {
+ "name": "getGuessState1()",
+ "value": ["present", "absent", "absent", "absent", "correct"],
+ "descriptionHtml": "We expected the first letter to be present and the last one to be correct"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/bootcamp_content/projects/wordle/exercises/process-guess/example.jk b/bootcamp_content/projects/wordle/exercises/process-guess/example.jk
new file mode 100644
index 0000000000..7e4fc86905
--- /dev/null
+++ b/bootcamp_content/projects/wordle/exercises/process-guess/example.jk
@@ -0,0 +1,6 @@
+move()
+move()
+turn_left()
+move()
+move()
+turn_right()
\ No newline at end of file
diff --git a/bootcamp_content/projects/wordle/exercises/process-guess/introduction.md b/bootcamp_content/projects/wordle/exercises/process-guess/introduction.md
new file mode 100644
index 0000000000..bf45496945
--- /dev/null
+++ b/bootcamp_content/projects/wordle/exercises/process-guess/introduction.md
@@ -0,0 +1,11 @@
+# Solve the Maze
+
+Your task is to solve the following maze.
+
+You have three functions you can use:
+
+- `move()` which moves the character one step forward
+- `turn_left()` turns the character left (relative to the direction they're currently facing)
+- `turn_right()` turns the character right (relative to the direction they're currently facing)
+
+Remember to use one function per line.
diff --git a/bootcamp_content/projects/wordle/exercises/process-guess/stub.jk b/bootcamp_content/projects/wordle/exercises/process-guess/stub.jk
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/bootcamp_content/projects/wordle/exercises/process-guess/task-1.md b/bootcamp_content/projects/wordle/exercises/process-guess/task-1.md
new file mode 100644
index 0000000000..b470c01201
--- /dev/null
+++ b/bootcamp_content/projects/wordle/exercises/process-guess/task-1.md
@@ -0,0 +1,8 @@
+# Task 1
+
+We've started by adding a single `move()` for you, which will move the character one step forward.
+
+Use the "Run code" button to see how close you're getting.
+
+It's good practice to get into the habit of running your code reguarly.
+In this exercise, we'd recommend running it after adding each new instruction, although you might like to try and challenge yourself to solve it all in one go instead!
diff --git a/bootcamp_content/projects/wordle/introduction.md b/bootcamp_content/projects/wordle/introduction.md
new file mode 100644
index 0000000000..2d376d908f
--- /dev/null
+++ b/bootcamp_content/projects/wordle/introduction.md
@@ -0,0 +1,7 @@
+# Maze
+
+## Overview
+
+This project contains the first exercise you ever do, but continues all the way through into Part 2.
+
+Your job is to solve, and then later create a maze that your character can explore through.
diff --git a/config/routes.rb b/config/routes.rb
index 4bde8de519..3006e16447 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -486,4 +486,6 @@
post "/bootcamp/stripe/create-checkout-session" => "bootcamp#stripe_create_checkout_session", as: :bootcamp_
get "/bootcamp/stripe/session-status" => "bootcamp#stripe_session_status", as: :bootcamp_stripe_session_status
get "/bootcamp/confirmed" => "bootcamp#confirmed", as: :bootcamp_confirmed
+
+ draw(:bootcamp)
end
diff --git a/config/routes/bootcamp.rb b/config/routes/bootcamp.rb
new file mode 100644
index 0000000000..fc7182a255
--- /dev/null
+++ b/config/routes/bootcamp.rb
@@ -0,0 +1,24 @@
+namespace :bootcamp do
+ get "dashboard", to: "dashboard#index", as: :dashboard
+ resources "levels", param: :idx, only: %i[index show]
+ resources "concepts", param: :slug, only: %i[index show]
+ resources "projects", param: :slug, only: %i[index show] do
+ resources "exercises", param: :slug, only: %i[index show edit]
+ end
+
+ namespace :admin do
+ resource :settings, only: [:show] do
+ post :increment_level, on: :collection
+ end
+ resources :exercises
+ end
+end
+
+# namespace :api do
+# resources :solutions, param: :uuid, only: [] do
+# member do
+# patch :complete
+# end
+# resources :submissions, param: :uuid, only: [:create]
+# end
+# end
diff --git a/db/bootcamp_seeds.rb b/db/bootcamp_seeds.rb
new file mode 100644
index 0000000000..5c803e420e
--- /dev/null
+++ b/db/bootcamp_seeds.rb
@@ -0,0 +1,82 @@
+return unless Rails.env.development?
+
+# rubocop:disable Layout/LineLength
+Bootcamp::Submission.destroy_all
+Bootcamp::Solution.destroy_all
+Bootcamp::Exercise.destroy_all
+Bootcamp::Project.destroy_all
+Bootcamp::Concept.destroy_all
+Bootcamp::Level.destroy_all
+
+def concept_intro_for(slug) = File.read(Rails.root / "bootcamp_content/concepts/#{slug}.md")
+
+def project_config_for(slug) = JSON.parse(File.read(Rails.root / "bootcamp_content/projects/#{slug}/config.json"),
+ symbolize_names: true)
+
+def project_intro_for(slug) = File.read(Rails.root / "bootcamp_content/projects/#{slug}/introduction.md")
+
+def exercise_code_for(project_slug,
+ exercise_slug) = File.read(Rails.root / "bootcamp_content/projects/#{project_slug}/exercises/#{exercise_slug}/code")
+
+def exercise_config_for(project_slug,
+ exercise_slug) = JSON.parse(
+ File.read(Rails.root / "bootcamp_content/projects/#{project_slug}/exercises/#{exercise_slug}/config.json"), symbolize_names: true
+ )
+
+JSON.parse(File.read(Rails.root / "bootcamp_content/levels/config.json"), symbolize_names: true).each.with_index do |details, idx|
+ idx += 1
+ Bootcamp::Level.create!(
+ idx:,
+ title: details[:title],
+ description: details[:description],
+ content_markdown: File.read(Rails.root / "bootcamp_content/levels/#{idx}.md")
+ )
+end
+
+JSON.parse(File.read(Rails.root / "bootcamp_content/concepts/config.json"), symbolize_names: true).each do |details|
+ Bootcamp::Concept.create!(
+ slug: details[:slug],
+ parent: details[:parent] ? Bootcamp::Concept.find_by!(slug: details[:parent]) : nil,
+ title: details[:title],
+ description: details[:description],
+ content_markdown: concept_intro_for(details[:slug]),
+ level_idx: details[:level],
+ apex: details[:apex] || false
+ )
+end
+
+projects = %w[
+ two-fer
+ rock-paper-scissors
+ number-puzzles
+ drawing
+ maze
+ wordle
+]
+
+projects.each do |project_slug|
+ project_config = project_config_for(project_slug)
+ project = Bootcamp::Project.create!(
+ slug: project_slug,
+ title: project_config[:title],
+ description: project_config[:description],
+ introduction_markdown: project_intro_for(project_slug)
+ )
+ project_config[:exercises].each.with_index do |exercise_slug, idx|
+ exercise_config = exercise_config_for(project_slug, exercise_slug)
+ project.exercises.create!(
+ slug: exercise_slug,
+ idx: idx + 1,
+ title: exercise_config[:title],
+ description: exercise_config[:description],
+ level_idx: exercise_config[:level],
+ concepts: exercise_config[:concepts].map do |slug|
+ Bootcamp::Concept.find_by!(slug:)
+ end
+ )
+ end
+end
+
+Bootcamp::UserProject::CreateAll.(User.find_by!(handle: 'iHiD'))
+
+# rubocop:enable Layout/LineLength
diff --git a/tailwind.config.js b/tailwind.config.js
index 8a47825f07..a879d218ba 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -86,6 +86,37 @@ module.exports = {
navDropdown: '0px 6px 28px 4px rgba(var(--shadowColorMain), 0.5)',
},
colors: {
+ /* REPLACE */
+ 'gray-200': 'rgb(229 231 235)',
+ 'gray-300': 'rgb(209 213 219)',
+ 'gray-400': 'rgb(156 163 175)',
+ 'gray-600': 'rgb(209 213 219)',
+ 'gray-500': 'rgb(107 114 128)',
+ 'gray-800': 'rgb(31 41 55)',
+ 'gray-900': 'rgb(17 24 39)',
+ 'slate-400': 'rgb(148 163 184)',
+ 'indigo-300': 'rgb(191 204 255)',
+ 'blue-100': 'rgb(227 236 255)',
+ 'blue-400': 'rgb(59 130 246)',
+ 'blue-500': 'rgb(59 130 246)',
+ 'blue-700': 'rgb(59 130 246)',
+ 'red-100': 'rgb(255 236 236)',
+ 'red-300': 'rgb(255 236 236)',
+ 'red-500': 'rgb(239 68 68)',
+ 'red-700': 'rgb(239 68 68)',
+ 'red-900': 'rgb(107 10 10)',
+ 'green-100': 'rgb(236 253 245)',
+ 'green-300': 'rgb(236 253 245)',
+ 'green-400': 'rgb(0 230 118)',
+ 'green-500': 'rgb(0 230 118)',
+ 'green-700': 'rgb(0 128 0)',
+
+ 'thick-border-blue': '#F4F6FF',
+ 'primary-blue': 'rgb(46 87 232)',
+ 'background-purple': '#FCF9FF',
+ 'thin-border-blue': '#E2E9FF',
+ 'jiki-purple': '#7128F5',
+
transparent: 'transparent',
current: 'currentColor',
@@ -322,6 +353,7 @@ module.exports = {
auto: 'auto',
arbitary: '1px',
fill: '100%',
+ full: '100%',
32: '32px',
48: '48px',
100: '100%',
@@ -382,6 +414,7 @@ module.exports = {
auto: 'auto',
arbitary: '1px',
fill: '100%',
+ full: '100%',
'5-7': '71.4%',
'1-3': '33.3%',
'1-2': '50%',
@@ -404,6 +437,7 @@ module.exports = {
menu: '40',
dropdown: '50',
tooltip: '80',
+ 'tooltip-content': '81',
modal: '100',
redirect: '150',
},