one-for-all-framework 3.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: 26eab5d2f3279aef8aaf3e4f4f69af60d01c535a8a4ba2f41a82615f77e02732
4
- data.tar.gz: a6760ad95277f885fff52a45427049cb87f3a59e584dc09fffa533c4b7b57bd5
3
+ metadata.gz: a5483a78bd78c4dcf46a9866464a4888bec73705273fe2db6a4b58179429b685
4
+ data.tar.gz: 8423d7b463a98b56f470aeeb244f4f68e4cd38ca9f0cf53fcf1b14f84e57c599
5
5
  SHA512:
6
- metadata.gz: ac089f1a856bf5b31ffea2561db5a48d2fed567b14be86245c7962965595d9fb80209472d2427a4c10c4f15d17ed48c839897bb3141d0a46f860f43d7014d44b
7
- data.tar.gz: 9f10a41d2e81293e8cfd363253d08fbb1bd1dc595e97cc646b31ce2c36e30807ee385772c9ba903e98e70bec3205e5b265f0a694cdc3d62aaba12e488639eed0
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 v3.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)
@@ -16,10 +16,11 @@
16
16
 
17
17
  - **💎 Premium Aesthetics**: Beautiful Glassmorphism design system included by default with smooth dark/light mode transitions.
18
18
  - **🚀 Blazing Fast**: Built on a modular Nio4r-powered engine for minimal overhead and instant boot times.
19
- - **📂 Multi-Database**: Seamlessly switch between SQLite, MySQL, MariaDB, and MongoDB Atlas.
19
+ - **📂 Multi-Database**: Seamlessly switch and migrate data between SQLite, MySQL, MariaDB, and MongoDB Atlas.
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
 
@@ -52,9 +53,75 @@ Your app is now live at `http://localhost:3000` ⚡
52
53
 
53
54
  ---
54
55
 
55
- ## 🛠️ CLI Power Tools (Detailed Reference)
56
+ ## 🛠️ CLI Deep Dive & Expected Outputs
57
+
58
+ The `ofa` CLI is designed to be interactive and informative. Below is a detailed breakdown of all available commands and the visual feedback they provide.
59
+
60
+ ### 📦 Project & Environment
61
+ #### `ofa init [TYPE]`
62
+ Initialize your workspace. Triggers an interactive wizard for Database and Storage setup.
63
+ * **Output Example:**
64
+ ```text
65
+ 🛠️ Project Configuration
66
+ 💾 Choose Database [1. SQLite, 2. MongoDB Atlas]: 2
67
+ 🔗 Enter MongoDB Connection String: mongodb+srv://...
68
+ 🖼️ Choose Image Storage [1. Local, 2. Cloudinary]: 1
69
+ ✅ Connection string saved to .env
70
+ ✅ Project structure initialized.
71
+ ```
72
+
73
+ #### `ofa run`
74
+ Boots the high-performance Eksa Server engine.
75
+ * **Output Example:**
76
+ ```text
77
+ Starting One-For-All server...
78
+ [INFO] Mendengarkan di TCP: 0.0.0.0:3000
79
+ [EksCent] 2026-05-03 14:40:52 | GET / | Status: 200
80
+ ```
81
+
82
+ ### 🏗️ Generators (Scaffolding)
83
+ #### `ofa g controller NAME`
84
+ * **Output:** `✅ Created app/controllers/blog_controller.rb`
85
+ #### `ofa g model NAME`
86
+ * **Output:** `✅ Created app/models/product.rb`
87
+ #### `ofa g post TITLE [args]`
88
+ Creates a SEO-optimized blog post with metadata.
89
+ * **Example:** `./ofa g post "Hello World" --author Antigravity`
90
+ * **Output:** `✅ Created app/views/posts/hello_world.erb`
91
+
92
+ ### 📂 Database Management
93
+ #### `ofa db switch TYPE [URL]`
94
+ Switches your database adapter on the fly.
95
+ * **Output:** `Switched to mongodb mode.`
96
+
97
+ #### `ofa db migrate-data TYPE [URL]`
98
+ **The most powerful tool.** Moves all data from your current DB to a new one (SQL or MongoDB Atlas).
99
+ * **Output Example:**
100
+ ```text
101
+ 📦 Starting data migration: sqlite -> mongodb...
102
+ Migrating users... 12 rows.
103
+ Migrating posts... 45 rows.
104
+ ✅ Migration successful!
105
+ ```
106
+
107
+ #### `ofa db migrate` (or `ofa migrate`)
108
+ Runs pending database migrations in `db/migrations/`.
109
+ * **Output:** `✅ Migrations completed.`
110
+
111
+ ### 🎨 Customization & Security
112
+ #### `ofa theme NAME`
113
+ Changes the entire UI skin (Modern Glass, Retro, Cyber).
114
+ * **Output:** `✅ Theme set to: retro_terminal`
115
+
116
+ #### `ofa storage NAME`
117
+ Switches between local disk and Cloudinary cloud storage.
118
+ * **Output:** `✅ Storage set to: cloudinary`
119
+
120
+ #### `ofa reset-password USR PWD`
121
+ Securely manages admin credentials.
122
+ * **Output:** `✅ Password for 'admin' updated successfully.`
56
123
 
