one-for-all-framework 4.0.0 → 4.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6010d541e08ee4d29882dbfe22d000853a511985f01ee3b18239c57086bd7906
4
- data.tar.gz: d05cdc0c5df07e8f2d9c0f3715169a3817b04b34da654dc63ce8afbaba0b932a
3
+ metadata.gz: 1634666412d07dfeae3b9416c7a27b16436ba9d1a03f726df8b245e6f6fe71f7
4
+ data.tar.gz: 4240f873f851ecfccd556a7b061e2ac3c2020aa26d383b87a217506160c6295b
5
5
  SHA512:
6
- metadata.gz: 4b84becdb59e23e5177cd6d61b33ae6875cbcdc00a1ab81fe98c4e644c97e1549950cdeef4da11758564cf316d009f69bd69b431d33852e9384383294126acbb
7
- data.tar.gz: 45544d4a74c6636667674f46b3ede29f4bde1e7a5e14e3f0d9385450b03676e76c4f538169386255d8f5ed1a1c70f971f65c3a88bdb948b6ace802ca8bc05170
6
+ metadata.gz: efb0120858fb436c64f1ec7a92433deda88fee6dc1738661af68a0a303d8874cb4f6a5a7f2583a527ad8e9a9188b0e67b16abaf201c205044a421cb2a3780635
7
+ data.tar.gz: ff176420fce31ecfec8fffcec1862e911d64f8b78478d3a073f3b317bd98fa553df7b9d05fd36b5a0c219582ec1b835a83eb854480cdeb62a624822c2df02170
data/.gitignore ADDED
@@ -0,0 +1,35 @@
1
+ # Ruby
2
+ *.gem
3
+ *.rbc
4
+ /.bundle/
5
+ /vendor/bundle/
6
+ /lib/bundler/man/
7
+ /coverage/
8
+ /InstalledFiles
9
+ /pkg/
10
+ /spec/reports/
11
+ /spec/examples.txt
12
+ /test/tmp/
13
+ /test/version_tmp/
14
+ /tmp/
15
+ /log/
16
+
17
+ # Environment
18
+ .env
19
+ .env.test
20
+ .env.production
21
+ .env.local
22
+
23
+ # Database
24
+ db/*.sqlite3
25
+ db/*.sqlite3-journal
26
+
27
+ # IDEs
28
+ .idea/
29
+ .vscode/
30
+ .ruby-lsp/
31
+ .DS_Store
32
+
33
+ # Framework Specific
34
+ public/img/uploads/*
35
+ !public/img/uploads/.gitkeep
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <img src="public/images/logo.png" width="500" height="500" alt="OFA Framework Logo">
3
3
  </p>
4
4
 
5
- # ⚡ One-For-All (OFA) Framework v4.0.0
5
+ # ⚡ One-For-All (OFA) Framework v4.2.0
6
6
 
7
7
  [![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%203.0.0-red.svg)](https://www.ruby-lang.org/)
8
8
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
@@ -20,6 +20,7 @@
20
20
  - **🛠️ Developer First**: A robust CLI (`ofa`) that handles everything from scaffolding to deployment.
21
21
  - **🔐 Enterprise Ready**: Built-in CSRF protection, secure session management, and input validation.
22
22
  - **🌐 Global Support**: Multi-language (I18n) support and SEO optimization ready.
23
+ - **🖋️ Rich Text Editor**: Integrated Trix Editor for seamless post and page creation with image upload support.
23
24
 
24
25
  ---
25
26
 
@@ -125,10 +126,13 @@ Securely manages admin credentials.
125
126
  ### 📁 Project Lifecycle
126
127
  | Command | Description |
127
128
  | :--- | :--- |
128
- | `ofa new NAME [TYPE]` | **Create a new project.** Generates a new directory, initializes the framework structure, and automatically runs `bundle install`. <br> *Example:* `./ofa new my_portfolio portfolio` |
129
+ | `ofa new NAME [TYPE]` | **Create a new project.** Generates a new directory, initializes the framework structure, and automatically runs `bundle install`. |
129
130
  | `ofa init [TYPE]` | **Initialize in current folder.** Ideal if you've already created a folder or cloned a repository. It triggers an **Interactive Wizard** to configure your Database (SQLite/MongoDB) and Image Storage (Local/Cloudinary). |
130
131
  | `ofa run` | **Start Development Server.** Boots the high-performance Eksa Server. Your app will be accessible at `http://localhost:3000`. |
131
- | `ofa deploy` | **Production Deployment.** Automatically detects deployment targets. <br> 1. Checks if it's a Git repository. <br> 2. Detects **Railway CLI** and triggers `railway up`. <br> 3. Supports Docker via the included `Dockerfile`. |
132
+ | `ofa console` | **Interactive REPL.** Starts a Ruby console pre-loaded with your application environment and models for testing and debugging. |
133
+ | `ofa doctor` | **System Health Check.** Validates `.env` config, database connectivity (SQL/MongoDB), Ruby version, and dependencies. |
134
+ | `ofa routes` | **Route Inspection.** Lists all registered routes in your application in a clean tabular format. |
135
+ | `ofa deploy` | **Production Deployment.** Automatically detects deployment targets (Railway/Docker/Git). |
132
136
 
133
137
  ---
134
138
 
@@ -151,7 +155,7 @@ Fine-tune your application's behavior and appearance without touching the code.
151
155
  | :--- | :--- |
152
156
  | `ofa type NAME` | **Set Application Type.** Switches the layout logic between `portfolio`, `blog`, `landing_page`, and `e_commerce`. |
153
157
  | `ofa theme NAME` | **Change UI Aesthetic.** Instantly swap between premium themes: <br> • `light_glass` / `dark_glass` (Modern Glassmorphism) <br> • `cyber_sidebar` (High-tech) <br> • `retro_terminal` (Old-school hacker vibe) <br> • `light_sidebar` (Professional/Clean) |
154
- | `ofa feature ACTION FEATURE`| **Toggle Core Features.** Enable or disable system modules. <br> *Usage:* `./ofa feature enable auth` or `./ofa feature disable cms`. |
158
+ | `ofa feature ACTION FEATURE`| **Toggle Core Features.** Enable or disable system modules. <br> *Usage:* `./ofa feature enable auth`, `enable cms`, or `enable rich_text`. |
155
159
  | `ofa storage NAME` | **Set Media Storage.** Choose between `local` (uploads folder) or `cloudinary` (Cloud storage). |
156
160
 
157
161
  ---
@@ -36,7 +36,7 @@
36
36
  <h3 class="text-2xl font-black mb-4 tracking-tight group-hover:text-primary transition-colors"><%= post.title %></h3>
37
37
 
38
38
  <div class="text-slate-500 dark:text-slate-400 text-sm leading-relaxed mb-6">
39
- <%= markdown(post.content.split("\n")[0..1].join("\n") + "...") %>
39
+ <%= preview_text(post.content, 180) %>
40
40
  </div>
41
41
 
42
42
  <div class="flex items-center gap-2 text-primary font-bold text-sm">
@@ -93,13 +93,20 @@
93
93
 
94
94
  <div class="space-y-2">
95
95
  <div class="flex justify-between items-center px-1">
96
- <label class="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Page Content (Markdown)</label>
97
- <button type="button" class="upload-helper-btn text-xs font-bold text-primary hover:underline flex items-center gap-2" onclick="document.getElementById('image-upload').click()">
98
- <i class="fas fa-image"></i> Insert Image
99
- </button>
96
+ <label class="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Page Content (<%= FEATURES_CONFIG['rich_text'] ? 'Rich Text' : 'Markdown' %>)</label>
97
+ <% unless FEATURES_CONFIG['rich_text'] %>
98
+ <button type="button" class="upload-helper-btn text-xs font-bold text-primary hover:underline flex items-center gap-2" onclick="document.getElementById('image-upload').click()">
99
+ <i class="fas fa-image"></i> Insert Image
100
+ </button>
101
+ <% end %>
100
102
  </div>
101
- <textarea name="page[content]" rows="12" placeholder="Write page content here..."
102
- class="w-full px-5 py-4 bg-white/5 border border-white/10 rounded-2xl focus:border-primary focus:ring-4 focus:ring-primary/10 outline-none transition-all text-slate-700 dark:text-slate-300 font-mono leading-relaxed"><%= @page.content %></textarea>
103
+ <% if FEATURES_CONFIG['rich_text'] %>
104
+ <input id="page_content" type="hidden" name="page[content]" value="<%= h(@page.content) %>">
105
+ <trix-editor input="page_content" placeholder="Tulis konten halaman di sini..." class="trix-content"></trix-editor>
106
+ <% else %>
107
+ <textarea name="page[content]" rows="12" placeholder="Write page content here..."
108
+ class="w-full px-5 py-4 bg-white/5 border border-white/10 rounded-2xl focus:border-primary focus:ring-4 focus:ring-primary/10 outline-none transition-all text-slate-700 dark:text-slate-300 font-mono leading-relaxed"><%= @page.content %></textarea>
109
+ <% end %>
103
110
  <input type="file" id="image-upload" class="hidden" onchange="handleImageUpload(this)">
104
111
  </div>
105
112
 
@@ -139,9 +146,14 @@
139
146
  });
140
147
  const data = await response.json();
141
148
  if (data.url) {
142
- const textarea = document.querySelector('textarea');
143
- const imgHtml = `\n<img src="${data.url}" alt="image" style="max-width: 100%; border-radius: 1rem; margin: 1rem 0;">\n`;
144
- textarea.value += imgHtml;
149
+ const trixEditor = document.querySelector('trix-editor');
150
+ if (trixEditor) {
151
+ trixEditor.editor.insertHTML(`<img src="${data.url}" style="max-width: 100%; border-radius: 1rem; margin: 1rem 0;">`);
152
+ } else {
153
+ const textarea = document.querySelector('textarea');
154
+ const imgHtml = `\n<img src="${data.url}" alt="image" style="max-width: 100%; border-radius: 1rem; margin: 1rem 0;">\n`;
155
+ textarea.value += imgHtml;
156
+ }
145
157
  alert('Image uploaded successfully!');
146
158
  } else {
147
159
  alert('Upload failed: ' + (data.error || 'Unknown error'));
@@ -94,14 +94,16 @@
94
94
 
95
95
  <div class="space-y-2">
96
96
  <div class="flex justify-between items-center px-1">
97
- <label class="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Article Content (Markdown)</label>
97
+ <label class="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Article Content (<%= FEATURES_CONFIG['rich_text'] ? 'Rich Text' : 'Markdown' %>)</label>
98
98
  <div class="flex gap-4">
99
99
  <button type="button" class="text-xs font-bold text-primary hover:underline flex items-center gap-2" onclick="openUploader('thumbnail')">
100
100
  <i class="fas fa-image"></i> Set Thumbnail
101
101
  </button>
102
- <button type="button" class="text-xs font-bold text-primary hover:underline flex items-center gap-2" onclick="openUploader('content')">
103
- <i class="fas fa-file-image"></i> Insert Image
104
- </button>
102
+ <% unless FEATURES_CONFIG['rich_text'] %>
103
+ <button type="button" class="text-xs font-bold text-primary hover:underline flex items-center gap-2" onclick="openUploader('content')">
104
+ <i class="fas fa-file-image"></i> Insert Image
105
+ </button>
106
+ <% end %>
105
107
  </div>
106
108
  </div>
107
109
 
@@ -111,8 +113,13 @@
111
113
  </div>
112
114
 
113
115
  <input type="hidden" name="post[image_url]" id="thumbnail-url" value="<%= @post.image_url %>">
114
- <textarea name="post[content]" rows="15" placeholder="Start writing your story..."
115
- class="w-full px-5 py-4 bg-white/5 border border-white/10 rounded-2xl focus:border-primary focus:ring-4 focus:ring-primary/10 outline-none transition-all text-slate-700 dark:text-slate-300 font-mono leading-relaxed text-left"><%= @post.content %></textarea>
116
+ <% if FEATURES_CONFIG['rich_text'] %>
117
+ <input id="post_content" type="hidden" name="post[content]" value="<%= h(@post.content) %>">
118
+ <trix-editor input="post_content" placeholder="Mulai menulis cerita Anda..." class="trix-content"></trix-editor>
119
+ <% else %>
120
+ <textarea name="post[content]" rows="15" placeholder="Start writing your story..."
121
+ class="w-full px-5 py-4 bg-white/5 border border-white/10 rounded-2xl focus:border-primary focus:ring-4 focus:ring-primary/10 outline-none transition-all text-slate-700 dark:text-slate-300 font-mono leading-relaxed text-left"><%= @post.content %></textarea>
122
+ <% end %>
116
123
  <input type="file" id="image-upload" class="hidden" onchange="handleImageUpload(this)">
117
124
  </div>
118
125
 
@@ -165,8 +172,13 @@
165
172
  preview.src = data.url;
166
173
  container.style.display = 'block';
167
174
  } else {
168
- const textarea = document.querySelector('textarea');
169
- textarea.value += `\n![image](${data.url})\n`;
175
+ const trixEditor = document.querySelector('trix-editor');
176
+ if (trixEditor) {
177
+ trixEditor.editor.insertHTML(`<img src="${data.url}" style="max-width: 100%; border-radius: 1rem; margin: 1rem 0;">`);
178
+ } else {
179
+ const textarea = document.querySelector('textarea');
180
+ textarea.value += `\n![image](${data.url})\n`;
181
+ }
170
182
  }
171
183
  alert('Image uploaded successfully! Don\'t forget to click PUBLISH/SAVE below.');
172
184
  } else {
data/app/views/docs.erb CHANGED
@@ -1,6 +1,6 @@
1
1
  <div class="space-y-12 pb-20">
2
2
  <div class="text-left">
3
- <div class="badge-premium">Documentation v4.0.0</div>
3
+ <div class="badge-premium">Documentation v4.2.0</div>
4
4
  <h1 class="text-5xl font-black tracking-tighter mb-4 text-slate-700 dark:text-white">Framework <span class="text-primary">Guide</span></h1>
5
5
  <p class="text-xl text-slate-500 max-w-2xl leading-relaxed">Everything you need to know about building premium web applications with the One-For-All framework.</p>
6
6
  </div>
@@ -62,6 +62,18 @@
62
62
  <td class="px-6 py-4 font-mono text-primary">./ofa run</td>
63
63
  <td class="px-6 py-4 text-slate-600 dark:text-slate-400">Starts the high-performance Eksa Server on port 3000.</td>
64
64
  </tr>
65
+ <tr>
66
+ <td class="px-6 py-4 font-mono text-primary">./ofa console</td>
67
+ <td class="px-6 py-4 text-slate-600 dark:text-slate-400 text-xs">Interactive REPL pre-loaded with your DB & Models.</td>
68
+ </tr>
69
+ <tr>
70
+ <td class="px-6 py-4 font-mono text-primary">./ofa doctor</td>
71
+ <td class="px-6 py-4 text-slate-600 dark:text-slate-400 text-xs">Check system health (DB, Env, Gems, Config).</td>
72
+ </tr>
73
+ <tr>
74
+ <td class="px-6 py-4 font-mono text-primary">./ofa routes</td>
75
+ <td class="px-6 py-4 text-slate-600 dark:text-slate-400 text-xs">List all registered routes in a clean table.</td>
76
+ </tr>
65
77
  <tr>
66
78
  <td class="px-6 py-4 font-mono text-primary">./ofa deploy</td>
67
79
  <td class="px-6 py-4 text-slate-600 dark:text-slate-400 text-xs italic">Railway/Docker/Git auto-detection for production.</td>
@@ -136,6 +148,14 @@
136
148
  <code class="text-primary font-bold text-xs">./ofa run</code>
137
149
  <p class="text-slate-500 text-xs mt-1">Starts the high-performance Eksa Server on port 3000.</p>
138
150
  </div>
151
+ <div>
152
+ <code class="text-primary font-bold text-xs">./ofa console</code>
153
+ <p class="text-slate-500 text-xs mt-1">Interactive REPL session.</p>
154
+ </div>
155
+ <div>
156
+ <code class="text-primary font-bold text-xs">./ofa doctor</code>
157
+ <p class="text-slate-500 text-xs mt-1">System health check tool.</p>
158
+ </div>
139
159
  </div>
140
160
  </div>
141
161
 
@@ -249,6 +269,17 @@ resources :posts
249
269
  ./ofa feature enable cms
250
270
  </div>
251
271
  <p>Authentication is handled via BCrypt hashing with an 8-hour session sliding expiration window for optimal security.</p>
272
+
273
+ <h4 class="font-bold text-lg mt-8 mb-4">🖋️ Rich Text Editor (Trix)</h4>
274
+ <p class="text-sm text-slate-500 mb-4">Version 4.1.0 introduced a premium WYSIWYG editor integration. Enable it to transform your CMS experience:</p>
275
+ <div class="p-4 bg-white/5 border border-white/10 rounded-2xl mb-4 font-mono text-sm">
276
+ ./ofa feature enable rich_text
277
+ </div>
278
+ <ul class="text-xs space-y-2 text-slate-600 dark:text-slate-400">
279
+ <li>• <strong>Visual Editing:</strong> No more Markdown syntax. Format text, quotes, and lists visually.</li>
280
+ <li>• <strong>Image Handling:</strong> Drag-and-drop images directly into the editor with auto-storage sync.</li>
281
+ <li>• <strong>Code Blocks:</strong> Native support for code blocks with one-click "Copy" functionality.</li>
282
+ </ul>
252
283
  </div>
253
284
  </section>
254
285
 
data/app/views/index.erb CHANGED
@@ -1,6 +1,6 @@
1
1
  <div class="text-center space-y-6 max-w-2xl mx-auto py-12">
2
2
  <div class="inline-flex items-center px-4 py-1.5 rounded-full bg-primary/10 border border-primary/20 text-primary text-xs font-bold uppercase tracking-wider animate-pulse">
3
- Framework Version 4.0.0
3
+ Framework Version 4.2.0
4
4
  </div>
5
5
 
6
6
  <h1 class="text-5xl md:text-7xl font-black tracking-tight leading-tight">
data/app/views/layout.erb CHANGED
@@ -21,6 +21,80 @@
21
21
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
22
22
  <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-ruby.min.js"></script>
23
23
 
24
+ <% if FEATURES_CONFIG['rich_text'] %>
25
+ <link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css">
26
+ <script type="text/javascript" src="https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js"></script>
27
+ <style>
28
+ trix-editor {
29
+ min-height: 400px !important;
30
+ background: rgba(255, 255, 255, 0.03) !important;
31
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
32
+ border-radius: 1.25rem !important;
33
+ padding: 1.25rem !important;
34
+ color: inherit !important;
35
+ font-family: inherit !important;
36
+ line-height: 1.6 !important;
37
+ }
38
+ trix-toolbar {
39
+ margin-bottom: 0.5rem !important;
40
+ }
41
+ .dark trix-toolbar .trix-button-group {
42
+ border-color: rgba(255, 255, 255, 0.1) !important;
43
+ background: rgba(255, 255, 255, 0.05) !important;
44
+ }
45
+ .dark trix-toolbar .trix-button {
46
+ border-bottom: none !important;
47
+ }
48
+ .dark trix-toolbar .trix-button--active {
49
+ background: var(--primary) !important;
50
+ }
51
+ .trix-content {
52
+ font-size: 1.1rem !important;
53
+ }
54
+ </style>
55
+ <script type="text/javascript">
56
+ (function() {
57
+ document.addEventListener("trix-attachment-add", function(event) {
58
+ var attachment = event.attachment;
59
+ if (attachment.file) {
60
+ return uploadFileAttachment(attachment);
61
+ }
62
+ });
63
+
64
+ function uploadFileAttachment(attachment) {
65
+ var file = attachment.file;
66
+ var form = new FormData();
67
+ form.append("file", file);
68
+ form.append("csrf_token", "<%= csrf_token %>");
69
+
70
+ var xhr = new XMLHttpRequest();
71
+ xhr.open("POST", "/dashboard/upload", true);
72
+
73
+ xhr.upload.onprogress = function(event) {
74
+ var progress = (event.loaded / event.total) * 100;
75
+ attachment.setUploadProgress(progress);
76
+ };
77
+
78
+ xhr.onload = function() {
79
+ if (xhr.status === 200) {
80
+ try {
81
+ var data = JSON.parse(xhr.responseText);
82
+ if (data.url) {
83
+ return attachment.setAttributes({
84
+ url: data.url,
85
+ href: data.url
86
+ });
87
+ }
88
+ } catch (e) {}
89
+ }
90
+ };
91
+
92
+ return xhr.send(form);
93
+ }
94
+ })();
95
+ </script>
96
+ <% end %>
97
+
24
98
  <script>
25
99
  tailwind.config = {
26
100
  darkMode: 'class',
@@ -215,7 +289,56 @@
215
289
  .markdown-content { text-align: left; line-height: 1.6; }
216
290
  .markdown-content h1 { font-size: 2.25rem; font-weight: 800; margin: 2rem 0 1rem; color: var(--primary); }
217
291
  .markdown-content p { font-size: 1.125rem; color: #475569; margin-bottom: 1.5rem; }
292
+ .markdown-content ul { list-style-type: disc !important; margin-left: 1.5rem !important; margin-bottom: 1.5rem !important; display: block !important; }
293
+ .markdown-content ol { list-style-type: decimal !important; margin-left: 1.5rem !important; margin-bottom: 1.5rem !important; display: block !important; }
294
+ .markdown-content li { margin-bottom: 0.5rem !important; display: list-item !important; }
295
+ .markdown-content blockquote { border-left: 4px solid var(--primary); padding: 1rem 0 1rem 1.5rem; font-style: italic; margin: 1.5rem 0; color: #64748b; background: rgba(255, 255, 255, 0.03); }
296
+ .markdown-content pre { background: rgba(0,0,0,0.05); padding: 1.25rem; border-radius: 1rem; overflow-x: auto; font-family: 'Fira Code', monospace; margin: 1.5rem 0; line-height: 1.4; border: 1px solid rgba(0,0,0,0.1); }
297
+ .markdown-content code { background: rgba(0,0,0,0.05); padding: 0.2rem 0.4rem; border-radius: 0.4rem; font-family: 'Fira Code', monospace; font-size: 0.9em; }
298
+ .markdown-content figcaption { text-align: center; color: #64748b; opacity: 0.6; font-size: 0.85rem; margin-top: 0.75rem; font-style: italic; line-height: 1.4; }
299
+ .markdown-content a { color: var(--primary); font-weight: 700; text-decoration: none; border-bottom: 1px dashed var(--primary); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
300
+ .markdown-content a:hover { color: var(--secondary); border-bottom: 1px solid var(--secondary); opacity: 0.8; }
301
+
218
302
  .dark .markdown-content p { color: #cbd5e1; }
303
+ .dark .markdown-content ul, .dark .markdown-content ol { color: #cbd5e1; }
304
+ .dark .markdown-content blockquote { color: #94a3b8; background: rgba(255, 255, 255, 0.03); }
305
+ .dark .markdown-content pre { background: rgba(255,255,255,0.03); border-color: rgba(255,255,255,0.1); color: #e2e8f0; }
306
+ .dark .markdown-content code { background: rgba(255,255,255,0.1); color: var(--primary); }
307
+
308
+ /* Code Copy Button */
309
+ .code-wrapper { position: relative; margin: 1.5rem 0; }
310
+ .copy-btn {
311
+ position: absolute;
312
+ top: 0.75rem;
313
+ right: 0.75rem;
314
+ padding: 0.4rem 0.8rem;
315
+ background: rgba(255, 255, 255, 0.05);
316
+ border: 1px solid rgba(255, 255, 255, 0.1);
317
+ border-radius: 0.75rem;
318
+ color: #94a3b8;
319
+ font-size: 0.7rem;
320
+ font-weight: 800;
321
+ cursor: pointer;
322
+ transition: all 0.3s;
323
+ backdrop-filter: blur(8px);
324
+ z-index: 10;
325
+ text-transform: uppercase;
326
+ letter-spacing: 0.05em;
327
+ }
328
+ .copy-btn:hover {
329
+ background: var(--primary);
330
+ color: white;
331
+ border-color: var(--primary);
332
+ transform: translateY(-2px);
333
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
334
+ }
335
+ .copy-btn.copied {
336
+ background: #10b981;
337
+ color: white;
338
+ border-color: #10b981;
339
+ }
340
+
341
+ .markdown-content pre { margin: 0 !important; }
219
342
 
