one-for-all-framework 1.0.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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.env.example +4 -0
  3. data/Dockerfile +19 -0
  4. data/Gemfile +20 -0
  5. data/LICENSE +21 -0
  6. data/Procfile +1 -0
  7. data/README.md +99 -0
  8. data/app/controllers/api_controller.rb +19 -0
  9. data/app/controllers/application_controller.rb +77 -0
  10. data/app/controllers/auth_controller.rb +35 -0
  11. data/app/controllers/dashboard_controller.rb +41 -0
  12. data/app/controllers/pages_controller.rb +49 -0
  13. data/app/controllers/posts_controller.rb +48 -0
  14. data/app/controllers/projects_controller.rb +48 -0
  15. data/app/helpers/cloudinary_helper.rb +14 -0
  16. data/app/middleware/auth_middleware.rb +52 -0
  17. data/app/middleware/csrf_middleware.rb +27 -0
  18. data/app/models/page.rb +8 -0
  19. data/app/models/post.rb +8 -0
  20. data/app/models/project.rb +7 -0
  21. data/app/models/user.rb +83 -0
  22. data/app/views/blog_home.erb +70 -0
  23. data/app/views/cms/pages_form.erb +156 -0
  24. data/app/views/cms/pages_index.erb +104 -0
  25. data/app/views/cms/posts_form.erb +182 -0
  26. data/app/views/cms/posts_index.erb +100 -0
  27. data/app/views/cms/projects_form.erb +176 -0
  28. data/app/views/cms/projects_index.erb +96 -0
  29. data/app/views/dashboard.erb +122 -0
  30. data/app/views/docs.erb +140 -0
  31. data/app/views/error.erb +159 -0
  32. data/app/views/index.erb +70 -0
  33. data/app/views/layout.erb +251 -0
  34. data/app/views/login.erb +45 -0
  35. data/app/views/page.erb +21 -0
  36. data/app/views/portfolio.erb +73 -0
  37. data/app/views/post.erb +33 -0
  38. data/app/views/posts/hello_world.erb +3 -0
  39. data/app/views/project.erb +39 -0
  40. data/bin/ofa +499 -0
  41. data/config/boot.rb +134 -0
  42. data/config/database.json +4 -0
  43. data/config/database.rb +142 -0
  44. data/config/features.json +7 -0
  45. data/config/locales/en.json +6 -0
  46. data/config/locales/id.json +6 -0
  47. data/config/routes.rb +87 -0
  48. data/config.eks +11 -0
  49. data/config.ru +11 -0
  50. data/db/data.sqlite3 +0 -0
  51. data/db/development.sqlite3 +0 -0
  52. data/ofa +2 -0
  53. data/public/css/cms.css +134 -0
  54. data/public/images/logo.jpg +0 -0
  55. data/public/images/logo.png +0 -0
  56. metadata +259 -0
