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 +4 -4
- data/.gitignore +35 -0
- data/README.md +3 -2
- 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 +12 -1
- data/app/views/index.erb +1 -1
- data/app/views/layout.erb +151 -0
- data/bin/ofa +9 -3
- data/config/boot.rb +8 -0
- data/config/features.json +2 -1
- data/config/routes.rb +24 -20
- data/db/data.sqlite3 +0 -0
- metadata +2 -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 v4.
|
|
5
|
+
# ⚡ One-For-All (OFA) Framework v4.1.0
|
|
6
6
|
|
|
7
7
|
[](https://www.ruby-lang.org/)
|
|
8
8
|
[](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
|
|
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
|
---
|
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 v4.
|
|
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.
|
|
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.
|
|
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.
|
|
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
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
|
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.
|
|
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
|