220
343
  /* Cyber Theme Specifics */
221
344
  .cyber-border {
@@ -523,6 +646,34 @@
523
646
  menuBtn.addEventListener('click', toggleMenu);
524
647
  menuClose.addEventListener('click', toggleMenu);
525
648
  backdrop.addEventListener('click', toggleMenu);
649
+
650
+ // Code Copy Implementation
651
+ document.querySelectorAll('pre').forEach(pre => {
652
+ // Skip if already wrapped or inside trix-editor
653
+ if (pre.closest('trix-editor') || pre.parentElement.classList.contains('code-wrapper')) return;
654
+
655
+ const wrapper = document.createElement('div');
656
+ wrapper.className = 'code-wrapper group';
657
+ pre.parentNode.insertBefore(wrapper, pre);
658
+ wrapper.appendChild(pre);
659
+
660
+ const btn = document.createElement('button');
661
+ btn.className = 'copy-btn opacity-0 group-hover:opacity-100 transition-opacity duration-300';
662
+ btn.innerHTML = '<i class="fas fa-copy mr-1"></i> Copy';
663
+ wrapper.appendChild(btn);
664
+
665
+ btn.addEventListener('click', () => {
666
+ const code = pre.innerText;
667
+ navigator.clipboard.writeText(code).then(() => {
668
+ btn.innerHTML = '<i class="fas fa-check mr-1"></i> Copied!';
669
+ btn.classList.add('copied');
670
+ setTimeout(() => {
671
+ btn.innerHTML = '<i class="fas fa-copy mr-1"></i> Copy';
672
+ btn.classList.remove('copied');
673
+ }, 2000);
674
+ });
675
+ });
676
+ });
526
677
  });