57
- The `ofa` CLI is the heart of the One-For-All framework. It handles everything from project initialization to production deployment.
124
+ ---
58
125
 
59
126
  ### 📁 Project Lifecycle
60
127
  | Command | Description |
@@ -85,7 +152,7 @@ Fine-tune your application's behavior and appearance without touching the code.
85
152
  | :--- | :--- |
86
153
  | `ofa type NAME` | **Set Application Type.** Switches the layout logic between `portfolio`, `blog`, `landing_page`, and `e_commerce`. |
87
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) |
88
- | `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`. |
89
156
  | `ofa storage NAME` | **Set Media Storage.** Choose between `local` (uploads folder) or `cloudinary` (Cloud storage). |
90
157
 
91
158
  ---
@@ -93,9 +160,10 @@ Fine-tune your application's behavior and appearance without touching the code.
93
160
  ### 🔐 Security & Database
94
161
  | Command | Description |
95
162
  | :--- | :--- |
96
- | `ofa reset-password USR PWD`| **User Management.** Resets a password for an existing admin or creates a new one. <br> *Note:* Enforces strong password rules (8+ chars, 1 uppercase, 1 number). |
97
- | `ofa db switch ADAPTER` | **Hot-swap Database.** Configure your adapter on the fly: `sqlite`, `mysql`, `mariadb`, `postgres`, or `env` (for MongoDB Atlas). |
98
- | `ofa db migrate` | **Database Sync.** Runs all pending migrations in `db/migrations/` to keep your schema up to date. |
163
+ | `ofa reset-password USR PWD`| **Enterprise Recovery.** Resets admin credentials with strict enforcement: 8+ chars, uppercase, and numbers. Powered by BCrypt hashing. |
164
+ | `ofa db migrate-data TYPE [NAME]`| **Zero-Loss Migration.** The ultimate tool to move data (Users, Posts, Products) from SQLite to MongoDB Atlas or other SQL DBs without losing a single record. |
165
+ | `ofa db switch ADAPTER` | **Hot-swap Engine.** Instantly change your database adapter. Supports `sqlite`, `mysql`, `mariadb`, `postgres`, and `mongodb`. |
166
+ | `ofa db migrate` | **Schema Evolution.** Runs all pending migrations. OFA automatically handles table creation for core models during boot for maximum reliability. |
99
167
 
100
168
  ---
101
169
 
@@ -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 v3.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>
@@ -94,6 +94,27 @@
94
94
  <td class="px-6 py-4 font-mono text-primary">./ofa storage NAME</td>
95
95
  <td class="px-6 py-4 text-slate-600 dark:text-slate-400 text-xs">Hot-swap storage: <code>local</code> or <code>cloudinary</code>.</td>
96
96
  </tr>
