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.
- checksums.yaml +7 -0
- data/.env.example +4 -0
- data/Dockerfile +19 -0
- data/Gemfile +20 -0
- data/LICENSE +21 -0
- data/Procfile +1 -0
- data/README.md +99 -0
- data/app/controllers/api_controller.rb +19 -0
- data/app/controllers/application_controller.rb +77 -0
- data/app/controllers/auth_controller.rb +35 -0
- data/app/controllers/dashboard_controller.rb +41 -0
- data/app/controllers/pages_controller.rb +49 -0
- data/app/controllers/posts_controller.rb +48 -0
- data/app/controllers/projects_controller.rb +48 -0
- data/app/helpers/cloudinary_helper.rb +14 -0
- data/app/middleware/auth_middleware.rb +52 -0
- data/app/middleware/csrf_middleware.rb +27 -0
- data/app/models/page.rb +8 -0
- data/app/models/post.rb +8 -0
- data/app/models/project.rb +7 -0
- data/app/models/user.rb +83 -0
- data/app/views/blog_home.erb +70 -0
- data/app/views/cms/pages_form.erb +156 -0
- data/app/views/cms/pages_index.erb +104 -0
- data/app/views/cms/posts_form.erb +182 -0
- data/app/views/cms/posts_index.erb +100 -0
- data/app/views/cms/projects_form.erb +176 -0
- data/app/views/cms/projects_index.erb +96 -0
- data/app/views/dashboard.erb +122 -0
- data/app/views/docs.erb +140 -0
- data/app/views/error.erb +159 -0
- data/app/views/index.erb +70 -0
- data/app/views/layout.erb +251 -0
- data/app/views/login.erb +45 -0
- data/app/views/page.erb +21 -0
- data/app/views/portfolio.erb +73 -0
- data/app/views/post.erb +33 -0
- data/app/views/posts/hello_world.erb +3 -0
- data/app/views/project.erb +39 -0
- data/bin/ofa +499 -0
- data/config/boot.rb +134 -0
- data/config/database.json +4 -0
- data/config/database.rb +142 -0
- data/config/features.json +7 -0
- data/config/locales/en.json +6 -0
- data/config/locales/id.json +6 -0
- data/config/routes.rb +87 -0
- data/config.eks +11 -0
- data/config.ru +11 -0
- data/db/data.sqlite3 +0 -0
- data/db/development.sqlite3 +0 -0
- data/ofa +2 -0
- data/public/css/cms.css +134 -0
- data/public/images/logo.jpg +0 -0
- data/public/images/logo.png +0 -0
- metadata +259 -0
data/app/views/page.erb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<div class="glass-card anime-element">
|
|
2
|
+
<h1><%= @title %></h1>
|
|
3
|
+
<div class="page-content markdown-content" style="margin-top: 2rem;">
|
|
4
|
+
<%= markdown(@content) %>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="mt-12 pt-8 border-t border-white/5">
|
|
7
|
+
<a href="/" class="btn-secondary">
|
|
8
|
+
<i class="fas fa-arrow-left mr-2"></i> Back to Home
|
|
9
|
+
</a>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
anime({
|
|
15
|
+
targets: '.anime-element',
|
|
16
|
+
translateY: [30, 0],
|
|
17
|
+
opacity: [0, 1],
|
|
18
|
+
duration: 1000,
|
|
19
|
+
easing: 'easeOutExpo'
|
|
20
|
+
});
|
|
21
|
+
</script>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<div class="header-section anime-element" style="margin-bottom: 3rem;">
|
|
2
|
+
<h1>Creative Portfolio</h1>
|
|
3
|
+
<p>Showcasing my best work, creative journey, and engineering experiments.</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="portfolio-grid grid grid-cols-1 md:grid-cols-2 gap-8 text-left">
|
|
7
|
+
<% if @projects.empty? %>
|
|
8
|
+
<div class="glass-card col-span-full 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-folder-open"></i>
|
|
11
|
+
</div>
|
|
12
|
+
<p class="text-xl font-bold text-slate-400">No projects displayed yet.</p>
|
|
13
|
+
</div>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<% @projects.each do |project| %>
|
|
17
|
+
<div class="project-card glass-card flex flex-col h-full !p-0">
|
|
18
|
+
<div class="card-glow"></div>
|
|
19
|
+
|
|
20
|
+
<div class="relative overflow-hidden aspect-video group-hover:scale-[1.02] transition-transform duration-700">
|
|
21
|
+
<% if project.image_url && !project.image_url.empty? %>
|
|
22
|
+
<img src="<%= project.image_url %>" alt="<%= project.title %>" class="w-full h-full object-cover">
|
|
23
|
+
<% else %>
|
|
24
|
+
<div class="w-full h-full bg-gradient-to-br from-primary to-secondary opacity-20"></div>
|
|
25
|
+
<% end %>
|
|
26
|
+
<div class="absolute inset-0 bg-gradient-to-t from-bg via-transparent to-transparent opacity-60"></div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="p-8 flex flex-col flex-grow relative z-10">
|
|
30
|
+
<div class="badge-premium">Featured Project</div>
|
|
31
|
+
<h3 class="text-2xl font-black mb-4 tracking-tight group-hover:text-primary transition-colors"><%= project.title %></h3>
|
|
32
|
+
|
|
33
|
+
<div class="text-slate-500 dark:text-slate-400 text-sm leading-relaxed mb-8 flex-grow">
|
|
34
|
+
<%= markdown(project.description.split("\n")[0..2].join("\n") + "...") %>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="flex items-center justify-between mt-auto pt-6 border-t border-white/5">
|
|
38
|
+
<% if project.link && !project.link.empty? %>
|
|
39
|
+
<a href="<%= project.link %>" target="_blank" class="btn-premium !px-6 !py-2.5 !text-sm">
|
|
40
|
+
<i class="fas fa-external-link-alt mr-2"></i> Live Demo
|
|
41
|
+
</a>
|
|
42
|
+
<% end %>
|
|
43
|
+
<a href="/projects/<%= project.slug %>" class="group/link flex items-center gap-2 text-sm font-bold text-slate-500 hover:text-primary transition-colors">
|
|
44
|
+
Case Study <i class="fas fa-arrow-right group-hover/link:translate-x-1 transition-transform"></i>
|
|
45
|
+
</a>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<script>
|
|
53
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
54
|
+
anime({
|
|
55
|
+
targets: '.header-section',
|
|
56
|
+
translateY: [20, 0],
|
|
57
|
+
opacity: [0, 1],
|
|
58
|
+
duration: 800,
|
|
59
|
+
easing: 'easeOutCubic',
|
|
60
|
+
delay: 400
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
anime({
|
|
64
|
+
targets: '.project-card',
|
|
65
|
+
translateY: [50, 0],
|
|
66
|
+
scale: [0.95, 1],
|
|
67
|
+
opacity: [0, 1],
|
|
68
|
+
duration: 800,
|
|
69
|
+
delay: anime.stagger(150, {start: 600}),
|
|
70
|
+
easing: 'easeOutExpo'
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
</script>
|
data/app/views/post.erb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<div class="glass-card anime-element" style="text-align: left; max-width: 800px; margin: 0 auto;">
|
|
2
|
+
<div style="margin-bottom: 2rem;">
|
|
3
|
+
<span class="badge" style="background: var(--primary); color: white; padding: 0.3rem 0.8rem; border-radius: 0.5rem; font-size: 0.8rem; font-weight: 700;">
|
|
4
|
+
<%= @post.category.upcase %>
|
|
5
|
+
</span>
|
|
6
|
+
<h1 style="margin: 1rem 0 0.5rem 0; font-size: 3rem; line-height: 1.1;"><%= @post.title %></h1>
|
|
7
|
+
<p style="color: var(--text-muted); font-size: 0.95rem;">Published on <%= @post.created_at.strftime('%B %d, %Y') %></p>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<% if @post.image_url && !@post.image_url.empty? %>
|
|
11
|
+
<img src="<%= @post.image_url %>" alt="<%= @post.title %>" style="width: 100%; max-height: 400px; object-fit: cover; border-radius: 1.5rem; margin-bottom: 2.5rem; border: 1px solid var(--border);">
|
|
12
|
+
<% end %>
|
|
13
|
+
|
|
14
|
+
<div class="post-content markdown-content">
|
|
15
|
+
<%= markdown(@post.content) %>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="mt-12 pt-8 border-t border-white/5">
|
|
19
|
+
<a href="/" class="btn-secondary">
|
|
20
|
+
<i class="fas fa-arrow-left mr-2"></i> Back to Blog
|
|
21
|
+
</a>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<script>
|
|
26
|
+
anime({
|
|
27
|
+
targets: '.anime-element',
|
|
28
|
+
translateY: [30, 0],
|
|
29
|
+
opacity: [0, 1],
|
|
30
|
+
duration: 800,
|
|
31
|
+
easing: 'easeOutExpo'
|
|
32
|
+
});
|
|
33
|
+
</script>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<div class="glass-card anime-element" style="text-align: left; max-width: 800px; margin: 0 auto;">
|
|
2
|
+
<div style="margin-bottom: 2rem;">
|
|
3
|
+
<h1 style="margin: 0 0 0.5rem 0; font-size: 3rem; line-height: 1.1;"><%= @project.title %></h1>
|
|
4
|
+
<p style="color: var(--text-muted); font-size: 0.95rem;">Project created on <%= @project.created_at.strftime('%B %d, %Y') %></p>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<% if @project.image_url && !@project.image_url.empty? %>
|
|
8
|
+
<img src="<%= @project.image_url %>" alt="<%= @project.title %>" style="width: 100%; max-height: 400px; object-fit: cover; border-radius: 1.5rem; margin-bottom: 2.5rem; border: 1px solid var(--border);">
|
|
9
|
+
<% else %>
|
|
10
|
+
<div style="width: 100%; height: 300px; background: linear-gradient(135deg, var(--primary), var(--accent)); border-radius: 1.5rem; margin-bottom: 2.5rem; opacity: 0.8; display: flex; align-items: center; justify-content: center; color: white; font-size: 2rem; font-weight: 800;">
|
|
11
|
+
<%= @project.title %>
|
|
12
|
+
</div>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<div class="project-content markdown-content">
|
|
16
|
+
<%= markdown(@project.description) %>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="flex flex-wrap gap-4 items-center mt-12 pt-8 border-t border-white/5">
|
|
20
|
+
<% if @project.link && !@project.link.empty? %>
|
|
21
|
+
<a href="<%= @project.link %>" target="_blank" class="btn-premium">
|
|
22
|
+
<i class="fas fa-rocket mr-2"></i> Visit Live Demo
|
|
23
|
+
</a>
|
|
24
|
+
<% end %>
|
|
25
|
+
<a href="/" class="btn-secondary">
|
|
26
|
+
<i class="fas fa-arrow-left mr-2"></i> Back to Home
|
|
27
|
+
</a>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<script>
|
|
32
|
+
anime({
|
|
33
|
+
targets: '.anime-element',
|
|
34
|
+
translateY: [30, 0],
|
|
35
|
+
opacity: [0, 1],
|
|
36
|
+
duration: 800,
|
|
37
|
+
easing: 'easeOutExpo'
|
|
38
|
+
});
|
|
39
|
+
</script>
|
data/bin/ofa
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'optparse'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
# Framework core path
|
|
8
|
+
FRAMEWORK_ROOT = File.expand_path('..', __dir__)
|
|
9
|
+
# Current project path (where the user is running the command)
|
|
10
|
+
PROJECT_ROOT = Dir.pwd
|
|
11
|
+
|
|
12
|
+
# Load dotenv from project root if exists
|
|
13
|
+
begin
|
|
14
|
+
require 'dotenv'
|
|
15
|
+
Dotenv.load(File.join(PROJECT_ROOT, '.env'))
|
|
16
|
+
rescue LoadError
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def help
|
|
20
|
+
puts "✨ One-For-All Framework CLI ✨"
|
|
21
|
+
puts "-----------------------------"
|
|
22
|
+
puts "Usage:"
|
|
23
|
+
puts " ofa new NAME [TYPE] - Create a new project + automatic bundle install"
|
|
24
|
+
puts " ofa init [TYPE] - Initialize project in current folder"
|
|
25
|
+
puts " ofa g controller NAME - Generate a new controller"
|
|
26
|
+
puts " ofa g model NAME - Generate a new model"
|
|
27
|
+
puts " ofa g migration NAME - Generate a new migration"
|
|
28
|
+
puts " ofa g post TITLE - Create a new post [args: --category, --author, --image]"
|
|
29
|
+
puts " ofa feature ACTION F - Toggle features: action=enable/disable, F=cms/auth"
|
|
30
|
+
puts " ofa type NAME - Set application type: portfolio, blog, landing_page"
|
|
31
|
+
puts " ofa theme NAME - Set UI theme: light_glass, dark_glass"
|
|
32
|
+
puts " ofa storage NAME - Set image storage: local, cloudinary"
|
|
33
|
+
puts " ofa reset-password USR PWD - Reset admin account password"
|
|
34
|
+
puts " ofa db switch TYPE [NAME] - Switch DB: sqlite, mysql, mariadb, mongodb, postgres"
|
|
35
|
+
puts " ofa db migrate - Run database migrations"
|
|
36
|
+
puts " ofa migrate - Run database migrations (alias for db migrate)"
|
|
37
|
+
puts " ofa run - Start the application server"
|
|
38
|
+
puts " ofa deploy - Deploy project to production"
|
|
39
|
+
puts "-----------------------------"
|
|
40
|
+
exit
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def run_migrations
|
|
44
|
+
Object.const_set(:APP_ROOT, PROJECT_ROOT) unless defined?(APP_ROOT)
|
|
45
|
+
require File.join(FRAMEWORK_ROOT, 'config', 'boot')
|
|
46
|
+
puts "Running migrations..."
|
|
47
|
+
migration_path = File.join(PROJECT_ROOT, "db/migrations")
|
|
48
|
+
FileUtils.mkdir_p(migration_path)
|
|
49
|
+
|
|
50
|
+
if defined?(DB) && DB
|
|
51
|
+
if Dir.glob(File.join(migration_path, '*.rb')).empty?
|
|
52
|
+
puts "No custom migrations found in #{migration_path}."
|
|
53
|
+
puts "Core tables are automatically created. You're ready to go!"
|
|
54
|
+
else
|
|
55
|
+
Sequel.extension :migration
|
|
56
|
+
Sequel::Migrator.run(DB, migration_path)
|
|
57
|
+
puts "Migrations completed."
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def ensure_initialized!
|
|
63
|
+
config_path = File.join(PROJECT_ROOT, 'config', 'features.json')
|
|
64
|
+
unless File.exist?(config_path)
|
|
65
|
+
puts "❌ Error: Project not initialized in this folder."
|
|
66
|
+
puts " Please run 'ofa init' first."
|
|
67
|
+
exit 1
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
command = ARGV.shift
|
|
72
|
+
help if command.nil? || ['help', '-h', '--help'].include?(command)
|
|
73
|
+
|
|
74
|
+
case command
|
|
75
|
+
when 'new'
|
|
76
|
+
project_name = ARGV.shift
|
|
77
|
+
app_type = ARGV.shift || 'landing_page'
|
|
78
|
+
if project_name.nil? || project_name.empty?
|
|
79
|
+
puts "Usage: ofa new PROJECT_NAME [TYPE]"
|
|
80
|
+
exit 1
|
|
81
|
+
end
|
|
82
|
+
target_dir = File.join(PROJECT_ROOT, project_name)
|
|
83
|
+
if Dir.exist?(target_dir)
|
|
84
|
+
puts "❌ Folder '#{project_name}' already exists."
|
|
85
|
+
exit 1
|
|
86
|
+
end
|
|
87
|
+
FileUtils.mkdir_p(target_dir)
|
|
88
|
+
puts "✨ Creating new project '#{project_name}' (#{app_type})..."
|
|
89
|
+
# Delegate to 'init' logic by re-invoking ofa in the new directory using absolute path
|
|
90
|
+
absolute_ofa_path = File.expand_path(__FILE__)
|
|
91
|
+
system("cd #{target_dir} && ruby #{absolute_ofa_path} init #{app_type}")
|
|
92
|
+
puts "📦 Running bundle install..."
|
|
93
|
+
success = system("cd #{target_dir} && bundle install --quiet")
|
|
94
|
+
if success
|
|
95
|
+
puts ""
|
|
96
|
+
puts "✅ Project '#{project_name}' is ready!"
|
|
97
|
+
puts " cd #{project_name} && ofa run"
|
|
98
|
+
else
|
|
99
|
+
puts "⚠️ bundle install failed. Please run it manually in folder #{project_name}."
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
when 'init'
|
|
103
|
+
app_type = ARGV.shift || 'landing_page'
|
|
104
|
+
puts "Initializing One-For-All project as '#{app_type}' in #{PROJECT_ROOT}..."
|
|
105
|
+
|
|
106
|
+
# --- Interactive Wizard ---
|
|
107
|
+
puts "\n🛠️ Project Configuration (Press Enter for default)"
|
|
108
|
+
|
|
109
|
+
# 1. Database Configuration
|
|
110
|
+
print "💾 Choose Database [1. SQLite (default), 2. MongoDB Atlas]: "
|
|
111
|
+
db_choice = STDIN.gets.chomp
|
|
112
|
+
db_type = db_choice == '2' ? 'mongodb' : 'sqlite'
|
|
113
|
+
|
|
114
|
+
db_url = ""
|
|
115
|
+
if db_type == 'mongodb'
|
|
116
|
+
print "🔗 Enter MongoDB Connection String: "
|
|
117
|
+
db_url = STDIN.gets.chomp
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# 2. Image Storage Configuration
|
|
121
|
+
print "🖼️ Choose Image Storage [1. Local (default), 2. Cloudinary]: "
|
|
122
|
+
img_choice = STDIN.gets.chomp
|
|
123
|
+
img_storage = img_choice == '2' ? 'cloudinary' : 'local'
|
|
124
|
+
|
|
125
|
+
cloudinary_url = ""
|
|
126
|
+
if img_storage == 'cloudinary'
|
|
127
|
+
print "☁️ Enter Cloudinary URL (cloudinary://KEY:SECRET@NAME): "
|
|
128
|
+
cloudinary_url = STDIN.gets.chomp
|
|
129
|
+
end
|
|
130
|
+
puts "-----------------------------\n"
|
|
131
|
+
|
|
132
|
+
# Create directories
|
|
133
|
+
dirs = ['app/controllers', 'app/models', 'app/views', 'config', 'db/migrations', 'public/css', 'public/js', 'public/img', 'public/img/uploads']
|
|
134
|
+
dirs.each { |dir| FileUtils.mkdir_p(File.join(PROJECT_ROOT, dir)) }
|
|
135
|
+
|
|
136
|
+
# Copy framework core migrations
|
|
137
|
+
framework_migrations = File.join(FRAMEWORK_ROOT, 'db/migrations')
|
|
138
|
+
project_migrations = File.join(PROJECT_ROOT, 'db/migrations')
|
|
139
|
+
if File.directory?(framework_migrations)
|
|
140
|
+
Dir.glob(File.join(framework_migrations, '*.rb')).each do |mig|
|
|
141
|
+
FileUtils.cp(mig, project_migrations)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Create .env
|
|
146
|
+
env_content = []
|
|
147
|
+
env_content << "EKS_ENV=development"
|
|
148
|
+
env_content << "DATABASE_URL=#{db_url}" if db_type == 'mongodb'
|
|
149
|
+
env_content << "CLOUDINARY_URL=#{cloudinary_url}" if img_storage == 'cloudinary'
|
|
150
|
+
File.write(File.join(PROJECT_ROOT, '.env'), env_content.join("\n"))
|
|
151
|
+
|
|
152
|
+
# Create config.ru
|
|
153
|
+
puts " Creating system files..."
|
|
154
|
+
config_ru_content = <<~RUBY
|
|
155
|
+
# Generated by One-For-All CLI
|
|
156
|
+
# Target framework: #{FRAMEWORK_ROOT}
|
|
157
|
+
|
|
158
|
+
# Add framework to load path
|
|
159
|
+
$LOAD_PATH.unshift '#{FRAMEWORK_ROOT}'
|
|
160
|
+
APP_ROOT = File.expand_path(__dir__)
|
|
161
|
+
require 'config/boot'
|
|
162
|
+
|
|
163
|
+
# Middleware setup
|
|
164
|
+
use EksCent::Middleware::ShowExceptions
|
|
165
|
+
use EksCent::Middleware::Session, secret: EksCent.secret_key_base
|
|
166
|
+
use AuthMiddleware
|
|
167
|
+
use CSRFMiddleware
|
|
168
|
+
use EksCent::Middleware::Static, root: 'public'
|
|
169
|
+
use EksCent::Middleware::Logger
|
|
170
|
+
|
|
171
|
+
run ROUTES
|
|
172
|
+
RUBY
|
|
173
|
+
File.write(File.join(PROJECT_ROOT, 'config.ru'), config_ru_content)
|
|
174
|
+
|
|
175
|
+
# Create Gemfile
|
|
176
|
+
gemfile_content = <<~RUBY
|
|
177
|
+
source 'https://rubygems.org'
|
|
178
|
+
|
|
179
|
+
gem 'eks-cent', '4.0.0'
|
|
180
|
+
gem 'eksa-server'
|
|
181
|
+
gem 'sequel'
|
|
182
|
+
gem 'sqlite3'
|
|
183
|
+
gem 'bcrypt'
|
|
184
|
+
gem 'dotenv'
|
|
185
|
+
gem 'cloudinary'
|
|
186
|
+
gem 'mongo'
|
|
187
|
+
gem 'mysql2'
|
|
188
|
+
gem 'kramdown'
|
|
189
|
+
gem 'kramdown-parser-gfm'
|
|
190
|
+
RUBY
|
|
191
|
+
File.write(File.join(PROJECT_ROOT, 'Gemfile'), gemfile_content)
|
|
192
|
+
|
|
193
|
+
# Create base ApplicationController
|
|
194
|
+
app_controller_content = <<~'RUBY'
|
|
195
|
+
class ApplicationController
|
|
196
|
+
attr_reader :req, :res
|
|
197
|
+
|
|
198
|
+
def initialize(req, res)
|
|
199
|
+
@req = req
|
|
200
|
+
@res = res
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def params
|
|
204
|
+
@req.params
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def session
|
|
208
|
+
@req.env['eks_cent.session'] ||= {}
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def render(template, **locals)
|
|
212
|
+
# Pass controller instance variables to the view
|
|
213
|
+
instance_variables.each do |var|
|
|
214
|
+
locals[var.to_s.delete('@').to_sym] ||= instance_variable_get(var)
|
|
215
|
+
end
|
|
216
|
+
@res.render(template, **locals)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def redirect_to(path)
|
|
220
|
+
@res.status = 302
|
|
221
|
+
@res.headers['Location'] = path
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Validation Helper
|
|
225
|
+
def validate!(required_params)
|
|
226
|
+
missing = required_params.select { |p| params[p.to_s].nil? || params[p.to_s].empty? }
|
|
227
|
+
unless missing.empty?
|
|
228
|
+
@res.status = 400
|
|
229
|
+
render 'error', message: "Missing required parameters: #{missing.join(', ')}"
|
|
230
|
+
throw(:halt)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# CSRF Helper for views
|
|
235
|
+
def csrf_token
|
|
236
|
+
@req.env['eks_cent.csrf_token']
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
RUBY
|
|
240
|
+
File.write(File.join(PROJECT_ROOT, 'app/controllers/application_controller.rb'), app_controller_content)
|
|
241
|
+
|
|
242
|
+
# Setup features.json
|
|
243
|
+
config_path = File.join(PROJECT_ROOT, 'config', 'features.json')
|
|
244
|
+
unless File.exist?(config_path)
|
|
245
|
+
default_config = {
|
|
246
|
+
"auth" => true,
|
|
247
|
+
"cms" => true,
|
|
248
|
+
"type" => app_type,
|
|
249
|
+
"theme" => "dark_glass",
|
|
250
|
+
"storage" => img_storage
|
|
251
|
+
}
|
|
252
|
+
File.write(config_path, JSON.pretty_generate(default_config))
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Setup database.json
|
|
256
|
+
db_config_path = File.join(PROJECT_ROOT, 'config', 'database.json')
|
|
257
|
+
unless File.exist?(db_config_path)
|
|
258
|
+
if db_type == 'mongodb'
|
|
259
|
+
db_default = { "adapter" => "env" }
|
|
260
|
+
else
|
|
261
|
+
db_default = { "adapter" => "sqlite", "database" => "db/development.sqlite3" }
|
|
262
|
+
end
|
|
263
|
+
File.write(db_config_path, JSON.pretty_generate(db_default))
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Premium Scaffolding
|
|
267
|
+
puts " Generating premium UI components..."
|
|
268
|
+
# Copy views from framework to project
|
|
269
|
+
framework_views = File.join(FRAMEWORK_ROOT, 'app', 'views')
|
|
270
|
+
project_views = File.join(PROJECT_ROOT, 'app', 'views')
|
|
271
|
+
|
|
272
|
+
# Recursive copy for views (including cms folder)
|
|
273
|
+
FileUtils.cp_r(File.join(framework_views, '.'), project_views)
|
|
274
|
+
|
|
275
|
+
# Copy models from framework to project
|
|
276
|
+
framework_models = File.join(FRAMEWORK_ROOT, 'app', 'models')
|
|
277
|
+
project_models = File.join(PROJECT_ROOT, 'app', 'models')
|
|
278
|
+
FileUtils.cp_r(File.join(framework_models, '.'), project_models)
|
|
279
|
+
|
|
280
|
+
# Copy controllers from framework to project
|
|
281
|
+
framework_controllers = File.join(FRAMEWORK_ROOT, 'app', 'controllers')
|
|
282
|
+
project_controllers = File.join(PROJECT_ROOT, 'app', 'controllers')
|
|
283
|
+
# Filter out specific internal framework controllers if needed, but here we want them
|
|
284
|
+
Dir.glob(File.join(framework_controllers, '*.rb')).each do |file|
|
|
285
|
+
# Skip ApplicationController because we generated a custom one for the project
|
|
286
|
+
next if File.basename(file) == 'application_controller.rb'
|
|
287
|
+
FileUtils.cp(file, project_controllers)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Ensure public/css exists but we no longer rely on style.css for layout
|
|
291
|
+
css_dir = File.join(PROJECT_ROOT, 'public/css')
|
|
292
|
+
FileUtils.mkdir_p(css_dir)
|
|
293
|
+
File.write(File.join(css_dir, 'style.css'), "/* Custom styles here. Framework uses layout.erb for core UI. */")
|
|
294
|
+
|
|
295
|
+
# Scaffolding based on type
|
|
296
|
+
case app_type
|
|
297
|
+
when 'blog'
|
|
298
|
+
puts " Scaffolding Blog starter..."
|
|
299
|
+
File.write(File.join(PROJECT_ROOT, 'app/controllers/posts_controller.rb'), <<~RUBY)
|
|
300
|
+
require_relative 'application_controller'
|
|
301
|
+
class PostsController < ApplicationController
|
|
302
|
+
def index; end
|
|
303
|
+
end
|
|
304
|
+
RUBY
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
puts "Done! Run 'ofa run' to start your app."
|
|
308
|
+
|
|
309
|
+
when 'g'
|
|
310
|
+
ensure_initialized!
|
|
311
|
+
sub = ARGV.shift
|
|
312
|
+
name = ARGV.shift
|
|
313
|
+
if sub == 'controller' && name
|
|
314
|
+
class_name = name.capitalize + "Controller"
|
|
315
|
+
file_name = File.join(PROJECT_ROOT, "app/controllers/#{name.downcase}_controller.rb")
|
|
316
|
+
content = <<~RUBY
|
|
317
|
+
require_relative 'application_controller'
|
|
318
|
+
|
|
319
|
+
class #{class_name} < ApplicationController
|
|
320
|
+
def index
|
|
321
|
+
# Logic for #{name}
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
RUBY
|
|
325
|
+
File.write(file_name, content)
|
|
326
|
+
puts "Created: #{file_name}"
|
|
327
|
+
elsif sub == 'model' && name
|
|
328
|
+
class_name = name.capitalize
|
|
329
|
+
file_name = File.join(PROJECT_ROOT, "app/models/#{name.downcase}.rb")
|
|
330
|
+
content = <<~RUBY
|
|
331
|
+
class #{class_name} < Sequel::Model
|
|
332
|
+
end
|
|
333
|
+
RUBY
|
|
334
|
+
File.write(file_name, content)
|
|
335
|
+
puts "Created: #{file_name}"
|
|
336
|
+
elsif sub == 'migration' && name
|
|
337
|
+
version = Time.now.strftime('%Y%m%d%H%M%S')
|
|
338
|
+
dir_path = File.join(PROJECT_ROOT, "db/migrations")
|
|
339
|
+
FileUtils.mkdir_p(dir_path)
|
|
340
|
+
file_name = File.join(dir_path, "#{version}_#{name.downcase}.rb")
|
|
341
|
+
content = <<~RUBY
|
|
342
|
+
Sequel.migration do
|
|
343
|
+
change do
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
RUBY
|
|
347
|
+
File.write(file_name, content)
|
|
348
|
+
puts "Created migration: #{file_name}"
|
|
349
|
+
elsif sub == 'post' && name
|
|
350
|
+
options = {}
|
|
351
|
+
OptionParser.new do |opts|
|
|
352
|
+
opts.on("--category CAT") { |v| options[:category] = v }
|
|
353
|
+
opts.on("--author AUTH") { |v| options[:author] = v }
|
|
354
|
+
opts.on("--image IMG") { |v| options[:image] = v }
|
|
355
|
+
end.parse!(ARGV)
|
|
356
|
+
dir_path = File.join(PROJECT_ROOT, "app/views/posts")
|
|
357
|
+
FileUtils.mkdir_p(dir_path)
|
|
358
|
+
file_name = File.join(dir_path, "#{name.downcase.gsub(' ', '_')}.erb")
|
|
359
|
+
content = "<h1>#{name}</h1>\n<p>Author: #{options[:author] || 'Admin'}</p>\n<p>Category: #{options[:category] || 'General'}</p>"
|
|
360
|
+
File.write(file_name, content)
|
|
361
|
+
puts "Created post: #{file_name}"
|
|
362
|
+
else
|
|
363
|
+
puts "Usage: ofa g [controller|model|migration|post] NAME"
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
when 'feature'
|
|
367
|
+
ensure_initialized!
|
|
368
|
+
action = ARGV.shift
|
|
369
|
+
feature = ARGV.shift
|
|
370
|
+
config_path = File.join(PROJECT_ROOT, 'config', 'features.json')
|
|
371
|
+
config = JSON.parse(File.read(config_path))
|
|
372
|
+
config[feature] = (action == 'enable')
|
|
373
|
+
File.write(config_path, JSON.pretty_generate(config))
|
|
374
|
+
puts "Feature '#{feature}' has been #{action}d."
|
|
375
|
+
|
|
376
|
+
when 'type'
|
|
377
|
+
ensure_initialized!
|
|
378
|
+
name = ARGV.shift
|
|
379
|
+
unless %w[landing_page portfolio blog].include?(name)
|
|
380
|
+
puts "❌ Error: Invalid app type '#{name}'. Choose: landing_page, portfolio, blog."
|
|
381
|
+
exit 1
|
|
382
|
+
end
|
|
383
|
+
config_path = File.join(PROJECT_ROOT, 'config', 'features.json')
|
|
384
|
+
config = JSON.parse(File.read(config_path))
|
|
385
|
+
config['type'] = name
|
|
386
|
+
File.write(config_path, JSON.pretty_generate(config))
|
|
387
|
+
puts "Application type set to '#{name}'."
|
|
388
|
+
|
|
389
|
+
when 'theme'
|
|
390
|
+
ensure_initialized!
|
|
391
|
+
name = ARGV.shift
|
|
392
|
+
unless %w[light_glass dark_glass].include?(name)
|
|
393
|
+
puts "❌ Error: Invalid theme '#{name}'. Choose: light_glass, dark_glass."
|
|
394
|
+
exit 1
|
|
395
|
+
end
|
|
396
|
+
config_path = File.join(PROJECT_ROOT, 'config', 'features.json')
|
|
397
|
+
config = JSON.parse(File.read(config_path))
|
|
398
|
+
config['theme'] = name
|
|
399
|
+
File.write(config_path, JSON.pretty_generate(config))
|
|
400
|
+
puts "Application theme set to '#{name}'."
|
|
401
|
+
|
|
402
|
+
when 'storage'
|
|
403
|
+
ensure_initialized!
|
|
404
|
+
name = ARGV.shift
|
|
405
|
+
unless %w[local cloudinary].include?(name)
|
|
406
|
+
puts "❌ Error: Invalid storage type '#{name}'. Choose: local, cloudinary."
|
|
407
|
+
exit 1
|
|
408
|
+
end
|
|
409
|
+
config_path = File.join(PROJECT_ROOT, 'config', 'features.json')
|
|
410
|
+
config = JSON.parse(File.read(config_path))
|
|
411
|
+
config['storage'] = name
|
|
412
|
+
File.write(config_path, JSON.pretty_generate(config))
|
|
413
|
+
puts "Application storage set to '#{name}'."
|
|
414
|
+
|
|
415
|
+
when 'reset-password'
|
|
416
|
+
Object.const_set(:APP_ROOT, PROJECT_ROOT) unless defined?(APP_ROOT)
|
|
417
|
+
require File.join(FRAMEWORK_ROOT, 'config', 'boot')
|
|
418
|
+
user_name = ARGV.shift
|
|
419
|
+
new_pwd = ARGV.shift
|
|
420
|
+
if user_name.nil? || new_pwd.nil?
|
|
421
|
+
puts "Usage: ofa reset-password USERNAME PASSWORD"
|
|
422
|
+
exit 1
|
|
423
|
+
end
|
|
424
|
+
begin
|
|
425
|
+
user = User.find(username: user_name) rescue nil
|
|
426
|
+
if user
|
|
427
|
+
user.password = new_pwd
|
|
428
|
+
user.save
|
|
429
|
+
puts "✅ Password for '#{user_name}' has been reset."
|
|
430
|
+
else
|
|
431
|
+
user = User.new(username: user_name)
|
|
432
|
+
user.password = new_pwd
|
|
433
|
+
user.save
|
|
434
|
+
puts "✅ User '#{user_name}' created with the provided password."
|
|
435
|
+
end
|
|
436
|
+
rescue ArgumentError => e
|
|
437
|
+
puts "❌ Error: #{e.message}"
|
|
438
|
+
puts " Requirements: min 8 characters, one uppercase, one number."
|
|
439
|
+
exit 1
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
when 'migrate'
|
|
443
|
+
run_migrations
|
|
444
|
+
|
|
445
|
+
when 'db'
|
|
446
|
+
sub = ARGV.shift
|
|
447
|
+
config_path = File.join(PROJECT_ROOT, 'config', 'database.json')
|
|
448
|
+
case sub
|
|
449
|
+
when 'switch'
|
|
450
|
+
type = ARGV.shift
|
|
451
|
+
db_name = ARGV.shift
|
|
452
|
+
case type
|
|
453
|
+
when 'env'
|
|
454
|
+
config = { "adapter" => "env" }
|
|
455
|
+
when 'sqlite'
|
|
456
|
+
config = { "adapter" => "sqlite", "database" => db_name || "db/development.sqlite3" }
|
|
457
|
+
else
|
|
458
|
+
config = { "adapter" => type, "host" => "localhost", "user" => "root", "password" => "", "database" => db_name || "one_for_all" }
|
|
459
|
+
end
|
|
460
|
+
File.write(config_path, JSON.pretty_generate(config))
|
|
461
|
+
puts "Switched to #{type} mode."
|
|
462
|
+
when 'migrate'
|
|
463
|
+
run_migrations
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
when 'run'
|
|
467
|
+
ensure_initialized!
|
|
468
|
+
# Check for config.ru
|
|
469
|
+
unless File.exist?(File.join(PROJECT_ROOT, 'config.ru'))
|
|
470
|
+
puts "Error: config.ru not found. Ensure you are in the project root or run 'ofa init'."
|
|
471
|
+
exit 1
|
|
472
|
+
end
|
|
473
|
+
puts "Starting One-For-All server..."
|
|
474
|
+
exec "bundle exec eksa-server"
|
|
475
|
+
|
|
476
|
+
when 'deploy'
|
|
477
|
+
puts "🚀 Starting Deployment process..."
|
|
478
|
+
puts "-----------------------------"
|
|
479
|
+
|
|
480
|
+
# 1. Check for Git
|
|
481
|
+
unless system("git rev-parse --is-inside-work-tree > /dev/null 2>&1")
|
|
482
|
+
puts "❌ Error: Project not initialized with Git."
|
|
483
|
+
puts " Please run 'git init' and commit your changes first."
|
|
484
|
+
exit 1
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# 2. Check for Railway CLI as an example of deployment target
|
|
488
|
+
if system("railway --version > /dev/null 2>&1")
|
|
489
|
+
puts "📦 Railway CLI detected. Deploying to Railway..."
|
|
490
|
+
exec "railway up"
|
|
491
|
+
else
|
|
492
|
+
puts "⚠️ Deployment target not detected (Railway/Vercel)."
|
|
493
|
+
puts " Suggestion: Install Railway CLI or prepare a Dockerfile for VPS."
|
|
494
|
+
puts " This framework supports deployment via Docker or Git Push."
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
else
|
|
498
|
+
help
|
|
499
|
+
end
|