527
678
  </script>
528
679
  </body>
data/bin/ofa CHANGED
@@ -22,7 +22,7 @@ def help
22
22
  puts " / __ \\/ ____/ / | "
23
23
  puts " / / / / /_ / /| | Framework "
24
24
  puts "/ /_/ / __/ / ___ | Premium MVC "
25
- puts "\\____/_/ /_/ |_| v4.0.0 "
25
+ puts "\\____/_/ /_/ |_| v4.2.0 "
26
26
  puts " "
27
27
  puts "✨ One-For-All Framework CLI ✨"
28
28
  puts "-----------------------------"
@@ -33,7 +33,7 @@ def help
33
33
  puts " ofa g model NAME - Generate a new model"
34
34
  puts " ofa g migration NAME - Generate a new migration"
35
35
  puts " ofa g post TITLE - Create a new post [args: --category, --author, --image]"
36
- puts " ofa feature ACTION F - Toggle features: action=enable/disable, F=cms/auth"
36
+ puts " ofa feature ACTION F - Toggle features: action=enable/disable, F=cms/auth/rich_text"
37
37
  puts " ofa type NAME - Set application type: portfolio, blog, landing_page, e_commerce"
38
38
  puts " ofa theme NAME - Set UI theme: light_glass, dark_glass, cyber_sidebar, retro_terminal, light_sidebar"