97
+ <tr class="bg-slate-500/5"><td colspan="2" class="px-6 py-2 text-[10px] font-black uppercase tracking-tighter text-slate-400">Database & Security</td></tr>
98
+ <tr>
99
+ <td class="px-6 py-4 font-mono text-primary">./ofa db migrate-data TYPE [NAME]</td>
100
+ <td class="px-6 py-4 text-slate-600 dark:text-slate-400 text-xs">
101
+ Migrate all data from current DB to a new destination (SQL/Mongo).
102
+ <div class="mt-2 p-2 bg-slate-900 rounded font-mono text-[10px] text-green-400 border border-white/5">
103
+ 📦 Starting data migration: sqlite -> mongo...<br>
104
+ Migrating users... 12 rows.<br>
105
+ ✅ Migration successful!
106
+ </div>
107
+ </td>
108
+ </tr>
109
+ <tr>
110
+ <td class="px-6 py-4 font-mono text-primary">./ofa reset-password USR PWD</td>
111
+ <td class="px-6 py-4 text-slate-600 dark:text-slate-400 text-xs">
112
+ Reset admin password (min 8 chars, 1 upper, 1 num).
113
+ <div class="mt-2 p-2 bg-slate-900 rounded font-mono text-[10px] text-green-400 border border-white/5">
114
+ ✅ Password for 'admin' updated successfully.
115
+ </div>
116
+ </td>
117
+ </tr>
97
118
  </tbody>
98
119
  </table>
99
120
  </div>
@@ -145,6 +166,20 @@
145
166
  </div>
146
167
  </div>
147
168
  </div>
169
+
170
+ <div class="glass-panel p-6 space-y-4">
171
+ <h4 class="text-[10px] font-black uppercase tracking-widest text-primary border-b border-white/5 pb-2">Database & Security</h4>
172
+ <div class="space-y-4">
173
+ <div>
174
+ <code class="text-primary font-bold text-xs">./ofa db migrate-data TYPE [NAME]</code>
175
+ <p class="text-slate-500 text-xs mt-1">Migrate data to new DB destination.</p>
176
+ </div>
177
+ <div>
178
+ <code class="text-primary font-bold text-xs">./ofa reset-password USR PWD</code>
179
+ <p class="text-slate-500 text-xs mt-1">Manage admin credentials.</p>
180
+ </div>
181
+ </div>
182
+ </div>
148
183
  </div>
149
184
  </section>
150
185
 
@@ -214,6 +249,17 @@ resources :posts
214
249
  ./ofa feature enable cms
215
250
  </div>
216
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>
217
263
  </div>
218
264
  </section>
219
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 3.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 "\\____/_/ /_/ |_| v2.0.0 "
25
+ puts "\\____/_/ /_/ |_| v4.1.0 "
26
26
  puts " "
27
27
  puts "✨ One-For-All Framework CLI ✨"
28
28
  puts "-----------------------------"
@@ -33,12 +33,13 @@ 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"
40
40
  puts " ofa reset-password USR PWD - Reset admin account password"
41
41
  puts " ofa db switch TYPE [NAME] - Switch DB: sqlite, mysql, mariadb, mongodb, postgres"
42
+ puts " ofa db migrate-data TYPE [NAME] - Migrate data to another DB"
42
43
  puts " ofa db migrate - Run database migrations"
43
44
  puts " ofa migrate - Run database migrations (alias for db migrate)"
44
45
  puts " ofa run - Start the application server"
@@ -67,6 +68,117 @@ def run_migrations
67
68
  end
68
69
  end
69
70
 
