one-for-all-framework 1.0.0 → 3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7da16da0dd7fd64c3418a570d9d9c2f366fe487a464cd524214ead23b249a932
4
- data.tar.gz: 43e95c18a2a0adc29c1c1fb39f95e175a11db8dd9fb70979b9ee04156d49c6f7
3
+ metadata.gz: 26eab5d2f3279aef8aaf3e4f4f69af60d01c535a8a4ba2f41a82615f77e02732
4
+ data.tar.gz: a6760ad95277f885fff52a45427049cb87f3a59e584dc09fffa533c4b7b57bd5
5
5
  SHA512:
6
- metadata.gz: 7e770e771c8cfd8e8e2c7d62a6b46e3a06cada5144a60a35c3d5348a06040a59158de1c2b9c807026c982fd74ea4c1e0f1be8e114f36f91b420955706a9b2e50
7
- data.tar.gz: 28c6bc05a32ddf5af5c78e74939adc3886b811c31a812e7fcba9d1b6b437f8f34bc7858917e1f7ef8463dbe93c93c05b7b8927e074b03838cbe1966fc7760761
6
+ metadata.gz: ac089f1a856bf5b31ffea2561db5a48d2fed567b14be86245c7962965595d9fb80209472d2427a4c10c4f15d17ed48c839897bb3141d0a46f860f43d7014d44b
7
+ data.tar.gz: 9f10a41d2e81293e8cfd363253d08fbb1bd1dc595e97cc646b31ce2c36e30807ee385772c9ba903e98e70bec3205e5b265f0a694cdc3d62aaba12e488639eed0
data/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
  <img src="public/images/logo.png" width="500" height="500" alt="OFA Framework Logo">
3
3
  </p>
4
4
 
5
- # ⚡ One-For-All (OFA) Framework
5
+ # ⚡ One-For-All (OFA) Framework v3.0.0
6
6
 
