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 +4 -4
- data/.gitignore +35 -0
- data/README.md +76 -8
- data/app/views/blog_home.erb +1 -1
- data/app/views/cms/pages_form.erb +21 -9
- data/app/views/cms/posts_form.erb +20 -8
- data/app/views/docs.erb +47 -1
- data/app/views/index.erb +1 -1
- data/app/views/layout.erb +151 -0
- data/bin/ofa +140 -4
- data/config/boot.rb +8 -0
- data/config/database.json +1 -2
- data/config/database.rb +16 -0
- data/config/features.json +2 -1
- data/config/routes.rb +24 -20
- data/db/data.sqlite3 +0 -0
- data/db/development.sqlite3 +0 -0
- data/db/target.sqlite3 +0 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a5483a78bd78c4dcf46a9866464a4888bec73705273fe2db6a4b58179429b685
|
|
4
|
+
data.tar.gz: 8423d7b463a98b56f470aeeb244f4f68e4cd38ca9f0cf53fcf1b14f84e57c599
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
5
|
+
# ⚡ One-For-All (OFA) Framework v4.1.0
|
|
6
6
|
|
|
7
7
|
[](https://www.ruby-lang.org/)
|
|
8
8
|
[](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
|
|
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
|
-
|
|
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
|
|
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`| **
|
|
97
|
-
| `ofa db
|
|
98
|
-
| `ofa db
|
|
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
|
|
data/app/views/blog_home.erb
CHANGED
|
@@ -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
|
-
<%=
|
|
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
|
-
|
|
98
|
-
<
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
103
|
-
<
|
|
104
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
|
169
|
-
|
|
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\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
|
|
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
|
+
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 "\\____/_/ /_/ |_|
|
|
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 "\\____/_/ /_/ |_|
|
|
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
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
data/config/routes.rb
CHANGED
|
@@ -52,31 +52,35 @@ ROUTES = EksCent::Router.new do
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# --- CMS Dashboard ---
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
if FEATURES_CONFIG['auth']
|
|
73
|
+
get '/login' do |req, res|
|
|
74
|
+
AuthController.new(req, res).show_login
|
|
75
|
+
end
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
post '/login' do |req, res|
|
|
78
|
+
AuthController.new(req, res).login
|
|
79
|
+
end
|
|
77
80
|
|
|
78
|
-
|
|
79
|
-
|
|
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
|
data/db/development.sqlite3
CHANGED
|
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:
|
|
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
|