39
39
  puts " ofa storage NAME - Set image storage: local, cloudinary"
@@ -42,6 +42,9 @@ def help
42
42
  puts " ofa db migrate-data TYPE [NAME] - Migrate data to another DB"
43
43
  puts " ofa db migrate - Run database migrations"
44
44
  puts " ofa migrate - Run database migrations (alias for db migrate)"
45
+ puts " ofa console - Start interactive REPL session"
46
+ puts " ofa doctor - Check system health (DB, config, gems)"
47
+ puts " ofa routes - List all registered routes"
45
48
  puts " ofa run - Start the application server"
46
49
  puts " ofa deploy - Deploy project to production"
47
50
  puts "-----------------------------"
@@ -226,7 +229,7 @@ when 'init'
226
229
  puts " / __ \\/ ____/ / | "
227
230
  puts " / / / / /_ / /| | Framework "
228
231
  puts "/ /_/ / __/ / ___ | Premium MVC "
229
- puts "\\____/_/ /_/ |_| v4.0.0 "
232
+ puts "\\____/_/ /_/ |_| v4.2.0 "
230
233
  puts " "
231
234
  puts "Initializing One-For-All project as '#{app_type}' in #{PROJECT_ROOT}..."
232
235
 
@@ -513,6 +516,12 @@ when 'feature'
513
516
  config[feature] = (action == 'enable')
