rubydojo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ module Rubydojo
2
+ class DashboardController < ApplicationController
3
+ def index
4
+ @lessons = Rubydojo::Lesson.all
5
+ @completed_lessons = session[:completed_lessons] || []
6
+ @progress_percent = if @lessons.any?
7
+ ((@completed_lessons.size.to_f / @lessons.size) * 100).round
8
+ else
9
+ 0
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,99 @@
1
+ module Rubydojo
2
+ class LessonsController < ApplicationController
3
+ before_action :find_lesson, only: [:show, :run, :validate]
4
+
5
+ def show
6
+ @completed_lessons = session[:completed_lessons] || []
7
+ @is_completed = @completed_lessons.include?(@lesson.id.to_s)
8
+ end
9
+
10
+ def run
11
+ code = params[:code].to_s
12
+ eval_result = evaluate_ruby(code)
13
+
14
+ render json: {
15
+ stdout: eval_result[:stdout],
16
+ result: eval_result[:result],
17
+ error: eval_result[:error]
18
+ }
19
+ end
20
+
21
+ def validate
22
+ code = params[:code].to_s
23
+ validation_code = @lesson.validation_code
24
+
25
+ eval_result = evaluate_ruby(code, validation_code)
26
+
27
+ if eval_result[:error].nil?
28
+ # Mark lesson as completed
29
+ session[:completed_lessons] ||= []
30
+ unless session[:completed_lessons].include?(@lesson.id.to_s)
31
+ session[:completed_lessons] << @lesson.id.to_s
32
+ end
33
+
34
+ render json: {
35
+ success: true,
36
+ stdout: eval_result[:stdout],
37
+ result: eval_result[:result]
38
+ }
39
+ else
40
+ render json: {
41
+ success: false,
42
+ error: eval_result[:error],
43
+ stdout: eval_result[:stdout]
44
+ }
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def find_lesson
51
+ @lesson = Rubydojo::Lesson.find(params[:id])
52
+ unless @lesson
53
+ redirect_to root_path, alert: "Lesson not found."
54
+ end
55
+ end
56
+
57
+ def evaluate_ruby(user_code, validation_code = nil)
58
+ require "stringio"
59
+
60
+ stdout_io = StringIO.new
61
+ original_stdout = $stdout
62
+ $stdout = stdout_io
63
+
64
+ error = nil
65
+ result = nil
66
+
67
+ # Create an isolated sandbox class and instance to prevent global namespace/constant pollution
68
+ sandbox_class = Class.new
69
+ sandbox_class.class_eval do
70
+ def get_binding
71
+ binding
72
+ end
73
+ end
74
+ sandbox_instance = sandbox_class.new
75
+ eval_binding = sandbox_instance.get_binding
76
+
77
+ begin
78
+ # Evaluate user code
79
+ result = eval(user_code, eval_binding, "(user_code)", 1)
80
+
81
+ # Evaluate validation code if provided
82
+ if validation_code.present?
83
+ eval(validation_code, eval_binding, "(validation_code)", 1)
84
+ end
85
+ rescue Exception => e
86
+ error = e.message
87
+ ensure
88
+ # Restore stdout
89
+ $stdout = original_stdout
90
+ end
91
+
92
+ {
93
+ stdout: stdout_io.string,
94
+ result: result.inspect,
95
+ error: error
96
+ }
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,4 @@
1
+ module Rubydojo
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,5 @@
1
+ module Rubydojo
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,61 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Rubydojo | Interactive Ruby Roadmap & Playground</title>
7
+ <meta name="description" content="An interactive suite inside your Rails development environment designed to accelerate learning the Ruby language with structured roadmaps, live compilation, and exercises.">
8
+
9
+ <%= csrf_meta_tags %>
10
+ <%= csp_meta_tag %>
11
+
12
+ <!-- Google Fonts -->
13
+ <link rel="preconnect" href="https://fonts.googleapis.com">
14
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15
+
16
+ <!-- CodeMirror CDN Stylesheets -->
17
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.css">
18
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/theme/material-palenight.min.css">
19
+
20
+ <!-- Engine Custom CSS -->
21
+ <%= stylesheet_link_tag "rubydojo/application", media: "all" %>
22
+
23
+ <%= yield :head %>
24
+ </head>
25
+ <body>
26
+
27
+ <!-- Top Glassmorphism Navigation -->
28
+ <header class="app-header">
29
+ <a href="<%= root_path %>" class="logo">
30
+ <svg viewBox="0 0 100 100" style="fill: currentColor; width: 2.2rem; height: 2.2rem; padding: 0.2rem; background: linear-gradient(135deg, var(--color-primary), var(--color-secondary)); border-radius: 0.5rem; color: white; box-shadow: 0 4px 10px var(--color-primary-glow);">
31
+ <polygon points="50,15 80,15 90,40 50,85 10,40 20,15" />
32
+ <polygon points="50,15 20,15 10,40 35,40" style="fill: rgba(255,255,255,0.25);" />
33
+ <polygon points="50,15 80,15 90,40 65,40" style="fill: rgba(0,0,0,0.15);" />
34
+ <polygon points="35,40 65,40 50,85" style="fill: rgba(255,255,255,0.15);" />
35
+ </svg>
36
+ <span>Rubydojo</span>
37
+ </a>
38
+
39
+ <nav class="nav-links">
40
+ <a href="<%= root_path %>" class="nav-btn secondary">Roadmap</a>
41
+ <a href="https://ruby-doc.org" target="_blank" rel="noopener noreferrer" class="nav-btn secondary">Ruby Docs</a>
42
+ <a href="https://guides.rubyonrails.org" target="_blank" rel="noopener noreferrer" class="nav-btn secondary">Rails Guides</a>
43
+ </nav>
44
+ </header>
45
+
46
+ <main>
47
+ <%= yield %>
48
+ </main>
49
+
50
+ <!-- CodeMirror CDN JavaScripts -->
51
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.js"></script>
52
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/mode/ruby/ruby.min.js"></script>
53
+
54
+ <!-- Engine Custom JavaScript -->
55
+ <script>
56
+ // Global asset config or simple helpers if needed
57
+ window.Rubydojo = {};
58
+ </script>
59
+
60
+ </body>
61
+ </html>
@@ -0,0 +1,56 @@
1
+ <div class="container animate-fade-in">
2
+ <!-- Hero Section -->
3
+ <section class="hero-section">
4
+ <h1 class="hero-title">Your Ruby Learning Roadmap</h1>
5
+ <p class="hero-subtitle">
6
+ Master Ruby fundamentals, dynamic programming, and Rails patterns step-by-step with interactive coding exercises.
7
+ </p>
8
+
9
+ <!-- Progress Tracker -->
10
+ <div class="progress-container">
11
+ <div class="progress-text">
12
+ <span>Roadmap Progress</span>
13
+ <strong><%= @completed_lessons.size %> / <%= @lessons.size %> Completed</strong>
14
+ </div>
15
+ <div class="progress-bar-bg">
16
+ <div class="progress-bar-fill" style="width: <%= @progress_percent %>%;"></div>
17
+ </div>
18
+ </div>
19
+ </section>
20
+
21
+ <!-- Interactive Roadmap Path -->
22
+ <section class="roadmap-path">
23
+ <div class="roadmap-line"></div>
24
+
25
+ <% @lessons.each_with_index do |lesson, index| %>
26
+ <% is_done = @completed_lessons.include?(lesson.id.to_s) %>
27
+ <div class="roadmap-node <%= 'completed' if is_done %>" style="animation-delay: <%= index * 0.1 %>s">
28
+ <!-- Node Number Circle -->
29
+ <div class="node-circle">
30
+ <% if is_done %>
31
+
32
+ <% else %>
33
+ <%= index + 1 %>
34
+ <% end %>
35
+ </div>
36
+
37
+ <!-- Node Content Card -->
38
+ <div class="node-card <%= 'completed' if is_done %>">
39
+ <div class="node-badge"><%= lesson.level %></div>
40
+ <h2 class="node-title"><%= lesson.title %></h2>
41
+ <p class="node-description"><%= lesson.description %></p>
42
+
43
+ <div class="node-footer">
44
+ <span class="node-status-text">
45
+ <span class="status-indicator <%= is_done ? 'completed' : 'pending' %>"></span>
46
+ <%= is_done ? 'Completed' : 'Not Started' %>
47
+ </span>
48
+ <a href="<%= lesson_path(lesson.id) %>" class="nav-btn <%= is_done ? 'secondary' : 'primary' %>">
49
+ <%= is_done ? 'Review Lesson' : 'Start Lesson' %>
50
+ </a>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ <% end %>
55
+ </section>
56
+ </div>
@@ -0,0 +1,220 @@
1
+ <div class="lesson-layout">
2
+ <!-- Left Side: Instruction Pane -->
3
+ <div class="instruction-pane">
4
+ <a href="<%= root_path %>" class="back-link">
5
+ <span>←</span> Back to Roadmap
6
+ </a>
7
+
8
+ <!-- Raw Markdown content stored in a script tag for JS parsing -->
9
+ <script id="markdown-raw" type="text/markdown"><%= raw @lesson.explanation %></script>
10
+
11
+ <!-- Rendered HTML goes here -->
12
+ <div id="markdown-viewer" class="markdown-content"></div>
13
+
14
+ <!-- Hint Box -->
15
+ <% if @lesson.hint.present? %>
16
+ <button id="btn-hint" class="btn btn-secondary" style="margin-top: 2rem; width: 100%;">
17
+ 💡 Toggle Hint
18
+ </button>
19
+ <div id="hint-box" class="hint-box">
20
+ <strong>Hint:</strong> <%= @lesson.hint %>
21
+ </div>
22
+ <% end %>
23
+
24
+ <!-- Success banner if completed -->
25
+ <div id="success-banner" class="success-banner" style="display: <%= @is_completed ? 'flex' : 'none' %>;">
26
+ <span class="success-banner-icon">🎉</span>
27
+ <div>
28
+ <strong>Excellent Work!</strong> You've successfully completed this lesson.
29
+ </div>
30
+ </div>
31
+ </div>
32
+
33
+ <!-- Right Side: Code Editor and Terminal Console -->
34
+ <div class="workspace-pane">
35
+ <div class="pane-header">
36
+ <div class="pane-title">
37
+ <span>solution.rb</span>
38
+ </div>
39
+ <span class="node-badge" style="color: var(--color-primary);"><%= @lesson.level %></span>
40
+ </div>
41
+
42
+ <!-- Editor -->
43
+ <div class="editor-container">
44
+ <textarea id="code-editor"><%= @lesson.code_template %></textarea>
45
+ </div>
46
+
47
+ <!-- Simulated Terminal -->
48
+ <div class="terminal-container">
49
+ <div class="terminal-header">
50
+ <span>CONSOLE OUTPUT</span>
51
+ <span id="terminal-clear" style="cursor: pointer; hover: color: white;">Clear</span>
52
+ </div>
53
+ <div id="terminal-body" class="terminal-body">
54
+ <div class="terminal-line system">
55
+ <span class="prompt-symbol">system ></span>
56
+ <span>Welcome to Rubydojo interactive terminal. Write code and hit Run.</span>
57
+ </div>
58
+ </div>
59
+ </div>
60
+
61
+ <!-- Workspace Actions -->
62
+ <div class="workspace-actions">
63
+ <button id="btn-run" class="btn btn-secondary">
64
+ ▶ Run Code
65
+ </button>
66
+
67
+ <button id="btn-validate" class="btn btn-primary">
68
+ ✔ Validate Solution
69
+ </button>
70
+
71
+ <% next_les = Rubydojo::Lesson.next_lesson(@lesson.id) %>
72
+ <% if next_les %>
73
+ <a id="btn-next" href="<%= lesson_path(next_les.id) %>" class="btn btn-success" style="display: <%= @is_completed ? 'inline-flex' : 'none' %>;">
74
+ Next Lesson ➔
75
+ </a>
76
+ <% else %>
77
+ <a id="btn-next" href="<%= root_path %>" class="btn btn-success" style="display: <%= @is_completed ? 'inline-flex' : 'none' %>;">
78
+ Roadmap Complete ➔
79
+ </a>
80
+ <% end %>
81
+ </div>
82
+ </div>
83
+ </div>
84
+
85
+ <!-- Load Marked.js from CDN for browser markdown parsing -->
86
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
87
+
88
+ <script>
89
+ document.addEventListener("DOMContentLoaded", function() {
90
+ // 1. Render Markdown Explanation
91
+ const rawMarkdown = document.getElementById("markdown-raw").textContent;
92
+ document.getElementById("markdown-viewer").innerHTML = marked.parse(rawMarkdown);
93
+
94
+ // 2. Initialize CodeMirror Editor
95
+ const editorEl = document.getElementById("code-editor");
96
+ const editor = CodeMirror.fromTextArea(editorEl, {
97
+ mode: "ruby",
98
+ theme: "material-palenight",
99
+ lineNumbers: true,
100
+ tabSize: 2,
101
+ indentWithTabs: false,
102
+ smartIndent: true,
103
+ lineWrapping: true
104
+ });
105
+
106
+ // 3. Setup Terminal helpers
107
+ const terminalBody = document.getElementById("terminal-body");
108
+
109
+ function writeToTerminal(content, type = 'output') {
110
+ const line = document.createElement("div");
111
+ line.className = `terminal-line ${type}`;
112
+
113
+ const prompt = document.createElement("span");
114
+ prompt.className = "prompt-symbol";
115
+ prompt.textContent = type === 'system' ? "system >" : "ruby >";
116
+
117
+ const text = document.createElement("span");
118
+ text.textContent = content;
119
+
120
+ line.appendChild(prompt);
121
+ line.appendChild(text);
122
+ terminalBody.appendChild(line);
123
+
124
+ // Auto scroll terminal to bottom
125
+ terminalBody.scrollTop = terminalBody.scrollHeight;
126
+ }
127
+
128
+ document.getElementById("terminal-clear").addEventListener("click", () => {
129
+ terminalBody.innerHTML = '';
130
+ writeToTerminal("Console cleared.", "system");
131
+ });
132
+
133
+ // 4. Toggle Hint Box
134
+ const btnHint = document.getElementById("btn-hint");
135
+ if (btnHint) {
136
+ btnHint.addEventListener("click", () => {
137
+ const hintBox = document.getElementById("hint-box");
138
+ hintBox.classList.toggle("visible");
139
+ });
140
+ }
141
+
142
+ // 5. Run Code AJAX Action
143
+ const btnRun = document.getElementById("btn-run");
144
+ btnRun.addEventListener("click", function() {
145
+ const code = editor.getValue();
146
+ btnRun.disabled = true;
147
+ btnRun.innerHTML = `<span class="spinner"></span> Running...`;
148
+
149
+ fetch("<%= run_lesson_path(@lesson.id) %>", {
150
+ method: "POST",
151
+ headers: {
152
+ "Content-Type": "application/json",
153
+ "X-CSRF-Token": "<%= form_authenticity_token %>"
154
+ },
155
+ body: JSON.stringify({ code: code })
156
+ })
157
+ .then(res => res.json())
158
+ .then(data => {
159
+ btnRun.disabled = false;
160
+ btnRun.textContent = "▶ Run Code";
161
+
162
+ if (data.stdout && data.stdout.trim() !== '') {
163
+ writeToTerminal(data.stdout, 'output');
164
+ }
165
+
166
+ if (data.error) {
167
+ writeToTerminal(`Error: ${data.error}`, 'error');
168
+ } else {
169
+ writeToTerminal(`=> ${data.result}`, 'system');
170
+ }
171
+ })
172
+ .catch(err => {
173
+ btnRun.disabled = false;
174
+ btnRun.textContent = "▶ Run Code";
175
+ writeToTerminal("Network error while trying to run code.", "error");
176
+ });
177
+ });
178
+
179
+ // 6. Validate Code AJAX Action
180
+ const btnValidate = document.getElementById("btn-validate");
181
+ btnValidate.addEventListener("click", function() {
182
+ const code = editor.getValue();
183
+ btnValidate.disabled = true;
184
+ btnValidate.innerHTML = `<span class="spinner"></span> Validating...`;
185
+
186
+ fetch("<%= validate_lesson_path(@lesson.id) %>", {
187
+ method: "POST",
188
+ headers: {
189
+ "Content-Type": "application/json",
190
+ "X-CSRF-Token": "<%= form_authenticity_token %>"
191
+ },
192
+ body: JSON.stringify({ code: code })
193
+ })
194
+ .then(res => res.json())
195
+ .then(data => {
196
+ btnValidate.disabled = false;
197
+ btnValidate.textContent = "✔ Validate Solution";
198
+
199
+ if (data.stdout && data.stdout.trim() !== '') {
200
+ writeToTerminal(data.stdout, 'output');
201
+ }
202
+
203
+ if (data.success) {
204
+ writeToTerminal("✔ SUCCESS: All validation checks passed!", "success");
205
+ document.getElementById("success-banner").style.display = "flex";
206
+ document.getElementById("btn-next").style.display = "inline-flex";
207
+ // Trigger a subtle console success log
208
+ writeToTerminal("Nice job! Move on to the next lesson.", "system");
209
+ } else {
210
+ writeToTerminal(`❌ VALIDATION FAILED: ${data.error}`, 'error');
211
+ }
212
+ })
213
+ .catch(err => {
214
+ btnValidate.disabled = false;
215
+ btnValidate.textContent = "✔ Validate Solution";
216
+ writeToTerminal("Network error during validation.", "error");
217
+ });
218
+ });
219
+ });
220
+ </script>
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ Rubydojo::Engine.routes.draw do
2
+ root to: "dashboard#index"
3
+ resources :lessons, only: [:show] do
4
+ post :run, on: :member
5
+ post :validate, on: :member
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ module Rubydojo
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Rubydojo
4
+ end
5
+ end