active_canvas 0.0.1
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/MIT-LICENSE +20 -0
- data/README.md +318 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/active_canvas/editor/ai_panel.js +1607 -0
- data/app/assets/javascripts/active_canvas/editor/asset_manager.js +498 -0
- data/app/assets/javascripts/active_canvas/editor/blocks.js +1083 -0
- data/app/assets/javascripts/active_canvas/editor/code_panel.js +572 -0
- data/app/assets/javascripts/active_canvas/editor/component_toolbar.js +394 -0
- data/app/assets/javascripts/active_canvas/editor/panels.js +460 -0
- data/app/assets/javascripts/active_canvas/editor/utils.js +56 -0
- data/app/assets/javascripts/active_canvas/editor.js +295 -0
- data/app/assets/stylesheets/active_canvas/application.css +15 -0
- data/app/assets/stylesheets/active_canvas/editor.css +2929 -0
- data/app/controllers/active_canvas/admin/ai_controller.rb +181 -0
- data/app/controllers/active_canvas/admin/application_controller.rb +56 -0
- data/app/controllers/active_canvas/admin/media_controller.rb +61 -0
- data/app/controllers/active_canvas/admin/page_types_controller.rb +57 -0
- data/app/controllers/active_canvas/admin/page_versions_controller.rb +23 -0
- data/app/controllers/active_canvas/admin/pages_controller.rb +133 -0
- data/app/controllers/active_canvas/admin/partials_controller.rb +88 -0
- data/app/controllers/active_canvas/admin/settings_controller.rb +256 -0
- data/app/controllers/active_canvas/application_controller.rb +20 -0
- data/app/controllers/active_canvas/pages_controller.rb +18 -0
- data/app/controllers/concerns/active_canvas/current_user.rb +12 -0
- data/app/controllers/concerns/active_canvas/rate_limitable.rb +75 -0
- data/app/controllers/concerns/active_canvas/tailwind_compilation.rb +39 -0
- data/app/helpers/active_canvas/application_helper.rb +4 -0
- data/app/jobs/active_canvas/application_job.rb +4 -0
- data/app/jobs/active_canvas/compile_tailwind_job.rb +64 -0
- data/app/mailers/active_canvas/application_mailer.rb +6 -0
- data/app/models/active_canvas/ai_model.rb +136 -0
- data/app/models/active_canvas/application_record.rb +5 -0
- data/app/models/active_canvas/media.rb +141 -0
- data/app/models/active_canvas/page.rb +85 -0
- data/app/models/active_canvas/page_type.rb +22 -0
- data/app/models/active_canvas/page_version.rb +80 -0
- data/app/models/active_canvas/partial.rb +73 -0
- data/app/models/active_canvas/setting.rb +292 -0
- data/app/services/active_canvas/ai_configuration.rb +40 -0
- data/app/services/active_canvas/ai_models.rb +128 -0
- data/app/services/active_canvas/ai_service.rb +289 -0
- data/app/services/active_canvas/content_sanitizer.rb +112 -0
- data/app/services/active_canvas/tailwind_compiler.rb +156 -0
- data/app/views/active_canvas/admin/media/index.html.erb +401 -0
- data/app/views/active_canvas/admin/media/show.html.erb +297 -0
- data/app/views/active_canvas/admin/page_types/_form.html.erb +25 -0
- data/app/views/active_canvas/admin/page_types/edit.html.erb +13 -0
- data/app/views/active_canvas/admin/page_types/index.html.erb +29 -0
- data/app/views/active_canvas/admin/page_types/new.html.erb +9 -0
- data/app/views/active_canvas/admin/page_types/show.html.erb +18 -0
- data/app/views/active_canvas/admin/page_versions/show.html.erb +469 -0
- data/app/views/active_canvas/admin/pages/_form.html.erb +62 -0
- data/app/views/active_canvas/admin/pages/content.html.erb +139 -0
- data/app/views/active_canvas/admin/pages/edit.html.erb +335 -0
- data/app/views/active_canvas/admin/pages/editor.html.erb +710 -0
- data/app/views/active_canvas/admin/pages/index.html.erb +149 -0
- data/app/views/active_canvas/admin/pages/new.html.erb +19 -0
- data/app/views/active_canvas/admin/pages/show.html.erb +258 -0
- data/app/views/active_canvas/admin/pages/versions.html.erb +333 -0
- data/app/views/active_canvas/admin/partials/edit.html.erb +182 -0
- data/app/views/active_canvas/admin/partials/editor.html.erb +703 -0
- data/app/views/active_canvas/admin/partials/index.html.erb +131 -0
- data/app/views/active_canvas/admin/settings/show.html.erb +1864 -0
- data/app/views/active_canvas/pages/no_homepage.html.erb +45 -0
- data/app/views/active_canvas/pages/show.html.erb +113 -0
- data/app/views/layouts/active_canvas/admin/application.html.erb +960 -0
- data/app/views/layouts/active_canvas/admin/editor.html.erb +826 -0
- data/app/views/layouts/active_canvas/application.html.erb +55 -0
- data/config/routes.rb +48 -0
- data/db/migrate/20260202000001_create_active_canvas_tables.rb +113 -0
- data/db/migrate/20260202000002_create_active_canvas_ai_models.rb +26 -0
- data/lib/active_canvas/configuration.rb +232 -0
- data/lib/active_canvas/engine.rb +44 -0
- data/lib/active_canvas/version.rb +3 -0
- data/lib/active_canvas.rb +26 -0
- data/lib/generators/active_canvas/install/install_generator.rb +263 -0
- data/lib/generators/active_canvas/install/templates/initializer.rb.tt +163 -0
- data/lib/tasks/active_canvas_tasks.rake +69 -0
- metadata +150 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
|
|
3
|
+
module ActiveCanvas
|
|
4
|
+
class TailwindCompiler
|
|
5
|
+
class CompilationError < StandardError; end
|
|
6
|
+
|
|
7
|
+
LOG_PREFIX = "[ActiveCanvas::TailwindCompiler]".freeze
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
# Compile Tailwind CSS for raw HTML content
|
|
11
|
+
def compile(html_content, identifier: "content")
|
|
12
|
+
log_info "Starting compilation for #{identifier}"
|
|
13
|
+
start_time = Time.current
|
|
14
|
+
|
|
15
|
+
unless available?
|
|
16
|
+
log_error "tailwindcss-ruby gem is not installed"
|
|
17
|
+
raise CompilationError, "tailwindcss-ruby gem is not installed"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if html_content.blank?
|
|
21
|
+
log_info "#{identifier} has no content, skipping compilation"
|
|
22
|
+
return ""
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
log_debug "Content size: #{html_content.bytesize} bytes"
|
|
26
|
+
|
|
27
|
+
Dir.mktmpdir("active_canvas_tailwind") do |dir|
|
|
28
|
+
log_debug "Created temp directory: #{dir}"
|
|
29
|
+
|
|
30
|
+
html_file = File.join(dir, "input.html")
|
|
31
|
+
css_file = File.join(dir, "output.css")
|
|
32
|
+
|
|
33
|
+
File.write(html_file, html_content)
|
|
34
|
+
log_debug "Wrote HTML content to #{html_file}"
|
|
35
|
+
|
|
36
|
+
compiled_css = compile_css(html_file, css_file, identifier)
|
|
37
|
+
|
|
38
|
+
elapsed = ((Time.current - start_time) * 1000).round(2)
|
|
39
|
+
log_info "Compilation completed for #{identifier} in #{elapsed}ms (output: #{compiled_css.bytesize} bytes)"
|
|
40
|
+
|
|
41
|
+
compiled_css
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def compile_for_page(page)
|
|
46
|
+
compile(page.content.to_s, identifier: "page ##{page.id} (#{page.title})")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def available?
|
|
50
|
+
# Always check fresh - no caching
|
|
51
|
+
defined?(Tailwindcss::Ruby) && Tailwindcss::Ruby.respond_to?(:executable)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def clear_availability_cache!
|
|
55
|
+
# No longer caches, but keep method for compatibility
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def compile_css(html_file, css_file, identifier)
|
|
61
|
+
executable = Tailwindcss::Ruby.executable
|
|
62
|
+
log_debug "Using Tailwind executable: #{executable}"
|
|
63
|
+
|
|
64
|
+
dir = File.dirname(html_file)
|
|
65
|
+
input_css_file = File.join(dir, "input.css")
|
|
66
|
+
|
|
67
|
+
# Build input CSS with Tailwind import and source directive
|
|
68
|
+
input_css = build_input_css(html_file)
|
|
69
|
+
File.write(input_css_file, input_css)
|
|
70
|
+
log_debug "Input CSS with config: #{input_css.lines.first(10).join.strip}..."
|
|
71
|
+
|
|
72
|
+
command = [
|
|
73
|
+
executable,
|
|
74
|
+
"--input", input_css_file,
|
|
75
|
+
"--output", css_file,
|
|
76
|
+
"--minify"
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
log_debug "Running command: #{command.join(' ')}"
|
|
80
|
+
|
|
81
|
+
compile_start = Time.current
|
|
82
|
+
stdout, stderr, status = Open3.capture3(*command)
|
|
83
|
+
compile_elapsed = ((Time.current - compile_start) * 1000).round(2)
|
|
84
|
+
|
|
85
|
+
log_debug "Tailwind CLI execution took #{compile_elapsed}ms"
|
|
86
|
+
|
|
87
|
+
if stdout.present?
|
|
88
|
+
log_debug "Tailwind stdout: #{stdout.truncate(500)}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
unless status.success?
|
|
92
|
+
log_error "Compilation failed for #{identifier} (exit code: #{status.exitstatus})"
|
|
93
|
+
log_error "Tailwind stderr: #{stderr}"
|
|
94
|
+
raise CompilationError, "Tailwind compilation failed: #{stderr}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
if stderr.present? && !stderr.strip.empty?
|
|
98
|
+
log_warn "Tailwind warnings for #{identifier}: #{stderr.truncate(500)}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
unless File.exist?(css_file)
|
|
102
|
+
log_error "Output file not created for #{identifier}"
|
|
103
|
+
raise CompilationError, "Tailwind output file was not created"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
css_content = File.read(css_file)
|
|
107
|
+
log_debug "Read #{css_content.bytesize} bytes from output file"
|
|
108
|
+
|
|
109
|
+
css_content
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def build_input_css(html_file)
|
|
113
|
+
config = Setting.tailwind_config rescue {}
|
|
114
|
+
theme_extends = config.dig(:theme, :extend) || {}
|
|
115
|
+
|
|
116
|
+
css_parts = [
|
|
117
|
+
'@import "tailwindcss";',
|
|
118
|
+
"@source \"#{html_file}\";"
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
# Apply custom colors from config
|
|
122
|
+
if theme_extends[:colors].present?
|
|
123
|
+
theme_extends[:colors].each do |name, value|
|
|
124
|
+
css_parts << "@theme { --color-#{name}: #{value}; }"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Apply custom fonts from config
|
|
129
|
+
if theme_extends[:fontFamily].present?
|
|
130
|
+
theme_extends[:fontFamily].each do |name, fonts|
|
|
131
|
+
font_list = Array(fonts).map { |f| f.include?(" ") ? "\"#{f}\"" : f }.join(", ")
|
|
132
|
+
css_parts << "@theme { --font-#{name}: #{font_list}; }"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
css_parts.join("\n")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def log_info(message)
|
|
140
|
+
Rails.logger.info "#{LOG_PREFIX} #{message}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def log_debug(message)
|
|
144
|
+
Rails.logger.debug "#{LOG_PREFIX} #{message}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def log_warn(message)
|
|
148
|
+
Rails.logger.warn "#{LOG_PREFIX} #{message}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def log_error(message)
|
|
152
|
+
Rails.logger.error "#{LOG_PREFIX} #{message}"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
<% content_for :page_title, "Media Library" %>
|
|
2
|
+
|
|
3
|
+
<!-- Page Header -->
|
|
4
|
+
<div class="page-header">
|
|
5
|
+
<div class="page-header-left">
|
|
6
|
+
<h2>Media Library</h2>
|
|
7
|
+
<p class="page-header-subtitle">Manage images and files for your pages</p>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<!-- Stats -->
|
|
12
|
+
<div class="stats-grid">
|
|
13
|
+
<div class="stat-card">
|
|
14
|
+
<div class="stat-icon stat-icon-primary">
|
|
15
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
16
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
|
17
|
+
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
18
|
+
<polyline points="21 15 16 10 5 21"/>
|
|
19
|
+
</svg>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="stat-content">
|
|
22
|
+
<h3><%= @media.count %></h3>
|
|
23
|
+
<p>Total Files</p>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="stat-card">
|
|
27
|
+
<div class="stat-icon stat-icon-success">
|
|
28
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
29
|
+
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
|
30
|
+
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
|
|
31
|
+
<line x1="12" y1="22.08" x2="12" y2="12"/>
|
|
32
|
+
</svg>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="stat-content">
|
|
35
|
+
<h3><%= number_to_human_size(@media.sum(:byte_size)) %></h3>
|
|
36
|
+
<p>Total Size</p>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<!-- Hidden Upload Form -->
|
|
42
|
+
<%= form_with url: admin_media_path, method: :post, multipart: true, id: "media-upload-form", style: "display: none;" do |f| %>
|
|
43
|
+
<%= f.file_field :file, id: "media-upload-input", accept: "image/*", multiple: true, onchange: "handleMediaUpload(this)" %>
|
|
44
|
+
<% end %>
|
|
45
|
+
|
|
46
|
+
<!-- Upload Zone -->
|
|
47
|
+
<div class="card" style="margin-bottom: 1.5rem;">
|
|
48
|
+
<div class="upload-zone" id="upload-zone" onclick="document.getElementById('media-upload-input').click()">
|
|
49
|
+
<div class="upload-zone-content">
|
|
50
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
51
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
52
|
+
<polyline points="17 8 12 3 7 8"/>
|
|
53
|
+
<line x1="12" y1="3" x2="12" y2="15"/>
|
|
54
|
+
</svg>
|
|
55
|
+
<h3>Drop files here or click to upload</h3>
|
|
56
|
+
<p>Supports JPEG, PNG, GIF, WebP, SVG (max 10MB)</p>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="upload-progress" id="upload-progress" style="display: none;">
|
|
59
|
+
<div class="upload-progress-bar" id="upload-progress-bar"></div>
|
|
60
|
+
<p id="upload-status">Uploading...</p>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<!-- Media Grid -->
|
|
66
|
+
<% if @media.any? %>
|
|
67
|
+
<div class="card">
|
|
68
|
+
<div class="card-header">
|
|
69
|
+
<span class="card-title">All Media</span>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="media-grid">
|
|
72
|
+
<% @media.each do |media| %>
|
|
73
|
+
<div class="media-item" data-media-id="<%= media.id %>">
|
|
74
|
+
<%= link_to admin_medium_path(media), class: "media-thumbnail-link" do %>
|
|
75
|
+
<div class="media-thumbnail">
|
|
76
|
+
<% if media.file.attached? %>
|
|
77
|
+
<%= image_tag media.url, alt: media.filename %>
|
|
78
|
+
<% end %>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="media-info">
|
|
81
|
+
<p class="media-filename" title="<%= media.filename %>"><%= truncate(media.filename, length: 20) %></p>
|
|
82
|
+
<p class="media-meta"><%= number_to_human_size(media.byte_size) %></p>
|
|
83
|
+
</div>
|
|
84
|
+
<% end %>
|
|
85
|
+
<div class="media-actions">
|
|
86
|
+
<button type="button" class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); copyMediaUrl('<%= media.url %>')" title="Copy URL">
|
|
87
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
88
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
|
89
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
90
|
+
</svg>
|
|
91
|
+
</button>
|
|
92
|
+
<%= button_to admin_medium_path(media), method: :delete, class: "btn btn-sm btn-danger", data: { confirm: "Are you sure you want to delete this file?" }, title: "Delete" do %>
|
|
93
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
94
|
+
<polyline points="3 6 5 6 21 6"/>
|
|
95
|
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
|
96
|
+
</svg>
|
|
97
|
+
<% end %>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
<% end %>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
<% else %>
|
|
104
|
+
<div class="card">
|
|
105
|
+
<div class="empty-state">
|
|
106
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
107
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
|
108
|
+
<circle cx="8.5" cy="8.5" r="1.5"/>
|
|
109
|
+
<polyline points="21 15 16 10 5 21"/>
|
|
110
|
+
</svg>
|
|
111
|
+
<h3>No media uploaded yet</h3>
|
|
112
|
+
<p>Upload images to use them in your pages</p>
|
|
113
|
+
<button type="button" class="btn btn-primary" onclick="document.getElementById('media-upload-input').click()">
|
|
114
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
115
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
116
|
+
<polyline points="17 8 12 3 7 8"/>
|
|
117
|
+
<line x1="12" y1="3" x2="12" y2="15"/>
|
|
118
|
+
</svg>
|
|
119
|
+
Upload Your First Image
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<% end %>
|
|
124
|
+
|
|
125
|
+
<style>
|
|
126
|
+
.upload-zone {
|
|
127
|
+
border: 2px dashed var(--border);
|
|
128
|
+
border-radius: var(--radius-lg);
|
|
129
|
+
padding: 3rem 2rem;
|
|
130
|
+
text-align: center;
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
transition: all 0.2s ease;
|
|
133
|
+
background: var(--bg-main);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.upload-zone:hover,
|
|
137
|
+
.upload-zone.dragover {
|
|
138
|
+
border-color: var(--primary);
|
|
139
|
+
background: var(--primary-light);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.upload-zone-content svg {
|
|
143
|
+
color: var(--text-muted);
|
|
144
|
+
margin-bottom: 1rem;
|
|
145
|
+
opacity: 0.5;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.upload-zone-content h3 {
|
|
149
|
+
font-size: 1rem;
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
color: var(--text);
|
|
152
|
+
margin-bottom: 0.5rem;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.upload-zone-content p {
|
|
156
|
+
font-size: 0.875rem;
|
|
157
|
+
color: var(--text-muted);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.upload-progress {
|
|
161
|
+
padding: 1rem;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.upload-progress-bar {
|
|
165
|
+
height: 4px;
|
|
166
|
+
background: var(--primary);
|
|
167
|
+
border-radius: 2px;
|
|
168
|
+
width: 0%;
|
|
169
|
+
transition: width 0.3s ease;
|
|
170
|
+
margin-bottom: 0.5rem;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.upload-progress p {
|
|
174
|
+
font-size: 0.875rem;
|
|
175
|
+
color: var(--text-muted);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.media-grid {
|
|
179
|
+
display: grid;
|
|
180
|
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
181
|
+
gap: 1rem;
|
|
182
|
+
padding: 1.25rem;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.media-item {
|
|
186
|
+
background: var(--bg-main);
|
|
187
|
+
border-radius: var(--radius);
|
|
188
|
+
overflow: hidden;
|
|
189
|
+
transition: all 0.2s ease;
|
|
190
|
+
border: 1px solid transparent;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.media-item:hover {
|
|
194
|
+
border-color: var(--primary);
|
|
195
|
+
box-shadow: var(--shadow-md);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.media-thumbnail-link {
|
|
199
|
+
display: block;
|
|
200
|
+
text-decoration: none;
|
|
201
|
+
color: inherit;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.media-thumbnail-link:hover .media-filename {
|
|
205
|
+
color: var(--primary);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.media-thumbnail {
|
|
209
|
+
aspect-ratio: 1;
|
|
210
|
+
overflow: hidden;
|
|
211
|
+
background: var(--sidebar-bg);
|
|
212
|
+
display: flex;
|
|
213
|
+
align-items: center;
|
|
214
|
+
justify-content: center;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.media-thumbnail img {
|
|
218
|
+
width: 100%;
|
|
219
|
+
height: 100%;
|
|
220
|
+
object-fit: cover;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.media-info {
|
|
224
|
+
padding: 0.75rem;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.media-filename {
|
|
228
|
+
font-size: 0.8125rem;
|
|
229
|
+
font-weight: 500;
|
|
230
|
+
color: var(--text);
|
|
231
|
+
white-space: nowrap;
|
|
232
|
+
overflow: hidden;
|
|
233
|
+
text-overflow: ellipsis;
|
|
234
|
+
margin-bottom: 0.25rem;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.media-meta {
|
|
238
|
+
font-size: 0.75rem;
|
|
239
|
+
color: var(--text-muted);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.media-actions {
|
|
243
|
+
display: flex;
|
|
244
|
+
gap: 0.5rem;
|
|
245
|
+
padding: 0 0.75rem 0.75rem;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.media-actions .btn {
|
|
249
|
+
flex: 1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.media-actions form {
|
|
253
|
+
flex: 1;
|
|
254
|
+
display: flex;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.media-actions form .btn {
|
|
258
|
+
width: 100%;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* Toast notification */
|
|
262
|
+
.toast {
|
|
263
|
+
position: fixed;
|
|
264
|
+
bottom: 2rem;
|
|
265
|
+
right: 2rem;
|
|
266
|
+
background: var(--sidebar-bg);
|
|
267
|
+
color: white;
|
|
268
|
+
padding: 0.875rem 1.25rem;
|
|
269
|
+
border-radius: var(--radius);
|
|
270
|
+
font-size: 0.875rem;
|
|
271
|
+
box-shadow: var(--shadow-md);
|
|
272
|
+
z-index: 1000;
|
|
273
|
+
animation: slideIn 0.3s ease;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.toast.success {
|
|
277
|
+
background: var(--success);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.toast.error {
|
|
281
|
+
background: var(--danger);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@keyframes slideIn {
|
|
285
|
+
from {
|
|
286
|
+
transform: translateY(1rem);
|
|
287
|
+
opacity: 0;
|
|
288
|
+
}
|
|
289
|
+
to {
|
|
290
|
+
transform: translateY(0);
|
|
291
|
+
opacity: 1;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
</style>
|
|
295
|
+
|
|
296
|
+
<script>
|
|
297
|
+
// Drag and drop handling
|
|
298
|
+
const uploadZone = document.getElementById('upload-zone');
|
|
299
|
+
const uploadInput = document.getElementById('media-upload-input');
|
|
300
|
+
|
|
301
|
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
302
|
+
uploadZone.addEventListener(eventName, preventDefaults, false);
|
|
303
|
+
document.body.addEventListener(eventName, preventDefaults, false);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
function preventDefaults(e) {
|
|
307
|
+
e.preventDefault();
|
|
308
|
+
e.stopPropagation();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
['dragenter', 'dragover'].forEach(eventName => {
|
|
312
|
+
uploadZone.addEventListener(eventName, () => {
|
|
313
|
+
uploadZone.classList.add('dragover');
|
|
314
|
+
}, false);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
['dragleave', 'drop'].forEach(eventName => {
|
|
318
|
+
uploadZone.addEventListener(eventName, () => {
|
|
319
|
+
uploadZone.classList.remove('dragover');
|
|
320
|
+
}, false);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
uploadZone.addEventListener('drop', (e) => {
|
|
324
|
+
const files = e.dataTransfer.files;
|
|
325
|
+
if (files.length > 0) {
|
|
326
|
+
handleFiles(files);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
function handleMediaUpload(input) {
|
|
331
|
+
if (input.files.length > 0) {
|
|
332
|
+
handleFiles(input.files);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function handleFiles(files) {
|
|
337
|
+
const uploadProgress = document.getElementById('upload-progress');
|
|
338
|
+
const uploadZoneContent = document.querySelector('.upload-zone-content');
|
|
339
|
+
const progressBar = document.getElementById('upload-progress-bar');
|
|
340
|
+
const uploadStatus = document.getElementById('upload-status');
|
|
341
|
+
|
|
342
|
+
uploadZoneContent.style.display = 'none';
|
|
343
|
+
uploadProgress.style.display = 'block';
|
|
344
|
+
|
|
345
|
+
const total = files.length;
|
|
346
|
+
let completed = 0;
|
|
347
|
+
|
|
348
|
+
for (const file of files) {
|
|
349
|
+
uploadStatus.textContent = `Uploading ${file.name}...`;
|
|
350
|
+
|
|
351
|
+
const formData = new FormData();
|
|
352
|
+
formData.append('media[file]', file);
|
|
353
|
+
formData.append('media[filename]', file.name);
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const response = await fetch('<%= admin_media_path %>', {
|
|
357
|
+
method: 'POST',
|
|
358
|
+
body: formData,
|
|
359
|
+
headers: {
|
|
360
|
+
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
if (response.ok) {
|
|
365
|
+
completed++;
|
|
366
|
+
progressBar.style.width = `${(completed / total) * 100}%`;
|
|
367
|
+
} else {
|
|
368
|
+
const data = await response.json();
|
|
369
|
+
showToast(`Failed to upload ${file.name}: ${data.errors.join(', ')}`, 'error');
|
|
370
|
+
}
|
|
371
|
+
} catch (error) {
|
|
372
|
+
showToast(`Error uploading ${file.name}`, 'error');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
uploadStatus.textContent = 'Upload complete!';
|
|
377
|
+
setTimeout(() => {
|
|
378
|
+
window.location.reload();
|
|
379
|
+
}, 500);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function copyMediaUrl(url) {
|
|
383
|
+
const fullUrl = window.location.origin + url;
|
|
384
|
+
navigator.clipboard.writeText(fullUrl).then(() => {
|
|
385
|
+
showToast('URL copied to clipboard!', 'success');
|
|
386
|
+
}).catch(() => {
|
|
387
|
+
showToast('Failed to copy URL', 'error');
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function showToast(message, type = 'success') {
|
|
392
|
+
const toast = document.createElement('div');
|
|
393
|
+
toast.className = `toast ${type}`;
|
|
394
|
+
toast.textContent = message;
|
|
395
|
+
document.body.appendChild(toast);
|
|
396
|
+
|
|
397
|
+
setTimeout(() => {
|
|
398
|
+
toast.remove();
|
|
399
|
+
}, 3000);
|
|
400
|
+
}
|
|
401
|
+
</script>
|