514
517
  File.write(config_path, JSON.pretty_generate(config))
515
518
  puts "Feature '#{feature}' has been #{action}d."
519
+
520
+ if action == 'enable' && feature == 'cms' && !config['auth']
521
+ puts "\n⚠️ [WARNING]: You have enabled CMS but 'auth' is disabled."
522
+ puts " It is highly recommended to enable 'auth' feature to secure your dashboard."
523
+ puts " Run: ./ofa feature enable auth"
524
+ end
516
525
 
517
526
  when 'type'
518
527
  ensure_initialized!
@@ -622,6 +631,111 @@ when 'db'
622
631
  perform_db_migration(target_type, target_name)
623
632
  end
624
633
 
634
+ when 'doctor'
635
+ puts "🩺 One-For-All Doctor - System Health Check"
636
+ puts "------------------------------------------"
637
+
638
+ # 1. Environment Check
639
+ print "Checking .env file... "
640
+ if File.exist?('.env')
641
+ puts "✅ Found."
642
+ env_content = File.read('.env')
643
+ if env_content.include?('DATABASE_URL')
644
+ puts " - DATABASE_URL: Defined"
645
+ end
646
+ else
647
+ puts "❌ Missing! Create a .env file based on .env.example."
648
+ end
649
+
650
+ # 2. Database Check
651
+ print "Checking Database connection... "
652
+ begin
653
+ Object.const_set(:APP_ROOT, PROJECT_ROOT) unless defined?(APP_ROOT)
654
+ ENV['EKS_ENV'] = 'test' # Silence some output
655
+ require File.join(FRAMEWORK_ROOT, 'config', 'boot')
656
+ if defined?(DB) && DB.test_connection
657
+ puts "✅ Connected (#{DB_CONFIG['adapter']})"
658
+ else
659
+ puts "❌ Failed!"
660
+ end
661
+
662
+ if defined?(MONGO_CLIENT)
663
+ print "Checking MongoDB connection... "
664
+ begin
665
+ MONGO_CLIENT.database_names
666
+ puts "✅ Connected"
667
+ rescue => e
668
+ puts "❌ Failed: #{e.message}"
669
+ end
670
+ end
671
+ rescue => e
672
+ puts "❌ Error during boot: #{e.message}"
673
+ end
674
+
675
+ # 3. Ruby Version
676
+ print "Checking Ruby version... "
677
+ puts "✅ #{RUBY_VERSION}"
678
+
679
+ # 4. Gems Check
680
+ print "Checking dependencies... "
681
+ if File.exist?('Gemfile.lock')
682
+ puts "✅ Gemfile.lock exists."
683
+ else
684
+ puts "⚠️ Gemfile.lock not found. Run 'bundle install'."
685
+ end
686
+
687
+ # 5. Features Config
688
+ print "Checking features.json... "
689
+ if File.exist?('config/features.json')
690
+ puts "✅ Found."
691
+ else
692
+ puts "❌ Missing! Run 'ofa init'."
693
+ end
694
+
695
+ puts "------------------------------------------"
696
+ puts "Doctor check complete! ✨"
697
+
698
+ when 'console'
699
+ ensure_initialized!
700
+ puts " "
701
+ puts " ____ _______ ___ "
702
+ puts " / __ \\/ ____/ / | "
703
+ puts " / / / / /_ / /| | Framework "
704
+ puts "/ /_/ / __/ / ___ | Console (REPL) "
705
+ puts "\\____/_/ /_/ |_| v4.2.0 "
706
+ puts " "
707
+ puts "✨ Loading environment... (Type 'exit' to quit)"
708
+
709
+ Object.const_set(:APP_ROOT, PROJECT_ROOT) unless defined?(APP_ROOT)
710
+ require File.join(FRAMEWORK_ROOT, 'config', 'boot')
711
+
712
+ require 'irb'
713
+ ARGV.clear
714
+ IRB.start
715
+
716
+ when 'routes'
717
+ ensure_initialized!
718
+ puts "🗺️ Registered Routes"
719
+ puts "--------------------------------------------------------"
720
+ printf "%-8s | %-40s\n", "Verb", "Path"
721
+ puts "--------------------------------------------------------"
722
+
723
+ Object.const_set(:APP_ROOT, PROJECT_ROOT) unless defined?(APP_ROOT)
724
+ ENV['EKS_ENV'] = 'test'
725
+ require File.join(FRAMEWORK_ROOT, 'config', 'boot')
726
+
727
+ if defined?(ROUTES)
728
+ routes_hash = ROUTES.instance_variable_get(:@routes)
729
+ routes_hash.each do |verb, list|
730
+ list.each do |r|
731
+ printf "%-8s | %-40s\n", verb, r[:path]
732
+ end
733
+ end
734
+ else
735
+ puts "❌ No routes found."
736
+ end
737
+ puts "--------------------------------------------------------"
738
+
625
739
  when 'run'
