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/config/boot.rb
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
require 'bundler/setup'
|
|
2
|
+
require 'dotenv/load'
|
|
3
|
+
Bundler.require(:default)
|
|
4
|
+
require 'eks-cent'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'kramdown'
|
|
7
|
+
|
|
8
|
+
# Basic project structure constants
|
|
9
|
+
APP_ROOT ||= File.expand_path('..', __dir__)
|
|
10
|
+
|
|
11
|
+
# Environment
|
|
12
|
+
EKS_ENV = ENV['EKS_ENV'] || 'development'
|
|
13
|
+
|
|
14
|
+
# Load features config
|
|
15
|
+
FEATURES_CONFIG = JSON.parse(File.read(File.join(APP_ROOT, 'config', 'features.json')))
|
|
16
|
+
|
|
17
|
+
# Load Locales (from framework)
|
|
18
|
+
LOCALES = {
|
|
19
|
+
'en' => JSON.parse(File.read(File.join(__dir__, 'locales', 'en.json'))),
|
|
20
|
+
'id' => JSON.parse(File.read(File.join(__dir__, 'locales', 'id.json')))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Monkeypatch EksCent
|
|
24
|
+
module EksCent
|
|
25
|
+
def self.secret_key_base
|
|
26
|
+
ENV['SECRET_KEY_BASE'] || 'd8a8b8c8d8e8f8g8h8i8j8k8l8m8n8o8p8q8r8s8t8u8v8w8x8y8z8'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class Response
|
|
30
|
+
def render(template_name, layout: true, **locals)
|
|
31
|
+
require 'erb'
|
|
32
|
+
template_path = File.join(APP_ROOT, 'app', 'views', "#{template_name}.erb")
|
|
33
|
+
unless File.file?(template_path)
|
|
34
|
+
raise "Template not found: #{template_path}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
template_content = File.read(template_path)
|
|
38
|
+
context = Object.new
|
|
39
|
+
context.extend(ERB::Util)
|
|
40
|
+
req = @request
|
|
41
|
+
context.instance_variable_set("@req", req)
|
|
42
|
+
context.instance_variable_set("@res", self)
|
|
43
|
+
|
|
44
|
+
# I18n Helper
|
|
45
|
+
lang = (req && req.params['lang']) || 'en'
|
|
46
|
+
context.define_singleton_method(:t) { |key| LOCALES[lang][key.to_s] || key }
|
|
47
|
+
context.define_singleton_method(:csrf_tag) { "<input type='hidden' name='csrf_token' value='#{req.env['eks_cent.csrf_token']}'>" }
|
|
48
|
+
context.define_singleton_method(:csrf_token) { req ? req.env['eks_cent.csrf_token'] : nil }
|
|
49
|
+
context.define_singleton_method(:markdown) { |text| Kramdown::Document.new(text.to_s, input: 'GFM').to_html }
|
|
50
|
+
|
|
51
|
+
context.define_singleton_method(:session) { req ? (req.env['eks_cent.session'] || req.env['rack.session'] || {}) : {} }
|
|
52
|
+
context.define_singleton_method(:h) { |s| CGI.escapeHTML(s.to_s) }
|
|
53
|
+
locals.each { |k, v| context.instance_variable_set("@#{k}", v) }
|
|
54
|
+
|
|
55
|
+
result = ERB.new(template_content).result(context.instance_eval { binding })
|
|
56
|
+
|
|
57
|
+
if layout
|
|
58
|
+
layout_name = layout == true ? 'layout' : layout.to_s
|
|
59
|
+
layout_path = File.join(APP_ROOT, 'app', 'views', "#{layout_name}.erb")
|
|
60
|
+
if File.file?(layout_path)
|
|
61
|
+
context.instance_variable_set("@content", result)
|
|
62
|
+
layout_content = File.read(layout_path)
|
|
63
|
+
result = ERB.new(layout_content).result(context.instance_eval { binding })
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@headers['Content-Type'] ||= 'text/html'
|
|
68
|
+
@body << result
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Load core framework components
|
|
74
|
+
require_relative 'database'
|
|
75
|
+
|
|
76
|
+
# Autoloading (Framework Core first, then APP_ROOT)
|
|
77
|
+
framework_app = File.expand_path('../app', __dir__)
|
|
78
|
+
['controllers', 'models', 'middleware', 'helpers'].each do |folder|
|
|
79
|
+
loaded = []
|
|
80
|
+
# Load framework core
|
|
81
|
+
Dir.glob(File.join(framework_app, folder, '*.rb')).each do |f|
|
|
82
|
+
require f
|
|
83
|
+
loaded << File.basename(f)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Load project specific (if not already loaded by framework)
|
|
87
|
+
if framework_app != APP_ROOT
|
|
88
|
+
Dir.glob(File.join(APP_ROOT, 'app', folder, '*.rb')).each do |f|
|
|
89
|
+
require f unless loaded.include?(File.basename(f))
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# ─── Routes DSL Extensions ─────────────────────────────────────────────────────
|
|
95
|
+
# Extends EksCent::Router with a `resources` helper that auto-generates RESTful
|
|
96
|
+
# routes (index, show, new, create, edit, update, destroy) for a given resource.
|
|
97
|
+
#
|
|
98
|
+
# Usage in config/routes.rb:
|
|
99
|
+
# resources :posts # All 7 RESTful routes
|
|
100
|
+
# resources :posts, only: [:index, :show]
|
|
101
|
+
module EksCent
|
|
102
|
+
class Router
|
|
103
|
+
# Generate RESTful routes for a resource
|
|
104
|
+
# Maps to a controller: e.g. :posts => PostsController
|
|
105
|
+
def resources(name, only: nil, prefix: nil)
|
|
106
|
+
controller_name = name.to_s.split('_').map(&:capitalize).join + "Controller"
|
|
107
|
+
plural = name.to_s
|
|
108
|
+
path_prefix = prefix ? "#{prefix}/#{plural}" : "/#{plural}"
|
|
109
|
+
|
|
110
|
+
all_actions = {
|
|
111
|
+
index: { method: :get, path: path_prefix },
|
|
112
|
+
new: { method: :get, path: "#{path_prefix}/new" },
|
|
113
|
+
show: { method: :get, path: "#{path_prefix}/:id" },
|
|
114
|
+
create: { method: :post, path: path_prefix },
|
|
115
|
+
edit: { method: :get, path: "#{path_prefix}/:id/edit" },
|
|
116
|
+
update: { method: :post, path: "#{path_prefix}/:id/update" },
|
|
117
|
+
destroy: { method: :post, path: "#{path_prefix}/:id/delete" },
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
actions = only ? all_actions.slice(*Array(only)) : all_actions
|
|
121
|
+
|
|
122
|
+
actions.each do |action, config|
|
|
123
|
+
send(config[:method], config[:path]) do |req, res|
|
|
124
|
+
controller_class = Object.const_get(controller_name)
|
|
125
|
+
controller_class.new(req, res).public_send(action)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
# ───────────────────────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
# Load routes
|
|
134
|
+
require_relative 'routes'
|
data/config/database.rb
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
require 'sequel'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
# Load config from JSON
|
|
6
|
+
config_file = File.join(APP_ROOT, 'config', 'database.json')
|
|
7
|
+
DB_CONFIG = JSON.parse(File.read(config_file)) rescue { "adapter" => "sqlite", "database" => "db/development.sqlite3" }
|
|
8
|
+
|
|
9
|
+
# Decide if we should use Environment Variable or JSON Config
|
|
10
|
+
# Priority:
|
|
11
|
+
# 1. If database.json adapter is 'env', STRICTLY use DATABASE_URL from .env
|
|
12
|
+
# 2. Otherwise, STRICTLY use JSON config (this allows local dev to override .env)
|
|
13
|
+
|
|
14
|
+
use_env = (DB_CONFIG['adapter'] == 'env')
|
|
15
|
+
database_url = ENV['DATABASE_URL'] if use_env
|
|
16
|
+
|
|
17
|
+
if use_env && database_url
|
|
18
|
+
if database_url.start_with?('mongodb')
|
|
19
|
+
require 'mongo'
|
|
20
|
+
begin
|
|
21
|
+
MONGO_CLIENT = Mongo::Client.new(database_url)
|
|
22
|
+
FileUtils.mkdir_p('db')
|
|
23
|
+
DB = Sequel.connect("sqlite://db/data.sqlite3")
|
|
24
|
+
puts "ℹ INFO: MongoDB Atlas connected via ENV." unless ENV['EKS_ENV'] == 'test'
|
|
25
|
+
rescue => e
|
|
26
|
+
puts "⚠ WARNING: Failed to connect to MongoDB Atlas. #{e.message}"
|
|
27
|
+
FileUtils.mkdir_p('db')
|
|
28
|
+
DB = Sequel.connect("sqlite://db/data.sqlite3")
|
|
29
|
+
end
|
|
30
|
+
else
|
|
31
|
+
begin
|
|
32
|
+
DB = Sequel.connect(database_url)
|
|
33
|
+
rescue => e
|
|
34
|
+
puts "⚠ WARNING: Connection failed to DATABASE_URL. Fallback to memory."
|
|
35
|
+
FileUtils.mkdir_p('db')
|
|
36
|
+
DB = Sequel.connect("sqlite://db/data.sqlite3")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
else
|
|
40
|
+
# Use JSON config (SQLite, MySQL, etc.)
|
|
41
|
+
case DB_CONFIG['adapter']
|
|
42
|
+
when 'sqlite'
|
|
43
|
+
db_path = DB_CONFIG['database'] || 'db/development.sqlite3'
|
|
44
|
+
FileUtils.mkdir_p('db') unless Dir.exist?('db')
|
|
45
|
+
DB = Sequel.connect("sqlite://#{db_path}")
|
|
46
|
+
when 'mongodb'
|
|
47
|
+
begin
|
|
48
|
+
require 'mongo'
|
|
49
|
+
MONGO_CLIENT = Mongo::Client.new([ DB_CONFIG['host'] || '127.0.0.1:27017' ], database: DB_CONFIG['database'])
|
|
50
|
+
FileUtils.mkdir_p('db')
|
|
51
|
+
DB = Sequel.connect("sqlite://db/data.sqlite3")
|
|
52
|
+
rescue => e
|
|
53
|
+
FileUtils.mkdir_p('db')
|
|
54
|
+
DB = Sequel.connect("sqlite://db/data.sqlite3")
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
begin
|
|
58
|
+
# For MySQL, MariaDB, Postgres
|
|
59
|
+
DB = Sequel.connect("#{DB_CONFIG['adapter']}://#{DB_CONFIG['user']}:#{DB_CONFIG['password']}@#{DB_CONFIG['host']}/#{DB_CONFIG['database']}")
|
|
60
|
+
rescue => e
|
|
61
|
+
FileUtils.mkdir_p('db')
|
|
62
|
+
DB = Sequel.connect("sqlite://db/data.sqlite3")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if DB
|
|
68
|
+
DB.extension :pagination
|
|
69
|
+
unless DB.table_exists?(:users)
|
|
70
|
+
DB.create_table :users do
|
|
71
|
+
primary_key :id
|
|
72
|
+
String :username, unique: true, null: false
|
|
73
|
+
String :password_hash, null: false
|
|
74
|
+
String :avatar_url
|
|
75
|
+
DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
unless DB.table_exists?(:pages)
|
|
80
|
+
DB.create_table :pages do
|
|
81
|
+
primary_key :id
|
|
82
|
+
String :title, null: false
|
|
83
|
+
String :slug, unique: true, null: false
|
|
84
|
+
String :content, text: true
|
|
85
|
+
TrueClass :is_active, default: true
|
|
86
|
+
TrueClass :is_nav, default: true
|
|
87
|
+
DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
unless DB.table_exists?(:posts)
|
|
92
|
+
DB.create_table :posts do
|
|
93
|
+
primary_key :id
|
|
94
|
+
String :title, null: false
|
|
95
|
+
String :slug, unique: true, null: false
|
|
96
|
+
String :content, text: true
|
|
97
|
+
String :image_url
|
|
98
|
+
String :category
|
|
99
|
+
TrueClass :is_active, default: true
|
|
100
|
+
DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
unless DB.table_exists?(:projects)
|
|
105
|
+
DB.create_table :projects do
|
|
106
|
+
primary_key :id
|
|
107
|
+
String :title, null: false
|
|
108
|
+
String :slug, unique: true, null: false
|
|
109
|
+
String :description, text: true
|
|
110
|
+
String :link
|
|
111
|
+
String :image_url
|
|
112
|
+
TrueClass :is_active, default: true
|
|
113
|
+
DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Quick Migration for existing tables
|
|
118
|
+
if DB.table_exists?(:pages)
|
|
119
|
+
DB.alter_table(:pages) { add_column :is_active, TrueClass, default: true unless DB[:pages].columns.include?(:is_active) }
|
|
120
|
+
DB.alter_table(:pages) { add_column :is_nav, TrueClass, default: true unless DB[:pages].columns.include?(:is_nav) }
|
|
121
|
+
end
|
|
122
|
+
if DB.table_exists?(:posts)
|
|
123
|
+
DB.alter_table(:posts) { add_column :is_active, TrueClass, default: true unless DB[:posts].columns.include?(:is_active) }
|
|
124
|
+
end
|
|
125
|
+
if DB.table_exists?(:projects)
|
|
126
|
+
DB.alter_table(:projects) { add_column :is_active, TrueClass, default: true unless DB[:projects].columns.include?(:is_active) }
|
|
127
|
+
DB.alter_table(:projects) { add_column :slug, String, unique: true unless DB[:projects].columns.include?(:slug) }
|
|
128
|
+
end
|
|
129
|
+
# Seed default content if empty
|
|
130
|
+
if DB[:pages].count == 0
|
|
131
|
+
DB[:pages].insert(title: 'About Me', slug: 'about', content: '<h1>About Me</h1><p>Welcome to my website.</p>')
|
|
132
|
+
DB[:pages].insert(title: 'Contact', slug: 'contact', content: '<h1>Contact Me</h1><p>Email: admin@example.com</p>')
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
if DB[:posts].count == 0
|
|
136
|
+
DB[:posts].insert(title: 'Hello World', slug: 'hello-world', content: 'This is my first post.', category: 'General')
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
if DB[:projects].count == 0
|
|
140
|
+
DB[:projects].insert(title: 'My First Project', slug: 'my-first-project', description: 'Description of my first project.', link: 'https://github.com')
|
|
141
|
+
end
|
|
142
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require 'eks-cent'
|
|
2
|
+
|
|
3
|
+
ROUTES = EksCent::Router.new do
|
|
4
|
+
# Main Pages - Dynamic based on Type
|
|
5
|
+
get '/' do |req, res|
|
|
6
|
+
type = FEATURES_CONFIG['type'] || 'landing_page'
|
|
7
|
+
case type
|
|
8
|
+
when 'portfolio'
|
|
9
|
+
projects = Project.where(is_active: true).order(Sequel.desc(:created_at)).all
|
|
10
|
+
res.render 'portfolio', title: "Portfolio - One-For-All", projects: projects
|
|
11
|
+
when 'blog'
|
|
12
|
+
posts = Post.where(is_active: true).order(Sequel.desc(:created_at)).all
|
|
13
|
+
res.render 'blog_home', title: "Blog - One-For-All", posts: posts
|
|
14
|
+
else
|
|
15
|
+
res.render 'index', title: "One-For-All Framework"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
get '/docs' do |req, res|
|
|
20
|
+
res.render 'docs', title: "Documentation - One-For-All"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# --- CMS Dashboard ---
|
|
24
|
+
get '/dashboard' do |req, res|
|
|
25
|
+
DashboardController.new(req, res).index
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
post '/dashboard/upload' do |req, res|
|
|
29
|
+
DashboardController.new(req, res).upload
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Resourceful CMS Routes
|
|
33
|
+
resources :pages, prefix: '/dashboard'
|
|
34
|
+
resources :posts, prefix: '/dashboard'
|
|
35
|
+
resources :projects, prefix: '/dashboard'
|
|
36
|
+
|
|
37
|
+
# Auth Routes
|
|
38
|
+
get '/login' do |req, res|
|
|
39
|
+
AuthController.new(req, res).show_login
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
post '/login' do |req, res|
|
|
43
|
+
AuthController.new(req, res).login
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
post '/logout' do |req, res|
|
|
47
|
+
AuthController.new(req, res).logout
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# API Namespace
|
|
51
|
+
namespace '/api' do
|
|
52
|
+
get '/status' do |req, res|
|
|
53
|
+
ApiController.new(req, res).status
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
# --- Dynamic Content ---
|
|
57
|
+
get '/posts/:slug' do |req, res|
|
|
58
|
+
post = Post.find(slug: req.params['slug'], is_active: true)
|
|
59
|
+
if post
|
|
60
|
+
res.render('post', title: post.title, post: post)
|
|
61
|
+
else
|
|
62
|
+
res.status = 404
|
|
63
|
+
res.render('error', layout: false, message: "Article not found.")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
get '/projects/:slug' do |req, res|
|
|
68
|
+
project = Project.find(slug: req.params['slug'], is_active: true)
|
|
69
|
+
if project
|
|
70
|
+
res.render('project', title: project.title, project: project)
|
|
71
|
+
else
|
|
72
|
+
res.status = 404
|
|
73
|
+
res.render('error', layout: false, message: "Project not found.")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# --- Dynamic Pages (Catch-All) ---
|
|
78
|
+
get '/:slug' do |req, res|
|
|
79
|
+
page = Page.find(slug: req.params['slug'], is_active: true)
|
|
80
|
+
if page
|
|
81
|
+
res.render('page', title: page.title, content: page.content)
|
|
82
|
+
else
|
|
83
|
+
res.status = 404
|
|
84
|
+
res.render('error', layout: false, message: "Page '#{req.params['slug']}' not found.")
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/config.eks
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require_relative 'config/boot'
|
|
2
|
+
|
|
3
|
+
# Middleware setup
|
|
4
|
+
use EksCent::Middleware::ShowExceptions
|
|
5
|
+
use EksCent::Middleware::Session, secret: EksCent.secret_key_base
|
|
6
|
+
use AuthMiddleware
|
|
7
|
+
use CSRFMiddleware
|
|
8
|
+
use EksCent::Middleware::Static, root: 'public'
|
|
9
|
+
use EksCent::Middleware::Logger
|
|
10
|
+
|
|
11
|
+
run ROUTES
|
data/config.ru
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require_relative 'config/boot'
|
|
2
|
+
|
|
3
|
+
# Middleware setup
|
|
4
|
+
use EksCent::Middleware::ShowExceptions
|
|
5
|
+
use EksCent::Middleware::Session, secret: EksCent.secret_key_base
|
|
6
|
+
use AuthMiddleware
|
|
7
|
+
use CSRFMiddleware
|
|
8
|
+
use EksCent::Middleware::Static, root: 'public'
|
|
9
|
+
use EksCent::Middleware::Logger
|
|
10
|
+
|
|
11
|
+
run ROUTES
|
data/db/data.sqlite3
ADDED
|
Binary file
|
|
Binary file
|
data/public/css/cms.css
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--cms-primary: #6366f1;
|
|
3
|
+
--cms-bg: #0f172a;
|
|
4
|
+
--cms-card: rgba(30, 41, 59, 0.7);
|
|
5
|
+
--cms-border: rgba(255, 255, 255, 0.1);
|
|
6
|
+
--cms-text: #f8fafc;
|
|
7
|
+
--cms-text-muted: #94a3b8;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.cms-layout {
|
|
11
|
+
display: grid;
|
|
12
|
+
grid-template-columns: 280px 1fr;
|
|
13
|
+
gap: 2rem;
|
|
14
|
+
min-height: 100vh;
|
|
15
|
+
padding: 2rem;
|
|
16
|
+
background: var(--cms-bg);
|
|
17
|
+
color: var(--cms-text);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.cms-sidebar {
|
|
21
|
+
background: var(--cms-card);
|
|
22
|
+
backdrop-filter: blur(20px);
|
|
23
|
+
border: 1px solid var(--cms-border);
|
|
24
|
+
border-radius: 1.5rem;
|
|
25
|
+
padding: 2rem;
|
|
26
|
+
height: calc(100vh - 4rem);
|
|
27
|
+
position: sticky;
|
|
28
|
+
top: 2rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.sidebar-header h3 {
|
|
32
|
+
font-size: 1.5rem;
|
|
33
|
+
margin-bottom: 2.5rem;
|
|
34
|
+
font-weight: 800;
|
|
35
|
+
}
|
|
36
|
+
.sidebar-header span { color: var(--cms-primary); }
|
|
37
|
+
|
|
38
|
+
.sidebar-nav {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
gap: 0.75rem;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.sidebar-nav a {
|
|
45
|
+
padding: 1rem 1.5rem;
|
|
46
|
+
border-radius: 1rem;
|
|
47
|
+
text-decoration: none;
|
|
48
|
+
color: var(--cms-text-muted);
|
|
49
|
+
font-weight: 600;
|
|
50
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.sidebar-nav a:hover {
|
|
54
|
+
background: rgba(255, 255, 255, 0.05);
|
|
55
|
+
color: var(--cms-text);
|
|
56
|
+
transform: translateX(5px);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.sidebar-nav a.active {
|
|
60
|
+
background: var(--cms-primary);
|
|
61
|
+
color: white;
|
|
62
|
+
box-shadow: 0 10px 20px -5px rgba(99, 102, 241, 0.5);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.nav-divider {
|
|
66
|
+
height: 1px;
|
|
67
|
+
background: var(--cms-border);
|
|
68
|
+
margin: 1.5rem 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.cms-main {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
gap: 2rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.cms-header h2 { font-size: 2.5rem; font-weight: 800; }
|
|
78
|
+
.cms-header p { color: var(--cms-text-muted); }
|
|
79
|
+
|
|
80
|
+
.stats-grid {
|
|
81
|
+
display: grid;
|
|
82
|
+
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
83
|
+
gap: 1.5rem;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.stat-card {
|
|
87
|
+
background: var(--cms-card);
|
|
88
|
+
border: 1px solid var(--cms-border);
|
|
89
|
+
padding: 2rem;
|
|
90
|
+
border-radius: 1.5rem;
|
|
91
|
+
text-align: center;
|
|
92
|
+
transition: transform 0.3s;
|
|
93
|
+
}
|
|
94
|
+
.stat-card:hover { transform: translateY(-5px); }
|
|
95
|
+
.stat-card h4 { color: var(--cms-text-muted); font-size: 1rem; margin-bottom: 0.5rem; }
|
|
96
|
+
.stat-card .value { font-size: 3rem; font-weight: 800; color: var(--cms-primary); }
|
|
97
|
+
|
|
98
|
+
.glass-card {
|
|
99
|
+
background: var(--cms-card);
|
|
100
|
+
backdrop-filter: blur(20px);
|
|
101
|
+
border: 1px solid var(--cms-border);
|
|
102
|
+
border-radius: 1.5rem;
|
|
103
|
+
padding: 2rem;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.header-with-action {
|
|
107
|
+
display: flex;
|
|
108
|
+
justify-content: space-between;
|
|
109
|
+
align-items: center;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Table Styles */
|
|
113
|
+
.table-container { padding: 0; overflow: hidden; }
|
|
114
|
+
table { width: 100%; border-collapse: collapse; }
|
|
115
|
+
th { text-align: left; padding: 1.5rem; color: var(--cms-text-muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 1px; background: rgba(0,0,0,0.2); }
|
|
116
|
+
td { padding: 1.5rem; border-bottom: 1px solid var(--cms-border); }
|
|
117
|
+
|
|
118
|
+
.table-actions { display: flex; gap: 1rem; }
|
|
119
|
+
.logout-btn {
|
|
120
|
+
background: rgba(239, 68, 68, 0.1);
|
|
121
|
+
border: 1px solid #ef4444;
|
|
122
|
+
color: #ef4444;
|
|
123
|
+
padding: 0.8rem;
|
|
124
|
+
border-radius: 1rem;
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
width: 100%;
|
|
127
|
+
font-weight: 700;
|
|
128
|
+
}
|
|
129
|
+
.logout-btn:hover { background: #ef4444; color: white; }
|
|
130
|
+
|
|
131
|
+
@media (max-width: 1024px) {
|
|
132
|
+
.cms-layout { grid-template-columns: 1fr; }
|
|
133
|
+
.cms-sidebar { height: auto; position: static; }
|
|
134
|
+
}
|
|
Binary file
|
|
Binary file
|