@@ -0,0 +1,83 @@
1
+ require 'bcrypt'
2
+
3
+ class User < Sequel::Model
4
+ include BCrypt
5
+
6
+ # Password strength rules
7
+ MIN_PASSWORD_LENGTH = 8
8
+ PASSWORD_RULES = [
9
+ [/[A-Z]/, "must contain at least one uppercase letter"],
10
+ [/[0-9]/, "must contain at least one number"],
11
+ ]
12
+
13
+ # Helper to check if we are in Mongo mode
14
+ def self.mongo?
15
+ defined?(MONGO_CLIENT) && MONGO_CLIENT
16
+ end
17
+
18
+ # Override find for Mongo support
19
+ def self.find(params)
20
+ if mongo?
21
+ doc = MONGO_CLIENT[:users].find(params).first
22
+ return nil unless doc
23
+ user = User.new
24
+ # Set virtual ID from Mongo _id
25
+ user.values[:id] = doc[:_id].to_s if doc[:_id]
26
+ user.set(
27
+ username: doc[:username],
28
+ password_hash: doc[:password_hash],
29
+ avatar_url: doc[:avatar_url]
30
+ )
31
+ user
32
+ else
33
+ super
34
+ end
35
+ end
36
+
37
+ # Override save for Mongo support
38
+ def save
39
+ if self.class.mongo?
40
+ data = {
41
+ username: self.username,
42
+ password_hash: self.password_hash,
43
+ avatar_url: self.avatar_url,
44
+ updated_at: Time.now
45
+ }
46
+ MONGO_CLIENT[:users].update_one({ username: self.username }, { "$set" => data }, { upsert: true })
47
+ self
48
+ else
49
+ super
50
+ end
51
+ end
52
+
53
+ def password
54
+ @password ||= Password.new(password_hash)
55
+ end
56
+
57
+ def password=(new_password)
58
+ errors = validate_password_strength(new_password)
59
+ raise ArgumentError, "Weak password: #{errors.join(', ')}" if errors.any?
60
+
61
+ @password = Password.create(new_password)
62
+ self.password_hash = @password
63
+ end
64
+
65
+ def self.authenticate(username, password)
66
+ user = self.find(username: username)
67
+ return user if user && user.password == password
68
+ nil
69
+ end
70
+
71
+ private
72
+
73
+ # Returns an array of error messages. Empty = valid.
74
+ def validate_password_strength(pwd)
75
+ return ["cannot be empty"] if pwd.nil? || pwd.empty?
76
+ errors = []
77
+ errors << "must be at least #{MIN_PASSWORD_LENGTH} characters" if pwd.length < MIN_PASSWORD_LENGTH
78
+ PASSWORD_RULES.each do |regex, message|
79
+ errors << message unless pwd.match?(regex)
80
+ end
81
+ errors
82
+ end
83
+ end
@@ -0,0 +1,70 @@
1
+ <div class="header-section anime-element" style="margin-bottom: 3rem;">
2
+ <h1>Blog Feed</h1>
3
+ <p>Thoughts, tutorials, and insights from the edge of technology.</p>
4
+ </div>
5
+
6
+ <div class="blog-list grid grid-cols-1 gap-6 text-left">
7
+ <% if @posts.empty? %>
8
+ <div class="glass-card py-20 text-center">
9
+ <div class="w-20 h-20 bg-primary/10 text-primary rounded-3xl flex items-center justify-center mx-auto mb-6 text-3xl">
10
+ <i class="fas fa-feather-pointed"></i>
11
+ </div>
12
+ <p class="text-xl font-bold text-slate-400">No articles published yet.</p>
13
+ </div>
14
+ <% end %>
15
+
16
+ <% @posts.each do |post| %>
17
+ <article class="blog-item glass-card !p-0 flex flex-col md:flex-row group cursor-pointer" onclick="location.href='/posts/<%= post.slug %>'">
18
+ <div class="card-glow"></div>
19
+
20
+ <div class="md:w-1/3 h-48 md:h-auto bg-gradient-to-br from-primary/20 to-secondary/20 relative overflow-hidden">
21
+ <% if post.image_url && !post.image_url.empty? %>
22
+ <img src="<%= post.image_url %>" alt="<%= post.title %>" class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700">
23
+ <% else %>
24
+ <div class="absolute inset-0 flex items-center justify-center text-primary/30 group-hover:scale-110 transition-transform duration-700">
25
+ <i class="fas fa-file-alt text-6xl"></i>
26
+ </div>
27
+ <% end %>
28
+ </div>
29
+
30
+ <div class="p-8 flex-1 relative z-10">
31
+ <div class="flex items-center gap-4 mb-4">
32
+ <span class="badge-premium !mb-0"><%= post.category ? post.category : 'General' %></span>
33
+ <span class="text-xs font-bold text-slate-400 uppercase tracking-widest"><%= post.created_at.strftime('%B %d, %Y') %></span>
34
+ </div>
35
+
36
+ <h3 class="text-2xl font-black mb-4 tracking-tight group-hover:text-primary transition-colors"><%= post.title %></h3>
37
+
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") + "...") %>
40
+ </div>
41
+
42
+ <div class="flex items-center gap-2 text-primary font-bold text-sm">
43
+ Read Article <i class="fas fa-chevron-right text-[10px] group-hover:translate-x-1 transition-transform"></i>
44
+ </div>
45
+ </div>
46
+ </article>
47
+ <% end %>
48
+ </div>
49
+
50
+ <script>
51
+ document.addEventListener('DOMContentLoaded', () => {
52
+ anime({
53
+ targets: '.header-section',
54
+ translateY: [-20, 0],
55
+ opacity: [0, 1],
56
+ duration: 800,
57
+ easing: 'easeOutCubic',
58
+ delay: 400
59
+ });
60
+
61
+ anime({
62
+ targets: '.blog-item',
63
+ translateX: [-50, 0],
64
+ opacity: [0, 1],
65
+ duration: 800,
66
+ delay: anime.stagger(200, {start: 600}),
67
+ easing: 'easeOutExpo'
68
+ });
69
+ });
70
+ </script>
@@ -0,0 +1,156 @@
1
+ <div class="flex flex-col md:flex-row gap-8 items-start">
2
+ <!-- CMS Sidebar -->
3
+ <aside class="glass-panel w-full md:w-64 p-6 sticky top-24">
4
+ <div class="flex items-center gap-2 mb-8 px-2">
5
+ <div class="w-6 h-6 bg-primary rounded flex items-center justify-center text-white text-xs">
6
+ <i class="fas fa-user-shield"></i>
7
+ </div>
8
+ <h3 class="font-black tracking-tight text-lg">Admin<span class="text-primary">Panel</span></h3>
9
+ </div>
10
+
11
+ <nav class="space-y-2">
12
+ <a href="/dashboard" class="flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 <%= @req.path == '/dashboard' ? 'bg-primary text-white shadow-lg shadow-primary/30' : 'text-slate-500 hover:bg-white/5' %>">
13
+ <i class="fas fa-chart-pie w-5"></i>
14
+ <span class="font-semibold">Dashboard</span>
15
+ </a>
16
+
17
+ <a href="/dashboard/pages" class="flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 <%= @req.path.start_with?('/dashboard/pages') ? 'bg-primary text-white shadow-lg shadow-primary/30' : 'text-slate-500 hover:bg-white/5' %>">
18
+ <i class="fas fa-file-lines w-5"></i>
19
+ <span class="font-semibold">Pages</span>
20
+ </a>
21
+
22
+ <% if FEATURES_CONFIG['type'] == 'blog' %>
23
+ <a href="/dashboard/posts" class="flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 <%= @req.path.start_with?('/dashboard/posts') ? 'bg-primary text-white shadow-lg shadow-primary/30' : 'text-slate-500 hover:bg-white/5' %>">
24
+ <i class="fas fa-pen-nib w-5"></i>
25
+ <span class="font-semibold">Blog Posts</span>
26
+ </a>
27
+ <% elsif FEATURES_CONFIG['type'] == 'portfolio' %>
28
+ <a href="/dashboard/projects" class="flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 <%= @req.path.start_with?('/dashboard/projects') ? 'bg-primary text-white shadow-lg shadow-primary/30' : 'text-slate-500 hover:bg-white/5' %>">
29
+ <i class="fas fa-palette w-5"></i>
30
+ <span class="font-semibold">Projects</span>
31
+ </a>
32
+ <% end %>
33
+
34
+ <div class="h-px bg-white/5 my-4"></div>
35
+
36
+ <form action="/logout" method="POST">
37
+ <%= csrf_tag %>
38
+ <button type="submit" class="w-full flex items-center gap-3 px-4 py-3 rounded-2xl text-red-400 hover:bg-red-500/10 transition-all duration-300 font-semibold">
39
+ <i class="fas fa-door-open w-5"></i>
40
+ <span>Logout</span>
41
+ </button>
42
+ </form>
43
+ </nav>
44
+ </aside>
45
+
46
+ <!-- Main Content -->
47
+ <div class="flex-1 space-y-6 w-full pb-20">
48
+ <div class="flex justify-between items-center bg-white/5 p-6 rounded-3xl border border-white/5">
49
+ <div>
50
+ <h2 class="text-2xl font-black tracking-tight"><%= @page.id ? 'Edit' : 'Add' %> Page</h2>
51
+ <p class="text-slate-500 text-sm">Customize the structure and content of your website pages.</p>
52
+ </div>
53
+ <a href="/dashboard/pages" class="px-6 py-2.5 rounded-xl bg-white/5 text-slate-500 hover:bg-white/10 transition-all font-bold">
54
+ Cancel
55
+ </a>
56
+ </div>
57
+
58
+ <div class="glass-panel p-8">
59
+ <form action="<%= @page.id ? "/dashboard/pages/#{@page.id}/update" : "/dashboard/pages" %>" method="POST" id="main-form" class="space-y-6">
60
+ <%= csrf_tag %>
61
+ <div class="space-y-2">
62
+ <label class="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 ml-1">Page Title</label>
63
+ <input type="text" name="page[title]" value="<%= @page.title %>" placeholder="Example: About Me" required oninput="generateSlug(this.value)"
64
+ 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-200 font-bold text-lg">
65
+ </div>
66
+
67
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
68
+ <div class="space-y-2">
69
+ <label class="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 ml-1">URL Slug (Automatic)</label>
70
+ <div class="relative">
71
+ <span class="absolute left-5 top-1/2 -translate-y-1/2 text-slate-500 font-mono text-sm">/</span>
72
+ <input type="text" name="page[slug]" id="slug-input" value="<%= @page.slug %>" placeholder="about-me" required
73
+ class="w-full pl-8 pr-5 py-3 bg-white/5 border border-white/10 rounded-2xl focus:border-primary focus:ring-4 focus:ring-primary/10 outline-none transition-all font-mono text-primary">
74
+ </div>
75
+ </div>
76
+ <div class="flex items-end gap-6 pb-2">
77
+ <label class="flex items-center gap-3 cursor-pointer group">
78
+ <div class="relative">
79
+ <input type="checkbox" name="page[is_nav]" <%= @page.is_nav ? 'checked' : '' %> class="sr-only peer">
80
+ <div class="w-11 h-6 bg-slate-200 dark:bg-white/10 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
81
+ </div>
82
+ <span class="text-sm font-bold text-slate-600 dark:text-slate-400 group-hover:text-primary transition-colors">Show in Navigation</span>
83
+ </label>
84
+ <label class="flex items-center gap-3 cursor-pointer group">
85
+ <div class="relative">
86
+ <input type="checkbox" name="page[is_active]" <%= @page.is_active ? 'checked' : '' %> class="sr-only peer">
87
+ <div class="w-11 h-6 bg-slate-200 dark:bg-white/10 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-green-500"></div>
88
+ </div>
89
+ <span class="text-sm font-bold text-slate-600 dark:text-slate-400 group-hover:text-green-500 transition-colors">Active</span>
90
+ </label>
91
+ </div>
92
+ </div>
93
+
94
+ <div class="space-y-2">
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>
100
+ </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
+ <input type="file" id="image-upload" class="hidden" onchange="handleImageUpload(this)">
104
+ </div>
105
+
106
+ <button type="submit" class="btn-premium w-full py-4 text-lg">
107
+ <i class="fas fa-save mr-2"></i> Save Page
108
+ </button>
109
+ </form>
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+
115
+ <script>
116
+ function generateSlug(text) {
117
+ const slug = text.toLowerCase()
118
+ .replace(/[^\w ]+/g, '')
119
+ .replace(/ +/g, '-');
120
+ document.getElementById('slug-input').value = slug;
121
+ }
122
+
123
+ async function handleImageUpload(input) {
124
+ if (!input.files || !input.files[0]) return;
125
+
126
+ const formData = new FormData();
127
+ formData.append('file', input.files[0]);
128
+ formData.append('csrf_token', '<%= csrf_token %>');
129
+
130
+ const btn = document.querySelector('.upload-helper-btn');
131
+ const originalText = btn.innerText;
132
+ btn.innerText = '⌛ Uploading...';
133
+ btn.disabled = true;
134
+
135
+ try {
136
+ const response = await fetch('/dashboard/upload', {
137
+ method: 'POST',
138
+ body: formData
139
+ });
140
+ const data = await response.json();
141
+ 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;
145
+ alert('Image uploaded successfully!');
146
+ } else {
147
+ alert('Upload failed: ' + (data.error || 'Unknown error'));
148
+ }
149
+ } catch (e) {
150
+ alert('An error occurred during upload.');
151
+ } finally {
152
+ btn.innerText = originalText;
153
+ btn.disabled = false;
154
+ }
155
+ }
156
+ </script>
@@ -0,0 +1,104 @@
1
+ <div class="flex flex-col md:flex-row gap-8 items-start">
2
+ <!-- CMS Sidebar -->
3
+ <aside class="glass-panel w-full md:w-64 p-6 sticky top-24">
4
+ <div class="flex items-center gap-2 mb-8 px-2">
5
+ <div class="w-6 h-6 bg-primary rounded flex items-center justify-center text-white text-xs">
6
+ <i class="fas fa-user-shield"></i>
7
+ </div>
8
+ <h3 class="font-black tracking-tight text-lg">Admin<span class="text-primary">Panel</span></h3>
9
+ </div>
10
+
11
+ <nav class="space-y-2">
12
+ <a href="/dashboard" class="flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 <%= @req.path == '/dashboard' ? 'bg-primary text-white shadow-lg shadow-primary/30' : 'text-slate-500 hover:bg-white/5' %>">
13
+ <i class="fas fa-chart-pie w-5"></i>
14
+ <span class="font-semibold">Dashboard</span>
15
+ </a>
16
+
17
+ <a href="/dashboard/pages" class="flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 <%= @req.path.start_with?('/dashboard/pages') ? 'bg-primary text-white shadow-lg shadow-primary/30' : 'text-slate-500 hover:bg-white/5' %>">
18
+ <i class="fas fa-file-lines w-5"></i>
19
+ <span class="font-semibold">Pages</span>
20
+ </a>
21
+
22
+ <% if FEATURES_CONFIG['type'] == 'blog' %>
23
+ <a href="/dashboard/posts" class="flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 <%= @req.path.start_with?('/dashboard/posts') ? 'bg-primary text-white shadow-lg shadow-primary/30' : 'text-slate-500 hover:bg-white/5' %>">
24
+ <i class="fas fa-pen-nib w-5"></i>
25
+ <span class="font-semibold">Blog Posts</span>
26
+ </a>
27
+ <% elsif FEATURES_CONFIG['type'] == 'portfolio' %>
28
+ <a href="/dashboard/projects" class="flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 <%= @req.path.start_with?('/dashboard/projects') ? 'bg-primary text-white shadow-lg shadow-primary/30' : 'text-slate-500 hover:bg-white/5' %>">
29
+ <i class="fas fa-palette w-5"></i>
30
+ <span class="font-semibold">Projects</span>
31
+ </a>
32
+ <% end %>
33
+
34
+ <div class="h-px bg-white/5 my-4"></div>
35
+
36
+ <form action="/logout" method="POST">
37
+ <%= csrf_tag %>
38
+ <button type="submit" class="w-full flex items-center gap-3 px-4 py-3 rounded-2xl text-red-400 hover:bg-red-500/10 transition-all duration-300 font-semibold">
39
+ <i class="fas fa-door-open w-5"></i>
40
+ <span>Logout</span>
41
+ </button>
42
+ </form>
43
+ </nav>
44
+ </aside>
45
+
46
+ <!-- Main Content -->
47
+ <div class="flex-1 space-y-6 w-full">
48
+ <div class="flex justify-between items-center bg-white/5 p-6 rounded-3xl border border-white/5">
49
+ <div>
50
+ <h2 class="text-2xl font-black tracking-tight">Page Management</h2>
51
+ <p class="text-slate-500 text-sm">Manage all static pages of your website.</p>
52
+ </div>
53
+ <a href="/dashboard/pages/new" class="btn-premium">
54
+ <i class="fas fa-plus mr-2 text-sm"></i> New Page
55
+ </a>
56
+ </div>
57
+
58
+ <div class="glass-panel overflow-hidden border-none shadow-xl">
59
+ <table class="w-full text-left border-collapse">
60
+ <thead>
61
+ <tr class="bg-white/5">
62
+ <th class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-400">Title</th>
63
+ <th class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-400">Slug</th>
64
+ <th class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-400">Created At</th>
65
+ <th class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-400 text-right">Action</th>
66
+ </tr>
67
+ </thead>
68
+ <tbody class="divide-y divide-white/5">
69
+ <% if @pages.empty? %>
70
+ <tr>
71
+ <td colspan="4" class="px-6 py-12 text-center text-slate-500 italic">No pages created yet.</td>
72
+ </tr>
73
+ <% end %>
74
+ <% @pages.each do |page| %>
75
+ <tr class="hover:bg-white/[0.02] transition-colors group">
76
+ <td class="px-6 py-4">
77
+ <span class="font-bold text-slate-700 dark:text-slate-200 group-hover:text-primary transition-colors"><%= page.title %></span>
78
+ </td>
79
+ <td class="px-6 py-4">
80
+ <code class="text-xs bg-primary/10 px-2 py-1 rounded-lg text-primary">/<%= page.slug %></code>
81
+ </td>
82
+ <td class="px-6 py-4 text-sm text-slate-500 dark:text-slate-400">
83
+ <%= page.created_at.strftime('%d %b %Y') %>
84
+ </td>
85
+ <td class="px-6 py-4 text-right">
86
+ <div class="flex justify-end gap-2">
87
+ <a href="/dashboard/pages/<%= page.id %>/edit" class="w-9 h-9 flex items-center justify-center rounded-xl bg-primary/10 text-primary hover:bg-primary hover:text-white transition-all shadow-lg shadow-primary/10">
88
+ <i class="fas fa-edit text-sm"></i>
89
+ </a>
90
+ <form action="/dashboard/pages/<%= page.id %>/delete" method="POST" onsubmit="return confirm('Delete this page?')" class="inline">
91
+ <%= csrf_tag %>
92
+ <button type="submit" class="w-9 h-9 flex items-center justify-center rounded-xl bg-red-500/10 text-red-500 hover:bg-red-500 hover:text-white transition-all shadow-lg shadow-red-500/10">
93
+ <i class="fas fa-trash-can text-sm"></i>
94
+ </button>
95
+ </form>
96
+ </div>
97
+ </td>
98
+ </tr>
99
+ <% end %>
100
+ </tbody>
101
+ </table>
102
+ </div>
103
+ </div>
104
+ </div>
@@ -0,0 +1,182 @@
1
+ <div class="flex flex-col md:flex-row gap-8 items-start">
2
+ <!-- CMS Sidebar -->
3
+ <aside class="glass-panel w-full md:w-64 p-6 sticky top-24">
4
+ <div class="flex items-center gap-2 mb-8 px-2">
5
+ <div class="w-6 h-6 bg-primary rounded flex items-center justify-center text-white text-xs">
6
+ <i class="fas fa-user-shield"></i>
7
+ </div>
8
+ <h3 class="font-black tracking-tight text-lg">Admin<span class="text-primary">Panel</span></h3>
9
+ </div>
10
+
11
+ <nav class="space-y-2">
12
+ <a href="/dashboard" class="flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 <%= @req.path == '/dashboard' ? 'bg-primary text-white shadow-lg shadow-primary/30' : 'text-slate-500 hover:bg-white/5' %>">
13
+ <i class="fas fa-chart-pie w-5"></i>
14
+ <span class="font-semibold">Dashboard</span>
15
+ </a>
16
+
17
+ <a href="/dashboard/pages" class="flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 <%= @req.path.start_with?('/dashboard/pages') ? 'bg-primary text-white shadow-lg shadow-primary/30' : 'text-slate-500 hover:bg-white/5' %>">
18
+ <i class="fas fa-file-lines w-5"></i>
19
+ <span class="font-semibold">Pages</span>
20
+ </a>
21
+
22
+ <% if FEATURES_CONFIG['type'] == 'blog' %>
23
+ <a href="/dashboard/posts" class="flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 <%= @req.path.start_with?('/dashboard/posts') ? 'bg-primary text-white shadow-lg shadow-primary/30' : 'text-slate-500 hover:bg-white/5' %>">
24
+ <i class="fas fa-pen-nib w-5"></i>
25
+ <span class="font-semibold">Blog Posts</span>
26
+ </a>
27
+ <% elsif FEATURES_CONFIG['type'] == 'portfolio' %>
28
+ <a href="/dashboard/projects" class="flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 <%= @req.path.start_with?('/dashboard/projects') ? 'bg-primary text-white shadow-lg shadow-primary/30' : 'text-slate-500 hover:bg-white/5' %>">
29
+ <i class="fas fa-palette w-5"></i>
30
+ <span class="font-semibold">Projects</span>
31
+ </a>
32
+ <% end %>
33
+
34
+ <div class="h-px bg-white/5 my-4"></div>
35
+
36
+ <form action="/logout" method="POST">
37
+ <%= csrf_tag %>
38
+ <button type="submit" class="w-full flex items-center gap-3 px-4 py-3 rounded-2xl text-red-400 hover:bg-red-500/10 transition-all duration-300 font-semibold">
39
+ <i class="fas fa-door-open w-5"></i>
40
+ <span>Logout</span>
41
+ </button>
42
+ </form>
43
+ </nav>
44
+ </aside>
45
+
46
+ <!-- Main Content -->
47
+ <div class="flex-1 space-y-6 w-full pb-20">
48
+ <div class="flex justify-between items-center bg-white/5 p-6 rounded-3xl border border-white/5">
49
+ <div>
50
+ <h2 class="text-2xl font-black tracking-tight"><%= @post.id ? 'Edit' : 'Add' %> Article</h2>
51
+ <p class="text-slate-500 text-sm">Share your thoughts and stories.</p>
52
+ </div>
53
+ <a href="/dashboard/posts" class="px-6 py-2.5 rounded-xl bg-white/5 text-slate-500 hover:bg-white/10 transition-all font-bold">
54
+ Cancel
55
+ </a>
56
+ </div>
57
+
58
+ <div class="glass-panel p-8">
59
+ <form action="<%= @post.id ? "/dashboard/posts/#{@post.id}/update" : "/dashboard/posts" %>" method="POST" id="main-form" class="space-y-6">
60
+ <%= csrf_tag %>
61
+
62
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6 items-end">
63
+ <div class="md:col-span-2 space-y-2">
64
+ <label class="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 ml-1">Article Title</label>
65
+ <input type="text" name="post[title]" value="<%= @post.title %>" placeholder="Example: Learning Ruby Basics" required oninput="generateSlug(this.value)"
66
+ 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-200 font-bold text-lg">
67
+ </div>
68
+ <div class="pb-3 pl-2 text-left">
69
+ <label class="flex items-center gap-3 cursor-pointer group">
70
+ <div class="relative">
71
+ <input type="checkbox" name="post[is_active]" <%= @post.is_active ? 'checked' : '' %> class="sr-only peer">
72
+ <div class="w-11 h-6 bg-slate-200 dark:bg-white/10 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-green-500"></div>
73
+ </div>
74
+ <span class="text-sm font-bold text-slate-600 dark:text-slate-400 group-hover:text-green-500 transition-colors">Publish</span>
75
+ </label>
76
+ </div>
77
+ </div>
78
+
79
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
80
+ <div class="space-y-2">
81
+ <label class="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 ml-1">URL Slug (Automatic)</label>
82
+ <div class="relative text-left">
83
+ <span class="absolute left-5 top-1/2 -translate-y-1/2 text-slate-500 font-mono text-sm">/</span>
84
+ <input type="text" name="post[slug]" id="slug-input" value="<%= @post.slug %>" placeholder="learning-ruby" required
85
+ class="w-full pl-8 pr-5 py-3 bg-white/5 border border-white/10 rounded-2xl focus:border-primary focus:ring-4 focus:ring-primary/10 outline-none transition-all font-mono text-primary">
86
+ </div>
87
+ </div>
88
+ <div class="space-y-2 text-left">
89
+ <label class="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 ml-1">Category</label>
90
+ <input type="text" name="post[category]" value="<%= @post.category %>" placeholder="Tutorial, Tips, etc"
91
+ class="w-full px-5 py-3 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-200">
92
+ </div>
93
+ </div>
94
+
95
+ <div class="space-y-2">
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>
98
+ <div class="flex gap-4">
99
+ <button type="button" class="text-xs font-bold text-primary hover:underline flex items-center gap-2" onclick="openUploader('thumbnail')">
100
+ <i class="fas fa-image"></i> Set Thumbnail
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>
105
+ </div>
106
+ </div>
107
+
108
+ <div id="thumbnail-preview-container" style="<%= @post.image_url && !@post.image_url.empty? ? '' : 'display: none;' %>" class="mb-4 p-2 border-2 border-dashed border-white/10 rounded-2xl text-left">
109
+ <span class="text-[10px] font-bold uppercase text-slate-500 mb-2 block ml-1">Thumbnail Preview</span>
110
+ <img id="thumbnail-preview" src="<%= @post.image_url %>" class="max-h-32 rounded-xl shadow-lg">
111
+ </div>
112
+
113
+ <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
+ <input type="file" id="image-upload" class="hidden" onchange="handleImageUpload(this)">
117
+ </div>
118
+
119
+ <button type="submit" class="btn-premium w-full py-4 text-lg">
120
+ <i class="fas fa-save mr-2"></i> Publish Now
121
+ </button>
122
+ </form>
123
+ </div>
124
+ </div>
125
+ </div>
126
+
127
+ <script>
128
+ let activeUploadTarget = 'content';
129
+
130
+ function generateSlug(text) {
131
+ const slug = text.toLowerCase().replace(/[^\w ]+/g, '').replace(/ +/g, '-');
132
+ document.getElementById('slug-input').value = slug;
133
+ }
134
+
135
+ function openUploader(target) {
136
+ activeUploadTarget = target;
137
+ document.getElementById('image-upload').click();
138
+ }
139
+
140
+ async function handleImageUpload(input) {
141
+ if (!input.files || !input.files[0]) return;
142
+
143
+ const targetBtn = activeUploadTarget === 'thumbnail' ?
144
+ document.querySelector('button[onclick*="thumbnail"]') :
145
+ document.querySelector('button[onclick*="content"]');
146
+ const originalText = targetBtn.innerHTML;
147
+ targetBtn.innerHTML = '⌛ Uploading...';
148
+ targetBtn.disabled = true;
149
+
150
+ const formData = new FormData();
151
+ formData.append('file', input.files[0]);
152
+ formData.append('csrf_token', '<%= csrf_token %>');
153
+
154
+ try {
155
+ const response = await fetch('/dashboard/upload', {
156
+ method: 'POST',
157
+ body: formData
158
+ });
159
+ const data = await response.json();
160
+ if (data.url) {
161
+ if (activeUploadTarget === 'thumbnail') {
162
+ document.getElementById('thumbnail-url').value = data.url;
163
+ const preview = document.getElementById('thumbnail-preview');
164
+ const container = document.getElementById('thumbnail-preview-container');
165
+ preview.src = data.url;
166
+ container.style.display = 'block';
167
+ } else {
168
+ const textarea = document.querySelector('textarea');
169
+ textarea.value += `\n![image](${data.url})\n`;
170
+ }
171
+ alert('Image uploaded successfully! Don\'t forget to click PUBLISH/SAVE below.');
172
+ } else {
173
+ alert('Upload failed: ' + (data.error || 'Unknown error'));
174
+ }
175
+ } catch (e) {
176
+ alert('An error occurred during upload.');
177
+ } finally {
178
+ targetBtn.innerHTML = originalText;
179
+ targetBtn.disabled = false;
180
+ }
181
+ }
182
+ </script>