626
740
  ensure_initialized!
627
741
  # Check for config.ru
data/config/boot.rb CHANGED
@@ -47,6 +47,14 @@ module EksCent
47
47
  context.define_singleton_method(:csrf_tag) { "<input type='hidden' name='csrf_token' value='#{req.env['eks_cent.csrf_token']}'>" }
48
48
  context.define_singleton_method(:csrf_token) { req ? req.env['eks_cent.csrf_token'] : nil }
49
49
  context.define_singleton_method(:markdown) { |text| Kramdown::Document.new(text.to_s, input: 'GFM').to_html }
50
+ context.define_singleton_method(:strip_tags) { |text| text.to_s.gsub(/<[^>]*>/, ' ').gsub(/\s+/, ' ').strip }
51
+ context.define_singleton_method(:preview_text) do |text, length = 160|
52
+ plain = text.to_s.gsub(/<[^>]*>/, ' ') # Hapus HTML tags
53
+ plain = plain.gsub(/!\[.*?\]\(.*?\)/, '') # Hapus Markdown Images
54
+ plain = plain.gsub(/\[(.*?)\]\(.*?\)/, '\1') # Ambil teks dari Markdown Links
55
+ plain = plain.gsub(/\s+/, ' ').strip
56
+ plain.length > length ? "#{plain[0...length]}..." : plain
57
+ end
50
58
 