7
7
  [![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%203.0.0-red.svg)](https://www.ruby-lang.org/)
8
8
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
  [![Framework](https://img.shields.io/badge/MVC-Lightweight-orange.svg)]()
10
10
 
11
- **One-For-All (OFA)** is a premium, ultra-fast Ruby MVC framework designed for developers who value both high performance and modern aesthetics. Built on the powerful **Eks-Cent** engine and optimized with **Eksa Server**, OFA provides a production-ready foundation with a stunning "Glassmorphism" UI out of the box.
11
+ **One-For-All (OFA)** is a premium, ultra-fast Ruby MVC framework designed for developers who value both high performance and modern aesthetics. Built on the powerful **Eks-Cent** engine and optimized with **Eksa Server**, OFA v3.0 now supports **Full E-Commerce integration** alongside its stunning "Glassmorphism" UI.
12
12
 
13
13
  ---
14
14
 
@@ -52,20 +52,50 @@ Your app is now live at `http://localhost:3000` ⚡
52
52
 
53
53
  ---
54
54
 
55
- ## 🛠️ CLI Power Tools
55
+ ## 🛠️ CLI Power Tools (Detailed Reference)
56
56
 
57
- The `ofa` CLI is your best friend. Use it to manage your entire application lifecycle:
57
+ The `ofa` CLI is the heart of the One-For-All framework. It handles everything from project initialization to production deployment.
58
58
 
59
+ ### 📁 Project Lifecycle
59
60
  | Command | Description |
60
61
  | :--- | :--- |
61
- | `ofa new NAME` | Create a brand new project from scratch. |
62
- | `ofa g controller Name` | Generate a RESTful controller. |
63
- | `ofa g model Name` | Generate a database model and migration. |
64
- | `ofa db migrate` | Sync your database with the latest schema. |
65
- | `ofa type [blog\|portfolio]` | Switch application mode instantly. |
66
- | `ofa theme [dark\|light]` | Toggle between premium UI themes. |
67
- | `ofa storage cloudinary` | Enable automated Cloudinary image hosting. |
68
- | `ofa reset-password USR PWD`| Securely reset admin credentials. |
62
+ | `ofa new NAME [TYPE]` | **Create a new project.** Generates a new directory, initializes the framework structure, and automatically runs `bundle install`. <br> *Example:* `./ofa new my_portfolio portfolio` |
63
+ | `ofa init [TYPE]` | **Initialize in current folder.** Ideal if you've already created a folder or cloned a repository. It triggers an **Interactive Wizard** to configure your Database (SQLite/MongoDB) and Image Storage (Local/Cloudinary). |
64
+ | `ofa run` | **Start Development Server.** Boots the high-performance Eksa Server. Your app will be accessible at `http://localhost:3000`. |
65
+ | `ofa deploy` | **Production Deployment.** Automatically detects deployment targets. <br> 1. Checks if it's a Git repository. <br> 2. Detects **Railway CLI** and triggers `railway up`. <br> 3. Supports Docker via the included `Dockerfile`. |
66
+
67
+ ---
68
+
69
+ ### 🏗️ Scaffolding & Generators (`ofa g`)
70
+ Automate the creation of boilerplate code with the generator command.
71
+
72
+ | Command | Description |
73
+ | :--- | :--- |
74
+ | `ofa g controller NAME` | Creates a new controller in `app/controllers/{name}_controller.rb` with a default `index` action. |
75
+ | `ofa g model NAME` | Generates a database model in `app/models/{name}.rb` integrated with the Sequel ORM. |
76
+ | `ofa g migration NAME` | Creates a timestamped migration file in `db/migrations/`. Use this to define your schema changes. |
77
+ | `ofa g post TITLE` | Creates a new Markdown/ERB post in `app/views/posts/`. <br> *Args:* `--category`, `--author`, `--image`. <br> *Example:* `./ofa g post "My First Journey" --category Tech --author "John Doe"` |
78
+
79
+ ---
80
+
81
+ ### 🎨 Configuration & Customization
82
+ Fine-tune your application's behavior and appearance without touching the code.
83
+
84
+ | Command | Description |
85
+ | :--- | :--- |
86
+ | `ofa type NAME` | **Set Application Type.** Switches the layout logic between `portfolio`, `blog`, `landing_page`, and `e_commerce`. |
87
+ | `ofa theme NAME` | **Change UI Aesthetic.** Instantly swap between premium themes: <br> • `light_glass` / `dark_glass` (Modern Glassmorphism) <br> • `cyber_sidebar` (High-tech) <br> • `retro_terminal` (Old-school hacker vibe) <br> • `light_sidebar` (Professional/Clean) |
88
+ | `ofa feature ACTION FEATURE`| **Toggle Core Features.** Enable or disable system modules. <br> *Usage:* `./ofa feature enable auth` or `./ofa feature disable cms`. |
89
+ | `ofa storage NAME` | **Set Media Storage.** Choose between `local` (uploads folder) or `cloudinary` (Cloud storage). |
90
+
91
+ ---
92
+
93
+ ### 🔐 Security & Database
94
+ | Command | Description |
95
+ | :--- | :--- |
96
+ | `ofa reset-password USR PWD`| **User Management.** Resets a password for an existing admin or creates a new one. <br> *Note:* Enforces strong password rules (8+ chars, 1 uppercase, 1 number). |
97
+ | `ofa db switch ADAPTER` | **Hot-swap Database.** Configure your adapter on the fly: `sqlite`, `mysql`, `mariadb`, `postgres`, or `env` (for MongoDB Atlas). |
98
+ | `ofa db migrate` | **Database Sync.** Runs all pending migrations in `db/migrations/` to keep your schema up to date. |
69
99
 
70
100
  ---
71
101
 
@@ -80,13 +110,41 @@ OFA follows a strict **MVC (Model-View-Controller)** pattern:
80
110
 
81
111
  ---
82
112
 
83
- ## 🚢 Deployment
84
-
85
- One-For-All is optimized for modern cloud platforms:
86
-
87
- - **Railway / Heroku**: Uses the included `Procfile` for automatic detection.
88
- - **Docker**: A lightweight `Dockerfile` based on `ruby:3.2-slim` is provided.
89
- - **VPS**: Can be run behind Nginx/Apache using the `ofa run` command.
113
+ ## 🚢 Deployment Guide
114
+
115
+ One-For-All is designed to be cloud-native and "deploy-ready" from day one.
116
+
117
+ ### 🚂 Railway (Recommended)
118
+ Railway is the easiest way to get your OFA app live. The framework includes a `Procfile` that Railway detects automatically.
119
+ 1. Install [Railway CLI](https://docs.railway.app/guides/cli).
120
+ 2. Run `railway login`.
121
+ 3. In your project folder, run:
122
+ ```bash
123
+ ./ofa deploy
124
+ ```
125
+ *The CLI will automatically trigger `railway up` and handle the build process.*
126
+
127
+ ### 🐳 Docker
128
+ For customized hosting or VPS providers, use the optimized `Dockerfile`.
129
+ 1. **Build the image**:
130
+ ```bash
131
+ docker build -t my-ofa-app .
132
+ ```
133
+ 2. **Run the container**:
134
+ ```bash
135
+ docker run -p 3000:3000 --env-file .env my-ofa-app
136
+ ```
137
+ *Note: Ensure your `.env` contains production-ready database credentials.*
138
+
139
+ ### 🖥️ VPS (DigitalOcean, Linode, AWS)
140
+ To run OFA on a raw Linux server:
141
+ 1. **Setup**: Clone your repo and run `bundle install --deployment`.
142
+ 2. **Database**: Run `./ofa db migrate` to sync your production schema.
143
+ 3. **Process Management**: Use [PM2](https://pm2.keymetrics.io/) to keep the server alive:
144
+ ```bash
145
+ pm2 start "./ofa run" --name ofa-app
146
+ ```
147
+ 4. **Reverse Proxy**: We recommend using **Nginx** as a reverse proxy to handle SSL and port 80/443 forwarding to port 3000.
90
148
 
91
149
  ---
92
150
 
@@ -9,7 +9,7 @@ class ApiController < ApplicationController
9
9
  end
10
10
 
11
11
  def status
12
- render_json({ status: 'ok', version: '1.0.0', type: FEATURES_CONFIG['type'] })
12
+ render_json({ status: 'ok', version: '2.0.0', type: FEATURES_CONFIG['type'] })
13
13
  end
14
14
 
15
15
  def halt_error(message, status: 400)
@@ -58,7 +58,7 @@ class ApplicationController
58
58
  rescue => e
59
59
  puts "⚠ Error deleting from Cloudinary: #{e.message}"
60
60
  end
61
- elsif url.start_with?('/img/uploads/')
61
+ elsif url.start_with?('/images/uploads/')
62
62
  path = File.join(APP_ROOT, 'public', url)
63
63
  FileUtils.rm(path) if File.exist?(path) rescue nil
64
64
  end
@@ -71,7 +71,7 @@ class ApplicationController
71
71
  urls.each { |url| delete_from_storage(url) }
72
72
 
73
73
  # Also find local uploads
74
- local_urls = content.scan(/\/img\/uploads\/[^\s\)]+/)
74
+ local_urls = content.scan(/\/images\/uploads\/[^\s\)]+/)
75
75
  local_urls.each { |url| delete_from_storage(url) }
76
76
  end
77
77
  end
@@ -0,0 +1,52 @@
1
+ require_relative 'application_controller'
2
+
3
+ class CartController < ApplicationController
4
+ def index
5
+ @cart_items = session['cart'] || {}
6
+ # Convert string keys to integers for the query
7
+ @products = Product.where(id: @cart_items.keys.map(&:to_i)).all
8
+ @total = @products.sum { |p| p.price * @cart_items[p.id.to_s].to_i }
9
+ render 'cart_index', title: "Your Cart - One-For-All"
10
+ end
11
+
12
+ def add
13
+ product_id = params['product_id']
14
+ qty = params['qty'].to_i
15
+ qty = 1 if qty <= 0
16
+
17
+ cart = session['cart'] || {}
18
+ cart[product_id] = (cart[product_id].to_i + qty)
19
+ session['cart'] = cart
20
+
21
+ redirect_to '/cart'
22
+ end
23
+
24
+ def remove
25
+ product_id = params['product_id']
26
+ cart = session['cart'] || {}
27
+ cart.delete(product_id)
28
+ session['cart'] = cart
29
+
30
+ redirect_to '/cart'
31
+ end
32
+
33
+ def update
34
+ product_id = params['product_id']
35
+ qty = params['qty'].to_i
36
+
37
+ cart = session['cart'] || {}
38
+ if qty <= 0
39
+ cart.delete(product_id)
40
+ else
41
+ cart[product_id] = qty
42
+ end
43
+ session['cart'] = cart
44
+
45
+ redirect_to '/cart'
46
+ end
47
+
48
+ def clear
49
+ session['cart'] = {}
50
+ redirect_to '/cart'
51
+ end
52
+ end
@@ -7,7 +7,8 @@ class DashboardController < ApplicationController
7
7
  @stats = {
8
8
  pages: Page.count,
9
9
  posts: Post.count,
10
- projects: Project.count
10
+ projects: Project.count,
11
+ products: defined?(Product) ? Product.count : 0
11
12
  }
12
13
  render 'dashboard'
13
14
  end
@@ -27,9 +28,9 @@ class DashboardController < ApplicationController
27
28
  end
28
29
  else
29
30
  filename = "#{Time.now.to_i}_#{file[:filename]}"
30
- target = File.join(APP_ROOT, 'public/img/uploads', filename)
31
+ target = File.join(APP_ROOT, 'public/images/uploads', filename)
31
32
  FileUtils.cp(file[:tempfile].path, target)
32
- @res.body << { url: "/img/uploads/#{filename}" }.to_json
33
+ @res.body << { url: "/images/uploads/#{filename}" }.to_json
33
34
  end
34
35
  else
35
36
  @res.status = 400
@@ -0,0 +1,58 @@
1
+ require_relative 'application_controller'
2
+
3
+ class ProductsController < ApplicationController
4
+ def index
5
+ if req.path.start_with?('/dashboard')
6
+ @products = Product.order(Sequel.desc(:created_at)).all
7
+ render 'cms/products_index'
8
+ else
9
+ @products = Product.where(is_active: true).order(Sequel.desc(:created_at)).all
10
+ render 'products_index', title: "Shop - One-For-All"
11
+ end
12
+ end
13
+
14
+ def new
15
+ @product = Product.new
16
+ render 'cms/products_form'
17
+ end
18
+
19
+ def create
20
+ data = params['product']
21
+ @product = Product.new(data)
22
+ if @product.save
23
+ redirect_to '/dashboard/products'
24
+ else
25
+ render 'cms/products_form'
26
+ end
27
+ end
28
+
29
+ def edit
30
+ @product = Product[params['id']]
31
+ render 'cms/products_form'
32
+ end
33
+
34
+ def update
35
+ @product = Product[params['id']]
36
+ if @product.update(params['product'])
37
+ redirect_to '/dashboard/products'
38
+ else
39
+ render 'cms/products_form'
40
+ end
41
+ end
42
+
43
+ def destroy
44
+ @product = Product[params['id']]
45
+ @product.destroy
46
+ redirect_to '/dashboard/products'
47
+ end
48
+
49
+ def show
50
+ @product = Product.find(slug: params['slug'], is_active: true)
51
+ if @product
52
+ render 'product_show', title: "#{@product.name} - One-For-All"
53
+ else
54
+ @res.status = 404
55
+ render 'error', message: "Product not found."
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,8 @@
1
+ class Product < Sequel::Model
2
+ plugin :timestamps, update_on_create: true
3
+
4
+ def before_save
5
+ self.slug ||= self.name.downcase.gsub(/[^a-z0-9]/, '-')
6
+ super
7
+ end
8
+ end
@@ -0,0 +1,112 @@
1
+ <div class="text-center mb-16">
2
+ <h1 class="text-5xl font-black mb-4 tracking-tighter">Your Shopping Cart</h1>
3
+ <p class="text-slate-500 text-lg">Review your items before checkout.</p>
4
+ </div>
5
+
6
+ <div class="glass-panel overflow-hidden mb-10">
7
+ <!-- Desktop Header (Hidden on Mobile) -->
8
+ <div class="hidden md:grid grid-cols-12 bg-white/5 border-b border-white/5 text-slate-400 font-bold uppercase text-xs tracking-widest px-8 py-4">
9
+ <div class="col-span-5">Product</div>
10
+ <div class="col-span-2">Price</div>
11
+ <div class="col-span-2 text-center">Quantity</div>
12
+ <div class="col-span-2 text-right">Subtotal</div>
13
+ <div class="col-span-1"></div>
14
+ </div>
15
+
16
+ <div class="divide-y divide-white/5">
17
+ <% @products.each do |product| %>
18
+ <% qty = @cart_items[product.id.to_s].to_i %>
19
+ <div class="grid grid-cols-1 md:grid-cols-12 items-center gap-4 md:gap-0 p-6 md:px-8 md:py-6 hover:bg-white/[0.02] transition-colors relative group">
20
+ <!-- Product Info -->
21
+ <div class="col-span-1 md:col-span-5">
22
+ <div class="flex items-center gap-4">
23
+ <div class="w-16 h-16 md:w-20 md:h-20 rounded-2xl overflow-hidden bg-slate-900/50 flex-shrink-0">
24
+ <img src="<%= product.image_url || '/images/logo.png' %>" class="w-full h-full object-cover">
25
+ </div>
26
+ <div>
27
+ <div class="font-black text-lg md:text-base"><%= product.name %></div>
28
+ <div class="text-xs text-slate-500 uppercase tracking-tighter"><%= product.category %></div>
29
+ </div>
30
+ </div>
31
+ </div>
32
+
33
+ <!-- Price (Label on Mobile) -->
34
+ <div class="col-span-1 md:col-span-2 flex justify-between md:block items-center">
35
+ <span class="md:hidden text-slate-500 text-sm font-bold uppercase tracking-widest">Price</span>
36
+ <span class="font-mono text-slate-400">$<%= product.price %></span>
37
+ </div>
38
+
39
+ <!-- Quantity -->
40
+ <div class="col-span-1 md:col-span-2 flex justify-between md:block items-center">
41
+ <span class="md:hidden text-slate-500 text-sm font-bold uppercase tracking-widest">Quantity</span>
42
+ <div class="flex items-center justify-center gap-2">
43
+ <form action="/cart/update" method="POST" class="inline">
44
+ <%= csrf_tag %>
45
+ <input type="hidden" name="product_id" value="<%= product.id %>">
46
+ <input type="hidden" name="qty" value="<%= qty - 1 %>">
47
+ <button type="submit" class="w-10 h-10 md:w-8 md:h-8 rounded-xl bg-white/5 hover:bg-red-500/10 hover:text-red-500 transition-all flex items-center justify-center">
48
+ <i class="fas fa-minus text-xs"></i>
49
+ </button>
50
+ </form>
51
+ <span class="w-8 text-center font-black text-lg md:text-base"><%= qty %></span>
52
+ <form action="/cart/update" method="POST" class="inline">
53
+ <%= csrf_tag %>
54
+ <input type="hidden" name="product_id" value="<%= product.id %>">
55
+ <input type="hidden" name="qty" value="<%= qty + 1 %>">
56
+ <button type="submit" class="w-10 h-10 md:w-8 md:h-8 rounded-xl bg-white/5 hover:bg-primary/10 hover:text-primary transition-all flex items-center justify-center">
57
+ <i class="fas fa-plus text-xs"></i>
58
+ </button>
59
+ </form>
60
+ </div>
61
+ </div>
62
+
63
+ <!-- Subtotal -->
64
+ <div class="col-span-1 md:col-span-2 flex justify-between md:block items-center border-t md:border-none border-white/5 pt-4 md:pt-0">
65
+ <span class="md:hidden text-slate-500 text-sm font-bold uppercase tracking-widest">Subtotal</span>
66
+ <span class="text-xl md:text-lg md:text-right block font-black text-primary">$<%= product.price * qty %></span>
67
+ </div>
68
+
69
+ <!-- Remove Btn -->
70
+ <div class="absolute top-6 right-6 md:static md:col-span-1 md:text-right">
71
+ <form action="/cart/remove" method="POST">
72
+ <%= csrf_tag %>
73
+ <input type="hidden" name="product_id" value="<%= product.id %>">
74
+ <button type="submit" class="w-10 h-10 md:w-auto flex items-center justify-center md:inline text-slate-400 hover:text-red-500 transition-colors">
75
+ <i class="fas fa-trash-alt"></i>
76
+ </button>
77
+ </form>
78
+ </div>
79
+ </div>
80
+ <% end %>
81
+
82
+ <% if @products.empty? %>
83
+ <div class="px-8 py-20 text-center">
84
+ <div class="w-20 h-20 bg-slate-900/20 rounded-full flex items-center justify-center mx-auto mb-6">
85
+ <i class="fas fa-shopping-basket text-4xl text-slate-700"></i>
86
+ </div>
87
+ <div class="text-slate-500 italic text-lg">Your cart is empty.</div>
88
+ <a href="/" class="btn-premium inline-flex mt-6 px-8">Start Shopping</a>
89
+ </div>
90
+ <% end %>
91
+ </div>
92
+ </div>
93
+
94
+ <% unless @products.empty? %>
95
+ <div class="flex flex-col md:flex-row justify-between items-end gap-8">
96
+ <form action="/cart/clear" method="POST">
97
+ <%= csrf_tag %>
98
+ <button type="submit" class="btn-secondary">Clear Cart</button>
99
+ </form>
100
+
101
+ <div class="glass-card w-full md:w-96 !p-8">
102
+ <div class="flex justify-between items-center mb-6">
103
+ <span class="text-slate-500 font-bold">Total Amount</span>
104
+ <span class="text-3xl font-black text-primary">$<%= @total %></span>
105
+ </div>
106
+ <button class="btn-premium w-full !py-5 !text-xl">
107
+ Proceed to Checkout <i class="fas fa-arrow-right ml-2"></i>
108
+ </button>
109
+ <p class="text-[10px] text-center text-slate-600 mt-4 uppercase tracking-widest font-black">Secure Checkout Powered by OFA.SYS</p>
110
+ </div>
111
+ </div>
112
+ <% end %>
@@ -44,7 +44,7 @@
44
44
  </aside>
45
45
 
46
46
  <!-- Main Content -->
47
- <div class="flex-1 space-y-6 w-full pb-20">
47
+ <div class="flex-1 space-y-6 pb-20 min-w-0">
48
48
  <div class="flex justify-between items-center bg-white/5 p-6 rounded-3xl border border-white/5">
49
49
  <div>
50
50
  <h2 class="text-2xl font-black tracking-tight"><%= @page.id ? 'Edit' : 'Add' %> Page</h2>
@@ -29,6 +29,11 @@
29
29
  <i class="fas fa-palette w-5"></i>
30
30
  <span class="font-semibold">Projects</span>
31
31
  </a>
32
+ <% elsif FEATURES_CONFIG['type'] == 'e_commerce' %>
33
+ <a href="/dashboard/products" class="flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 <%= @req.path.start_with?('/dashboard/products') ? 'bg-primary text-white shadow-lg shadow-primary/30' : 'text-slate-500 hover:bg-white/5' %>">
34
+ <i class="fas fa-tag w-5"></i>
35
+ <span class="font-semibold">Products</span>
36
+ </a>
32
37
  <% end %>
33
38
 
34
39
  <div class="h-px bg-white/5 my-4"></div>
@@ -45,60 +50,71 @@
45
50
 
46
51
  <!-- Main Content -->
47
52
  <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">
53
+ <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 bg-white/5 p-6 rounded-3xl border border-white/5">
49
54
  <div>
50
55
  <h2 class="text-2xl font-black tracking-tight">Page Management</h2>
51
56
  <p class="text-slate-500 text-sm">Manage all static pages of your website.</p>
52
57
  </div>
53
- <a href="/dashboard/pages/new" class="btn-premium">
58
+ <a href="/dashboard/pages/new" class="btn-premium w-full sm:w-auto text-center">
54
59
  <i class="fas fa-plus mr-2 text-sm"></i> New Page
55
60
  </a>
56
61
  </div>
57
62
 
58
63
  <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>
64
+ <!-- Header (Hidden on Mobile) -->
65
+ <div class="hidden md:grid grid-cols-12 bg-white/5 border-b border-white/5 text-slate-400 font-bold uppercase text-[10px] tracking-[0.2em] px-8 py-4">
66
+ <div class="col-span-5">Page Title</div>
67
+ <div class="col-span-3">Slug</div>
68
+ <div class="col-span-2">Created</div>
69
+ <div class="col-span-2 text-right">Actions</div>
70
+ </div>
71
+
72
+ <div class="divide-y divide-white/5">
73
+ <% if @pages.empty? %>
74
+ <div class="px-8 py-20 text-center">
75
+ <div class="w-16 h-16 bg-white/5 rounded-full flex items-center justify-center mx-auto mb-4">
76
+ <i class="fas fa-file-circle-plus text-2xl text-slate-600"></i>
77
+ </div>
78
+ <div class="text-slate-500 italic">No pages created yet.</div>
79
+ </div>
80
+ <% end %>
81
+ <% @pages.each do |page| %>
82
+ <div class="grid grid-cols-1 md:grid-cols-12 items-center gap-4 md:gap-0 p-6 md:px-8 md:py-5 hover:bg-white/[0.02] transition-colors relative group">
83
+ <!-- Title -->
84
+ <div class="col-span-1 md:col-span-5">
85
+ <div class="flex flex-col">
86
+ <span class="font-black text-slate-700 dark:text-slate-200 group-hover:text-primary transition-colors text-lg md:text-base"><%= page.title %></span>
87
+ <span class="md:hidden text-[10px] text-slate-500 uppercase tracking-widest font-black mt-1">Page Title</span>
88
+ </div>
89
+ </div>
90
+
91
+ <!-- Slug -->
92
+ <div class="col-span-1 md:col-span-3 flex flex-col">
93
+ <code class="text-xs bg-primary/10 px-3 py-1.5 rounded-xl text-primary w-fit font-bold font-mono">/<%= page.slug %></code>
94
+ <span class="md:hidden text-[10px] text-slate-500 uppercase tracking-widest font-black mt-2">Route Slug</span>
95
+ </div>
96
+
97
+ <!-- Date -->
98
+ <div class="col-span-1 md:col-span-2 flex flex-col">
99
+ <span class="text-sm text-slate-500 font-bold"><%= page.created_at.strftime('%d %b %Y') %></span>
100
+ <span class="md:hidden text-[10px] text-slate-500 uppercase tracking-widest font-black mt-1">Creation Date</span>
101
+ </div>
102
+
103
+ <!-- Actions -->
104
+ <div class="col-span-1 md:col-span-2 flex justify-end gap-2 pt-4 md:pt-0 border-t md:border-none border-white/5">
105
+ <a href="/dashboard/pages/<%= page.id %>/edit" class="w-10 h-10 flex items-center justify-center rounded-2xl bg-primary/10 text-primary hover:bg-primary hover:text-white transition-all shadow-lg shadow-primary/10">
106
+ <i class="fas fa-edit text-sm"></i>
107
+ </a>
108
+ <form action="/dashboard/pages/<%= page.id %>/delete" method="POST" onsubmit="return confirm('Delete this page?')" class="inline">
109
+ <%= csrf_tag %>
110
+ <button type="submit" class="w-10 h-10 flex items-center justify-center rounded-2xl bg-red-500/10 text-red-500 hover:bg-red-500 hover:text-white transition-all shadow-lg shadow-red-500/10">
111
+ <i class="fas fa-trash-can text-sm"></i>
112
+ </button>
113
+ </form>
114
+ </div>
115
+ </div>
116
+ <% end %>
117
+ </div>
102
118
  </div>
103
119
  </div>
104
120
  </div>
@@ -44,7 +44,7 @@
44
44
  </aside>
45
45
 
46
46
  <!-- Main Content -->
47
- <div class="flex-1 space-y-6 w-full pb-20">
47
+ <div class="flex-1 space-y-6 pb-20 min-w-0">
48
48
  <div class="flex justify-between items-center bg-white/5 p-6 rounded-3xl border border-white/5">
49
49
  <div>
50
50
  <h2 class="text-2xl font-black tracking-tight"><%= @post.id ? 'Edit' : 'Add' %> Article</h2>