71
+ def perform_db_migration(target_type, target_name)
72
+ # 1. Initialize source environment
73
+ Object.const_set(:APP_ROOT, PROJECT_ROOT) unless defined?(APP_ROOT)
74
+ require File.join(FRAMEWORK_ROOT, 'config', 'boot')
75
+ source_db = DB
76
+
77
+ puts "📦 Starting data migration: #{DB_CONFIG['adapter']} -> #{target_type}..."
78
+
79
+ # 2. Setup target connection
80
+ target_db = nil
81
+ target_mongo = nil
82
+
83
+ if ['mongodb', 'mongo'].include?(target_type)
84
+ require 'mongo'
85
+ target_mongo = Mongo::Client.new(target_name)
86
+ target_db = Sequel.connect("sqlite://db/data.sqlite3")
87
+ elsif target_type == 'sqlite'
88
+ target_db = Sequel.connect("sqlite://#{target_name || 'db/production.sqlite3'}")
89
+ else
90
+ target_db = Sequel.connect("#{target_type}://root:@localhost/#{target_name}")
91
+ end
92
+
93
+ # 3. Ensure target tables exist
94
+ if target_db
95
+ target_db.extension :pagination
96
+ # Re-use the schema creation logic by mimicking a mini-boot
97
+ # For simplicity, we'll just create core tables if they don't exist
98
+ [:users, :pages, :posts, :projects, :products].each do |table|
99
+ unless target_db.table_exists?(table)
100
+ # Copy schema from source (very basic approach)
101
+ schema = source_db.schema(table)
102
+ target_db.create_table(table) do
103
+ schema.each do |col_name, col_info|
104
+ column col_name, col_info[:db_type], primary_key: col_info[:primary_key], allow_null: col_info[:allow_null]
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ # 4. Migrate data
112
+ [:users, :pages, :posts, :projects, :products].each do |table|
113
+ print " Migrating #{table}... "
114
+ rows = []
115
+
116
+ # Try fetching from Mongo if it exists
117
+ source_mongo = defined?(MONGO_CLIENT) ? MONGO_CLIENT : nil
118
+ if source_mongo
119
+ begin
120
+ mongo_rows = source_mongo[table].find.to_a
121
+ if mongo_rows.any?
122
+ rows = mongo_rows.map do |r|
123
+ r[:id] = r[:_id].to_s if r[:_id]
124
+ r.delete(:_id)
125
+ r
126
+ end
127
+ end
128
+ rescue
129
+ # Collection might not exist in Mongo, fallback to SQL
130
+ end
131
+ end
132
+
133
+ # Fallback to SQL if no rows found in Mongo
134
+ if rows.empty?
135
+ begin
136
+ rows = source_db[table].all
137
+ rescue
138
+ rows = []
139
+ end
140
+ end
141
+
142
+ # Clear target table before migrating to avoid duplicates
143
+ if target_db && target_db.table_exists?(table)
144
+ target_db[table].delete
145
+ end
146
+
147
+ count = 0
148
+ rows.each do |row|
149
+ if target_type == 'mongodb' && table == :users
150
+ # Users go to Mongo in Mongo mode
151
+ target_mongo[:users].update_one({ username: row[:username] }, { "$set" => row.reject{|k| k == :id} }, { upsert: true })
152
+ elsif target_db
153
+ # Deep cleaning and type conversion for SQLite compatibility
154
+ target_columns = target_db[table].columns
155
+ clean_row = {}
156
+ row.each do |k, v|
157
+ sym_k = k.to_sym
158
+ if target_columns.include?(sym_k) && sym_k != :id
159
+ if v.is_a?(BSON::ObjectId)
160
+ clean_row[sym_k] = v.to_s
161
+ elsif v.is_a?(Time)
162
+ clean_row[sym_k] = v.strftime('%Y-%m-%d %H:%M:%S')
163
+ else
164
+ clean_row[sym_k] = v
165
+ end
166
+ end
167
+ end
168
+ target_db[table].insert(clean_row)
169
+ end
170
+ count += 1
171
+ end
172
+ puts "#{count} rows."
173
+ end
174
+
175
+ # 5. Update configuration to point to new DB
176
+ absolute_ofa_path = File.expand_path(__FILE__)
177
+ system("ruby #{absolute_ofa_path} db switch #{target_type} #{target_name}")
178
+
179
+ puts "✅ Migration successful!"
180
+ end
181
+
70
182
  def ensure_initialized!
71
183
  config_path = File.join(PROJECT_ROOT, 'config', 'features.json')
72
184
  unless File.exist?(config_path)
@@ -114,7 +226,7 @@ when 'init'
114
226
  puts " / __ \\/ ____/ / | "
115
227
  puts " / / / / /_ / /| | Framework "
116
228
  puts "/ /_/ / __/ / ___ | Premium MVC "
117
- puts "\\____/_/ /_/ |_| v2.0.0 "
229
+ puts "\\____/_/ /_/ |_| v4.1.0 "
118
230
  puts " "
