one-for-all-framework 4.0.0 → 4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6010d541e08ee4d29882dbfe22d000853a511985f01ee3b18239c57086bd7906
4
- data.tar.gz: d05cdc0c5df07e8f2d9c0f3715169a3817b04b34da654dc63ce8afbaba0b932a
3
+ metadata.gz: a5483a78bd78c4dcf46a9866464a4888bec73705273fe2db6a4b58179429b685
4
+ data.tar.gz: 8423d7b463a98b56f470aeeb244f4f68e4cd38ca9f0cf53fcf1b14f84e57c599
5
5
  SHA512:
6
- metadata.gz: 4b84becdb59e23e5177cd6d61b33ae6875cbcdc00a1ab81fe98c4e644c97e1549950cdeef4da11758564cf316d009f69bd69b431d33852e9384383294126acbb
7
- data.tar.gz: 45544d4a74c6636667674f46b3ede29f4bde1e7a5e14e3f0d9385450b03676e76c4f538169386255d8f5ed1a1c70f971f65c3a88bdb948b6ace802ca8bc05170
6
+ metadata.gz: 692a5ab6721f594764df4d47f6b39e4b1c5c863d78ce9e4f548523ca4239392a2be9d5fa6db851df8112eea0cb11ce58198409a4d6f2bbc8b625662c1d9f375e
7
+ data.tar.gz: f7c5aa734237704dd51efd801334d5c576e5cfb4e7fd257e5c905b5a8dde492faa63a033c11c08a6ede41c0cb9639b54e2fe97961ef4e167d568dee1c0152e8a
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.1.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
 
@@ -151,7 +152,7 @@ Fine-tune your application's behavior and appearance without touching the code.
151
152
  | :--- | :--- |
152
153
  | `ofa type NAME` | **Set Application Type.** Switches the layout logic between `portfolio`, `blog`, `landing_page`, and `e_commerce`. |
153
154
  | `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`. |
155
+ | `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
156
  | `ofa storage NAME` | **Set Media Storage.** Choose between `local` (uploads folder) or `cloudinary` (Cloud storage). |
156
157
 
157
158
  ---
@@ -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.1.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>
@@ -249,6 +249,17 @@ resources :posts
249
249
  ./ofa feature enable cms
250
250
  </div>
251
251
  <p>Authentication is handled via BCrypt hashing with an 8-hour session sliding expiration window for optimal security.</p>
252
+
253
+ <h4 class="font-bold text-lg mt-8 mb-4">🖋️ Rich Text Editor (Trix)</h4>
254
+ <p class="text-sm text-slate-500 mb-4">Version 4.1.0 introduces a premium WYSIWYG editor integration. Enable it to transform your CMS experience:</p>
255
+ <div class="p-4 bg-white/5 border border-white/10 rounded-2xl mb-4 font-mono text-sm">
256
+ ./ofa feature enable rich_text
257
+ </div>
258
+ <ul class="text-xs space-y-2 text-slate-600 dark:text-slate-400">
259
+ <li>• <strong>Visual Editing:</strong> No more Markdown syntax. Format text, quotes, and lists visually.</li>
260
+ <li>• <strong>Image Handling:</strong> Drag-and-drop images directly into the editor with auto-storage sync.</li>
261
+ <li>• <strong>Code Blocks:</strong> Native support for code blocks with one-click "Copy" functionality.</li>
262
+ </ul>
252
263
  </div>
253
264
  </section>
254
265
 
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.1.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.1.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"
@@ -226,7 +226,7 @@ when 'init'
226
226
  puts " / __ \\/ ____/ / | "
227
227
  puts " / / / / /_ / /| | Framework "
228
228
  puts "/ /_/ / __/ / ___ | Premium MVC "
229
- puts "\\____/_/ /_/ |_| v4.0.0 "
229
+ puts "\\____/_/ /_/ |_| v4.1.0 "
230
230
  puts " "
231
231
  puts "Initializing One-For-All project as '#{app_type}' in #{PROJECT_ROOT}..."
232
232
 
@@ -513,6 +513,12 @@ when 'feature'
513
513
  config[feature] = (action == 'enable')
514
514
  File.write(config_path, JSON.pretty_generate(config))
515
515
  puts "Feature '#{feature}' has been #{action}d."
516
+
517
+ if action == 'enable' && feature == 'cms' && !config['auth']
518
+ puts "\n⚠️ [WARNING]: You have enabled CMS but 'auth' is disabled."
519
+ puts " It is highly recommended to enable 'auth' feature to secure your dashboard."
520
+ puts " Run: ./ofa feature enable auth"
521
+ end
516
522
 
517
523
  when 'type'
518
524
  ensure_initialized!
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.1.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