51
59
  context.define_singleton_method(:session) { req ? (req.env['eks_cent.session'] || req.env['rack.session'] || {}) : {} }
52
60
  context.define_singleton_method(:h) { |s| CGI.escapeHTML(s.to_s) }
data/config/features.json CHANGED
@@ -3,5 +3,6 @@
3
3
  "cms": true,
4
4
  "type": "landing_page",
5
5
  "theme": "light_sidebar",
6
- "storage": "cloudinary"
6
+ "storage": "cloudinary",
7
+ "rich_text": true
7
8
  }
data/config/routes.rb CHANGED
@@ -52,31 +52,35 @@ ROUTES = EksCent::Router.new do
52
52
  end
53
53
 
54
54
  # --- CMS Dashboard ---
55
- get '/dashboard' do |req, res|
56
- DashboardController.new(req, res).index
57
- end
58
-
59
- post '/dashboard/upload' do |req, res|
60
- DashboardController.new(req, res).upload
61
- end
55
+ if FEATURES_CONFIG['cms']
56
+ get '/dashboard' do |req, res|
57
+ DashboardController.new(req, res).index
58
+ end
59
+
60
+ post '/dashboard/upload' do |req, res|
61
+ DashboardController.new(req, res).upload
62
+ end
62
63
 
63
- # Resourceful CMS Routes
64
- resources :pages, prefix: '/dashboard'
65
- resources :posts, prefix: '/dashboard'
66
- resources :projects, prefix: '/dashboard'
67
- resources :products, prefix: '/dashboard'
64
+ # Resourceful CMS Routes
65
+ resources :pages, prefix: '/dashboard'
66
+ resources :posts, prefix: '/dashboard'
67
+ resources :projects, prefix: '/dashboard'
68
+ resources :products, prefix: '/dashboard'
69
+ end
68
70
 
69
71
  # Auth Routes
70
- get '/login' do |req, res|
71
- AuthController.new(req, res).show_login
72
- end
72
+ if FEATURES_CONFIG['auth']
73
+ get '/login' do |req, res|
74
+ AuthController.new(req, res).show_login
75
+ end
73
76
 
74
- post '/login' do |req, res|
75
- AuthController.new(req, res).login
76
- end
77
+ post '/login' do |req, res|
78
+ AuthController.new(req, res).login
79
+ end
77
80
 
78
- post '/logout' do |req, res|
79
- AuthController.new(req, res).logout
81
+ post '/logout' do |req, res|
82
+ AuthController.new(req, res).logout
83
+ end
80
84
  end
81
85
 
82
86
  # API Namespace
data/db/data.sqlite3 CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: one-for-all-framework
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ishikawa Uta
@@ -180,6 +180,7 @@ extensions: []
180
180
  extra_rdoc_files: []
181
181
  files:
182
182
  - ".env.example"
183
+ - ".gitignore"
183
184
  - Dockerfile
184
185
  - Gemfile
185
186
  - LICENSE