119
231
  puts "Initializing One-For-All project as '#{app_type}' in #{PROJECT_ROOT}..."
120
232
 
@@ -401,6 +513,12 @@ when 'feature'
401
513
  config[feature] = (action == 'enable')
402
514
  File.write(config_path, JSON.pretty_generate(config))
403
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
404
522
 
405
523
  when 'type'
406
524
  ensure_initialized!
@@ -442,8 +560,10 @@ when 'storage'
442
560
  puts "Application storage set to '#{name}'."
443
561
 
444
562
  when 'reset-password'
563
+ ENV['SKIP_MODELS'] = '1'
445
564
  Object.const_set(:APP_ROOT, PROJECT_ROOT) unless defined?(APP_ROOT)
446
565
  require File.join(FRAMEWORK_ROOT, 'config', 'boot')
566
+ require File.join(FRAMEWORK_ROOT, 'app', 'models', 'user.rb')
447
567
  user_name = ARGV.shift
448
568
  new_pwd = ARGV.shift
449
569
  if user_name.nil? || new_pwd.nil?
@@ -479,8 +599,16 @@ when 'db'
479
599
  type = ARGV.shift
480
600
  db_name = ARGV.shift
481
601
  case type
482
- when 'env'
602
+ when 'env', 'mongo', 'mongodb'
483
603
  config = { "adapter" => "env" }
604
+ if db_name && db_name.start_with?('mongodb')
605
+ env_path = File.join(PROJECT_ROOT, '.env')
606
+ env_lines = File.exist?(env_path) ? File.readlines(env_path) : []
607
+ env_lines.reject! { |l| l.start_with?('DATABASE_URL=') }
608
+ env_lines << "DATABASE_URL=#{db_name}\n"
609
+ File.write(env_path, env_lines.join)
610
+ puts "✅ Connection string saved to .env"
611
+ end
484
612
  when 'sqlite'
485
613
  config = { "adapter" => "sqlite", "database" => db_name || "db/development.sqlite3" }
486
614
  else
@@ -490,6 +618,14 @@ when 'db'
490
618
  puts "Switched to #{type} mode."
491
619
  when 'migrate'
492
620
  run_migrations
621
+ when 'migrate-data'
622
+ target_type = ARGV.shift
623
+ target_name = ARGV.shift
624
+ if target_type.nil?
625
+ puts "Usage: ofa db migrate-data TYPE [NAME/URL]"
626
+ exit 1
627
+ end
628
+ perform_db_migration(target_type, target_name)
493
629
  end
494
630
 
495
631
  when 'run'
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/database.json CHANGED
@@ -1,4 +1,3 @@
1
1
  {
2
- "adapter": "sqlite",
3
- "database": "db/development.sqlite3"
2
+ "adapter": "env"
4
3
  }
data/config/database.rb CHANGED
@@ -114,6 +114,22 @@ if DB
114
114
  end
115
115
  end
116
116
 
117
+ unless DB.table_exists?(:products)
118
+ DB.create_table :products do
119
+ primary_key :id
120
+ String :name, null: false
121
+ String :slug, null: false, unique: true
122
+ String :description, text: true
123
+ Float :price, default: 0.0
124
+ Integer :stock, default: 0
125
+ String :image_url
126
+ String :category
127
+ TrueClass :is_active, default: true
128
+ DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
129
+ DateTime :updated_at, default: Sequel::CURRENT_TIMESTAMP
130
+ end
131
+ end
132
+
117
133
  # Quick Migration for existing tables
118
134
  if DB.table_exists?(:pages)
119
135
  DB.alter_table(:pages) { add_column :is_active, TrueClass, default: true unless DB[:pages].columns.include?(:is_active) }
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
Binary file
data/db/target.sqlite3 ADDED
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: 3.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
@@ -238,6 +239,7 @@ files:
238
239
  - db/data.sqlite3
239
240
  - db/development.sqlite3
240
241
  - db/migrations/20260502000000_create_products.rb
242
+ - db/target.sqlite3
241
243
  - ofa
242
244
  - public/css/cms.css
243
245
  - public/images/logo.jpg