add a webapplication to view captured pastes
This commit is contained in:
parent
92538b42b3
commit
3b225c3e50
14 changed files with 1247 additions and 106 deletions
3
Gemfile
3
Gemfile
|
@ -14,7 +14,7 @@ gem 'puma', '~> 5.6'
|
|||
# Use SCSS for stylesheets
|
||||
gem 'sass-rails', '~> 6.0'
|
||||
# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
|
||||
gem 'webpacker', '~> 4.0'
|
||||
# gem 'webpacker', '~> 4.0'
|
||||
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
|
||||
gem 'turbolinks', '~> 5'
|
||||
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
|
||||
|
@ -30,6 +30,7 @@ gem 'sidekiq', '~> 6.5'
|
|||
gem 'sidekiq-scheduler'
|
||||
gem 'pry'
|
||||
gem "sidekiq-cron", "~> 1.1"
|
||||
gem 'kaminari', '~> 1.2'
|
||||
# Use Active Storage variant
|
||||
# gem 'image_processing', '~> 1.2'
|
||||
|
||||
|
|
|
@ -4,12 +4,57 @@ class PastesController < ApplicationController
|
|||
# GET /pastes
|
||||
# GET /pastes.json
|
||||
def index
|
||||
@pastes = Paste.all
|
||||
# Get combined pastes from both SQLite and Elasticsearch
|
||||
if params[:search].present?
|
||||
# For search, use Elasticsearch only for now
|
||||
@pastes = Paste.from_elasticsearch(limit: 20).select do |paste|
|
||||
paste.content.downcase.include?(params[:search].downcase) ||
|
||||
paste.title.downcase.include?(params[:search].downcase)
|
||||
end
|
||||
else
|
||||
# Show combined recent pastes
|
||||
@pastes = Paste.recent_combined(limit: 20)
|
||||
end
|
||||
|
||||
# Filter by language if specified
|
||||
if params[:language].present?
|
||||
@pastes = @pastes.select { |paste| paste.language == params[:language] }
|
||||
end
|
||||
|
||||
# For filter dropdown - combine languages from both sources
|
||||
sqlite_languages = Paste.languages
|
||||
es_languages = @pastes.map(&:language).uniq.compact
|
||||
@languages = (sqlite_languages + es_languages).uniq.sort
|
||||
|
||||
# Stats for dashboard
|
||||
@total_pastes = Paste.count + get_elasticsearch_count
|
||||
@total_languages = @languages.count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_elasticsearch_count
|
||||
begin
|
||||
client = Elasticsearch::Client.new(url: ENV.fetch('ELASTICSEARCH_URL', 'http://pastebinner-elasticsearch:9200'))
|
||||
response = client.count(index: 'pastes')
|
||||
response['count'] || 0
|
||||
rescue => e
|
||||
Rails.logger.error "Elasticsearch count error: #{e.message}"
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
# GET /pastes/1
|
||||
# GET /pastes/1.json
|
||||
def show
|
||||
# Increment hit counter only for SQLite pastes
|
||||
@paste.increment_hits! if @paste.respond_to?(:increment_hits!)
|
||||
|
||||
# For raw text display
|
||||
if params[:raw] == 'true'
|
||||
render plain: @paste.content, content_type: 'text/plain'
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
# GET /pastes/new
|
||||
|
@ -63,12 +108,32 @@ class PastesController < ApplicationController
|
|||
|
||||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
# def set_paste
|
||||
# @paste = Paste.find(params[:id])
|
||||
# end
|
||||
def set_paste
|
||||
# Try to find in SQLite first
|
||||
begin
|
||||
@paste = Paste.find(params[:id])
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
# If not found in SQLite, try Elasticsearch by document ID
|
||||
@paste = find_elasticsearch_paste(params[:id])
|
||||
unless @paste
|
||||
redirect_to pastes_path, alert: 'Paste not found'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def find_elasticsearch_paste(doc_id)
|
||||
begin
|
||||
client = Elasticsearch::Client.new(url: ENV.fetch('ELASTICSEARCH_URL', 'http://pastebinner-elasticsearch:9200'))
|
||||
response = client.get(index: 'pastes', id: doc_id)
|
||||
Paste.elasticsearch_paste_to_object(response)
|
||||
rescue => e
|
||||
Rails.logger.error "Elasticsearch get error: #{e.message}"
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Never trust parameters from the scary internet, only allow the white list through.
|
||||
# def paste_params
|
||||
# params.require(:paste).permit(:search_pastes)
|
||||
# end
|
||||
def paste_params
|
||||
params.require(:paste).permit(:title, :content, :language, :expires_at)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,2 +1,62 @@
|
|||
module ApplicationHelper
|
||||
def syntax_highlight(content, language = 'text', options = {})
|
||||
return '' if content.blank?
|
||||
|
||||
css_classes = ["language-#{language}"]
|
||||
css_classes << 'line-numbers' if options[:line_numbers]
|
||||
|
||||
content_tag :pre, class: css_classes.join(' ') do
|
||||
content_tag :code, content, class: "language-#{language}"
|
||||
end
|
||||
end
|
||||
|
||||
def language_icon(language)
|
||||
icons = {
|
||||
'javascript' => '🟨',
|
||||
'python' => '🐍',
|
||||
'ruby' => '💎',
|
||||
'java' => '☕',
|
||||
'php' => '🐘',
|
||||
'html' => '🌐',
|
||||
'css' => '🎨',
|
||||
'sql' => '🗄️',
|
||||
'json' => '📋',
|
||||
'xml' => '📄',
|
||||
'bash' => '🐚',
|
||||
'go' => '🐹',
|
||||
'rust' => '🦀',
|
||||
'typescript' => '🔷'
|
||||
}
|
||||
|
||||
icons[language.to_s.downcase] || '📄'
|
||||
end
|
||||
|
||||
def time_ago_in_words_short(time)
|
||||
return '' unless time
|
||||
|
||||
distance = Time.current - time
|
||||
|
||||
case distance
|
||||
when 0..59
|
||||
"#{distance.to_i}s ago"
|
||||
when 60..3599
|
||||
"#{(distance / 60).to_i}m ago"
|
||||
when 3600..86399
|
||||
"#{(distance / 3600).to_i}h ago"
|
||||
when 86400..2591999
|
||||
"#{(distance / 86400).to_i}d ago"
|
||||
else
|
||||
time.strftime('%b %d, %Y')
|
||||
end
|
||||
end
|
||||
|
||||
def truncate_content(content, length = 150)
|
||||
return '' if content.blank?
|
||||
|
||||
if content.length > length
|
||||
content[0..length].gsub(/\s\w+\s*$/, '...')
|
||||
else
|
||||
content
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,35 @@ require("turbolinks").start()
|
|||
require("@rails/activestorage").start()
|
||||
require("channels")
|
||||
|
||||
// Initialize Prism.js on page load and turbolinks navigation
|
||||
document.addEventListener('turbolinks:load', function() {
|
||||
if (typeof Prism !== 'undefined') {
|
||||
Prism.highlightAll();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle copy to clipboard functionality
|
||||
document.addEventListener('turbolinks:load', function() {
|
||||
// Add click handler for copy buttons
|
||||
document.querySelectorAll('.copy-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
const target = this.getAttribute('data-target');
|
||||
const textArea = document.querySelector(target);
|
||||
if (textArea) {
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
|
||||
// Provide visual feedback
|
||||
const originalText = this.textContent;
|
||||
this.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
this.textContent = originalText;
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Uncomment to copy all static images under ../images to the output folder and reference
|
||||
// them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>)
|
||||
|
|
|
@ -1,31 +1,232 @@
|
|||
require 'elasticsearch/model'
|
||||
require 'ostruct'
|
||||
|
||||
class Paste < ApplicationRecord
|
||||
include Elasticsearch::Model
|
||||
include Elasticsearch::Model::Callbacks
|
||||
include Elasticsearch::Model
|
||||
# include Elasticsearch::Model::Callbacks # Temporarily disabled for seeding
|
||||
|
||||
# Pagination
|
||||
paginates_per 20
|
||||
|
||||
# this doesnt work yet, but this is how we build a way to progrmatically query ES
|
||||
# def self.search(query)
|
||||
# __elasticsearch__.search(
|
||||
# {
|
||||
# query: {
|
||||
# multi_match: {
|
||||
# query: query,
|
||||
# }
|
||||
# },
|
||||
# "highlight": {
|
||||
# "pre_tags": [
|
||||
# "@kibana-highlighted-field@"
|
||||
# ],
|
||||
# "post_tags": [
|
||||
# "@/kibana-highlighted-field@"
|
||||
# ],
|
||||
# "fields": {
|
||||
# "*": {}
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# )
|
||||
# end
|
||||
# Validations
|
||||
validates :content, presence: true
|
||||
validates :paste_key, uniqueness: true, allow_blank: true
|
||||
|
||||
# Callbacks
|
||||
before_save :set_defaults
|
||||
before_save :detect_language
|
||||
before_save :calculate_size
|
||||
|
||||
# Scopes
|
||||
scope :by_language, ->(lang) { where(language: lang) if lang.present? }
|
||||
scope :recent, -> { order(created_at: :desc) }
|
||||
scope :popular, -> { order(hits: :desc) }
|
||||
|
||||
# Class methods
|
||||
def self.search_content(query)
|
||||
return all if query.blank?
|
||||
|
||||
__elasticsearch__.search({
|
||||
query: {
|
||||
multi_match: {
|
||||
query: query,
|
||||
fields: ['content', 'title']
|
||||
}
|
||||
},
|
||||
highlight: {
|
||||
pre_tags: ['<mark>'],
|
||||
post_tags: ['</mark>'],
|
||||
fields: {
|
||||
content: {}
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
def self.languages
|
||||
distinct.pluck(:language).compact.sort
|
||||
end
|
||||
|
||||
# Fetch pastes from Elasticsearch
|
||||
def self.from_elasticsearch(limit: 20, offset: 0)
|
||||
begin
|
||||
client = Elasticsearch::Client.new(url: ENV.fetch('ELASTICSEARCH_URL', 'http://pastebinner-elasticsearch:9200'))
|
||||
|
||||
response = client.search(
|
||||
index: 'pastes',
|
||||
body: {
|
||||
size: limit,
|
||||
from: offset,
|
||||
sort: [{ '_score': { order: 'desc' } }],
|
||||
query: { match_all: {} }
|
||||
}
|
||||
)
|
||||
|
||||
response['hits']['hits'].map do |hit|
|
||||
elasticsearch_paste_to_object(hit)
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error "Elasticsearch error: #{e.message}"
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
# Convert Elasticsearch hit to paste-like object
|
||||
def self.elasticsearch_paste_to_object(hit)
|
||||
source = hit['_source']
|
||||
paste_id = extract_paste_key_from_metadata(source['paste_metadata'])
|
||||
|
||||
OpenStruct.new(
|
||||
id: hit['_id'],
|
||||
title: "Scraped Paste #{paste_id}",
|
||||
content: source['paste_text'] || '',
|
||||
language: detect_language_from_content(source['paste_text']),
|
||||
created_at: Time.current,
|
||||
updated_at: Time.current,
|
||||
hits: 0,
|
||||
size: (source['paste_text'] || '').length,
|
||||
paste_key: paste_id,
|
||||
source: 'elasticsearch'
|
||||
)
|
||||
end
|
||||
|
||||
# Extract paste key from metadata array
|
||||
def self.extract_paste_key_from_metadata(metadata)
|
||||
return 'unknown' unless metadata.is_a?(Array) && metadata.length >= 2
|
||||
|
||||
url = metadata[1] # The scrape URL
|
||||
if url =~ /\/api_scrape_item\.php\?i=([A-Za-z0-9]+)/
|
||||
$1
|
||||
else
|
||||
'unknown'
|
||||
end
|
||||
end
|
||||
|
||||
# Simple language detection based on content
|
||||
def self.detect_language_from_content(content)
|
||||
return 'text' if content.blank?
|
||||
|
||||
case content
|
||||
when /^\s*<\?php/m then 'php'
|
||||
when /^\s*#!/m then 'bash'
|
||||
when /function\s*\(/m, /=>\s*{/m, /console\.log/m then 'javascript'
|
||||
when /def\s+\w+/m, /import\s+\w+/m then 'python'
|
||||
when /<html/m, /<DOCTYPE/m then 'html'
|
||||
when /SELECT\s+/im, /INSERT\s+/im, /UPDATE\s+/im then 'sql'
|
||||
when /{[^}]*}/m, /\[[^\]]*\]/m then 'json'
|
||||
else 'text'
|
||||
end
|
||||
end
|
||||
|
||||
# Get combined pastes from both sources
|
||||
def self.recent_combined(limit: 20)
|
||||
# Get from SQLite
|
||||
sqlite_pastes = recent.limit(limit/2).to_a
|
||||
|
||||
# Get from Elasticsearch
|
||||
es_pastes = from_elasticsearch(limit: limit/2)
|
||||
|
||||
# Combine and return
|
||||
(sqlite_pastes + es_pastes).sort_by { |p| p.created_at || Time.current }.reverse.first(limit)
|
||||
end
|
||||
|
||||
# Instance methods
|
||||
def display_title
|
||||
title.present? ? title : "Paste ##{id}"
|
||||
end
|
||||
|
||||
def short_content(limit = 200)
|
||||
return '' if content.blank?
|
||||
content.length > limit ? "#{content[0..limit]}..." : content
|
||||
end
|
||||
|
||||
def formatted_size
|
||||
return '0 B' if size.nil? || size.zero?
|
||||
|
||||
units = %w[B KB MB GB]
|
||||
base = 1024
|
||||
exp = (Math.log(size) / Math.log(base)).to_i
|
||||
exp = [exp, units.size - 1].min
|
||||
|
||||
"%.1f %s" % [size.to_f / base**exp, units[exp]]
|
||||
end
|
||||
|
||||
def language_display
|
||||
language.present? ? language.humanize : 'Plain Text'
|
||||
end
|
||||
|
||||
def increment_hits!
|
||||
increment!(:hits)
|
||||
end
|
||||
|
||||
def expired?
|
||||
expires_at && expires_at < Time.current
|
||||
end
|
||||
|
||||
def prism_language
|
||||
# Map common languages to Prism.js language identifiers
|
||||
language_mappings = {
|
||||
'javascript' => 'javascript',
|
||||
'js' => 'javascript',
|
||||
'python' => 'python',
|
||||
'py' => 'python',
|
||||
'ruby' => 'ruby',
|
||||
'rb' => 'ruby',
|
||||
'java' => 'java',
|
||||
'cpp' => 'cpp',
|
||||
'c++' => 'cpp',
|
||||
'c' => 'c',
|
||||
'html' => 'html',
|
||||
'css' => 'css',
|
||||
'php' => 'php',
|
||||
'sql' => 'sql',
|
||||
'json' => 'json',
|
||||
'xml' => 'xml',
|
||||
'yaml' => 'yaml',
|
||||
'yml' => 'yaml',
|
||||
'bash' => 'bash',
|
||||
'shell' => 'bash',
|
||||
'go' => 'go',
|
||||
'rust' => 'rust',
|
||||
'typescript' => 'typescript',
|
||||
'ts' => 'typescript'
|
||||
}
|
||||
|
||||
return language_mappings[language.to_s.downcase] if language.present?
|
||||
'text'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_defaults
|
||||
self.paste_key = SecureRandom.hex(8) if paste_key.blank?
|
||||
self.hits ||= 0
|
||||
self.title = "Untitled" if title.blank?
|
||||
end
|
||||
|
||||
def detect_language
|
||||
return if content.blank?
|
||||
|
||||
# Simple language detection based on content patterns
|
||||
case content
|
||||
when /def\s+\w+\s*\(/
|
||||
self.language = 'python' if language.blank?
|
||||
when /function\s+\w+\s*\(/
|
||||
self.language = 'javascript' if language.blank?
|
||||
when /class\s+\w+/
|
||||
self.language = 'java' if language.blank?
|
||||
when /<\?php/
|
||||
self.language = 'php' if language.blank?
|
||||
when /<!DOCTYPE|<html/
|
||||
self.language = 'html' if language.blank?
|
||||
when /SELECT|INSERT|UPDATE|DELETE/i
|
||||
self.language = 'sql' if language.blank?
|
||||
else
|
||||
self.language = 'text' if language.blank?
|
||||
end
|
||||
end
|
||||
|
||||
def calculate_size
|
||||
self.size = content.present? ? content.bytesize : 0
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,11 +6,55 @@
|
|||
<%= csp_meta_tag %>
|
||||
|
||||
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
|
||||
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
|
||||
|
||||
<!-- Prism.js CSS Theme -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" />
|
||||
|
||||
<!-- Rails UJS and Turbolinks from CDN -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.2.0/turbolinks.js"></script>
|
||||
|
||||
<!-- Prism.js Core and Language Packs -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/copy-to-clipboard/prism-copy-to-clipboard.min.js"></script>
|
||||
|
||||
<!-- Line Numbers CSS -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/line-numbers/prism-line-numbers.min.css" />
|
||||
|
||||
<script>
|
||||
// Initialize Prism.js on page load and turbolinks navigation
|
||||
document.addEventListener('turbolinks:load', function() {
|
||||
if (typeof Prism !== 'undefined') {
|
||||
Prism.highlightAll();
|
||||
}
|
||||
|
||||
// Handle copy to clipboard functionality
|
||||
document.querySelectorAll('.copy-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
const target = this.getAttribute('data-target');
|
||||
const textArea = document.querySelector(target);
|
||||
if (textArea) {
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
|
||||
// Provide visual feedback
|
||||
const originalText = this.textContent;
|
||||
this.textContent = 'Copied!';
|
||||
setTimeout(() => {
|
||||
this.textContent = originalText;
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%= render 'search/form' %>
|
||||
<% unless controller_name == 'pastes' %>
|
||||
<%= render 'search/form' %>
|
||||
<% end %>
|
||||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,27 +1,174 @@
|
|||
<p id="notice"><%= notice %></p>
|
||||
<h1>Pastebinner - Browse Pastes</h1>
|
||||
|
||||
<h1>Pastes</h1>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Search pastes</th>
|
||||
<th colspan="3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<div class="simple-container">
|
||||
<h2>Recent Pastes (<%= @total_pastes %> total - SQLite + Elasticsearch)</h2>
|
||||
|
||||
<!-- Search and Filter Form -->
|
||||
<%= form_with url: pastes_path, method: :get, local: true, class: "search-form" do |f| %>
|
||||
<div class="search-controls">
|
||||
<%= f.text_field :search, placeholder: "Search pastes...", value: params[:search], class: "search-input" %>
|
||||
<%= f.select :language, options_for_select([["All Languages", ""]] + @languages.map { |l| [l.titleize, l] }, params[:language]), {}, { class: "language-select" } %>
|
||||
<%= f.submit "Search", class: "search-btn" %>
|
||||
<%= link_to "Clear", pastes_path, class: "clear-btn" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @pastes.any? %>
|
||||
<% @pastes.each do |paste| %>
|
||||
<tr>
|
||||
<td><%= paste.search_pastes %></td>
|
||||
<td><%= link_to 'Show', paste %></td>
|
||||
<td><%= link_to 'Edit', edit_paste_path(paste) %></td>
|
||||
<td><%= link_to 'Destroy', paste, method: :delete, data: { confirm: 'Are you sure?' } %></td>
|
||||
</tr>
|
||||
<div class="paste-card <%= 'elasticsearch-paste' if paste.respond_to?(:source) && paste.source == 'elasticsearch' %>">
|
||||
<div class="paste-header">
|
||||
<h3><%= link_to paste.display_title, paste_path(paste.id) %></h3>
|
||||
<% if paste.respond_to?(:source) && paste.source == 'elasticsearch' %>
|
||||
<span class="source-badge">Live Scraped</span>
|
||||
<% else %>
|
||||
<span class="source-badge local">Local</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="paste-meta">
|
||||
Language: <strong><%= paste.language %></strong> |
|
||||
Size: <strong><%= paste.size || (paste.content&.length || 0) %> characters</strong>
|
||||
<% if paste.paste_key.present? %>
|
||||
| Key: <strong><%= paste.paste_key %></strong>
|
||||
<% end %>
|
||||
</p>
|
||||
<div class="preview">
|
||||
<pre><code class="language-<%= paste.language %>"><%= truncate(paste.content, length: 300) %></code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p>No pastes found. <%= link_to "Clear filters", pastes_path %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<style>
|
||||
.simple-container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
<%= link_to 'New Paste', new_paste_path %>
|
||||
.search-form {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.search-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.language-select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.search-btn, .clear-btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.paste-card {
|
||||
border: 1px solid #ddd;
|
||||
margin: 15px 0;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.paste-card.elasticsearch-paste {
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.paste-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.paste-card h3 {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.source-badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.source-badge {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.source-badge.local {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.paste-meta {
|
||||
color: #666;
|
||||
margin: 10px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
border: 1px solid #e9ecef;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.preview code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.search-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.paste-header {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,9 +1,356 @@
|
|||
<p id="notice"><%= notice %></p>
|
||||
<div class="paste-viewer">
|
||||
<!-- Header Section -->
|
||||
<div class="paste-header">
|
||||
<div class="header-content">
|
||||
<div class="paste-info">
|
||||
<h1 class="paste-title">
|
||||
<%= language_icon(@paste.language) %>
|
||||
<%= @paste.display_title %>
|
||||
</h1>
|
||||
|
||||
<div class="paste-metadata">
|
||||
<div class="meta-group">
|
||||
<span class="meta-label">Language:</span>
|
||||
<span class="meta-value"><%= @paste.language_display %></span>
|
||||
</div>
|
||||
|
||||
<div class="meta-group">
|
||||
<span class="meta-label">Size:</span>
|
||||
<span class="meta-value"><%= @paste.formatted_size %></span>
|
||||
</div>
|
||||
|
||||
<div class="meta-group">
|
||||
<span class="meta-label">Created:</span>
|
||||
<span class="meta-value"><%= @paste.created_at.strftime('%B %d, %Y at %I:%M %p') %></span>
|
||||
</div>
|
||||
|
||||
<div class="meta-group">
|
||||
<span class="meta-label">Views:</span>
|
||||
<span class="meta-value"><%= @paste.hits %> hits</span>
|
||||
</div>
|
||||
|
||||
<% if @paste.paste_key.present? %>
|
||||
<div class="meta-group">
|
||||
<span class="meta-label">ID:</span>
|
||||
<span class="meta-value"><%= @paste.paste_key %></span>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="paste-actions">
|
||||
<%= link_to "📄 Raw Text", paste_path(@paste, raw: true),
|
||||
class: "action-btn secondary", target: "_blank" %>
|
||||
<button id="copy-paste-btn" class="action-btn primary">
|
||||
📋 Copy to Clipboard
|
||||
</button>
|
||||
<%= link_to "🔙 Back to List", pastes_path, class: "action-btn secondary" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<strong>Search pastes:</strong>
|
||||
<%= @paste.search_pastes %>
|
||||
</p>
|
||||
<!-- Code Content -->
|
||||
<div class="code-container">
|
||||
<% if @paste.content.present? %>
|
||||
<div class="code-header">
|
||||
<span class="language-badge">
|
||||
<%= language_icon(@paste.language) %> <%= @paste.language_display %>
|
||||
</span>
|
||||
<span class="line-count">
|
||||
📏 <%= @paste.content.lines.count %> lines
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="code-content line-numbers" id="paste-content">
|
||||
<%= syntax_highlight(@paste.content, @paste.prism_language, line_numbers: true) %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="no-content">
|
||||
<div class="no-content-icon">📭</div>
|
||||
<h3>No Content Available</h3>
|
||||
<p>This paste appears to be empty or the content could not be loaded.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= link_to 'Edit', edit_paste_path(@paste) %> |
|
||||
<%= link_to 'Back', pastes_path %>
|
||||
<!-- Hidden textarea for copying -->
|
||||
<textarea id="copy-source" style="position: absolute; left: -9999px;"><%= @paste.content %></textarea>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #f8fafc;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.paste-viewer {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.paste-header {
|
||||
background: white;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.paste-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.paste-title {
|
||||
font-size: 1.875rem;
|
||||
margin: 0 0 15px 0;
|
||||
color: #1a202c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.paste-metadata {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.meta-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
font-weight: 500;
|
||||
color: #4a5568;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
color: #2d3748;
|
||||
font-size: 0.875rem;
|
||||
background: #f7fafc;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.paste-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: #4299e1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.primary:hover {
|
||||
background: #3182ce;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: #e2e8f0;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.action-btn.secondary:hover {
|
||||
background: #cbd5e0;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.code-container {
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.code-header {
|
||||
background: #2d3748;
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.language-badge {
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.line-count {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.code-content {
|
||||
background: #1a202c !important;
|
||||
color: #e2e8f0;
|
||||
overflow-x: auto;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.code-content pre {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: transparent !important;
|
||||
color: inherit !important;
|
||||
font-family: 'Fira Code', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.code-content code {
|
||||
background: transparent !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
/* Line numbers styling */
|
||||
.line-numbers .line-numbers-rows {
|
||||
border-right: 1px solid #4a5568;
|
||||
margin-right: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.line-numbers .line-numbers-rows > span:before {
|
||||
color: #718096;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.no-content {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.no-content-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.no-content h3 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.no-content p {
|
||||
margin: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.paste-actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.paste-metadata {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.meta-group {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.code-header {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.code-content {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar for code content */
|
||||
.code-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.code-content::-webkit-scrollbar-track {
|
||||
background: #2d3748;
|
||||
}
|
||||
|
||||
.code-content::-webkit-scrollbar-thumb {
|
||||
background: #4a5568;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.code-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #718096;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('turbolinks:load', function() {
|
||||
const copyBtn = document.getElementById('copy-paste-btn');
|
||||
const copySource = document.getElementById('copy-source');
|
||||
|
||||
if (copyBtn && copySource) {
|
||||
copyBtn.addEventListener('click', function() {
|
||||
copySource.select();
|
||||
copySource.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
|
||||
// Provide visual feedback
|
||||
const originalText = this.innerHTML;
|
||||
this.innerHTML = '✅ Copied!';
|
||||
this.style.background = '#48bb78';
|
||||
|
||||
setTimeout(() => {
|
||||
this.innerHTML = originalText;
|
||||
this.style.background = '#4299e1';
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text: ', err);
|
||||
alert('Failed to copy to clipboard');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
# SQLite. Versions 3.8.0 and up are supported.
|
||||
# gem install sqlite3
|
||||
#
|
||||
# Ensure the SQLite 3 gem is defined in your Gemfile
|
||||
# gem 'sqlite3'
|
||||
#
|
||||
default: &default
|
||||
adapter: sqlite3
|
||||
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
||||
|
@ -13,13 +7,10 @@ development:
|
|||
<<: *default
|
||||
database: db/development.sqlite3
|
||||
|
||||
# Warning: The database defined as "test" will be erased and
|
||||
# re-generated from your development database when you run "rake".
|
||||
# Do not set this db to the same as development or production.
|
||||
test:
|
||||
<<: *default
|
||||
database: db/test.sqlite3
|
||||
|
||||
production:
|
||||
<<: *default
|
||||
database: db/production.sqlite3
|
||||
database: db/production.sqlite3
|
|
@ -1,5 +1,3 @@
|
|||
# Note: You must restart bin/webpack-dev-server for changes to take effect
|
||||
|
||||
default: &default
|
||||
source_path: app/javascript
|
||||
source_entry_path: packs
|
||||
|
@ -8,15 +6,8 @@ default: &default
|
|||
cache_path: tmp/cache/webpacker
|
||||
check_yarn_integrity: false
|
||||
webpack_compile_output: false
|
||||
|
||||
# Additional paths webpack should lookup modules
|
||||
# ['app/assets', 'engine/foo/app/assets']
|
||||
resolved_paths: []
|
||||
|
||||
# Reload manifest.json on all requests so we reload latest compiled packs
|
||||
cache_manifest: false
|
||||
|
||||
# Extract and emit a css file
|
||||
extract_css: false
|
||||
|
||||
static_assets_extensions:
|
||||
|
@ -51,18 +42,14 @@ default: &default
|
|||
development:
|
||||
<<: *default
|
||||
compile: true
|
||||
check_yarn_integrity: false
|
||||
|
||||
# Verifies that correct packages and versions are installed by inspecting package.json, yarn.lock, and node_modules
|
||||
check_yarn_integrity: true
|
||||
|
||||
# Reference: https://webpack.js.org/configuration/dev-server/
|
||||
dev_server:
|
||||
https: false
|
||||
host: localhost
|
||||
port: 3035
|
||||
public: localhost:3035
|
||||
hmr: false
|
||||
# Inline should be set to true if using HMR
|
||||
inline: true
|
||||
overlay: true
|
||||
compress: true
|
||||
|
@ -74,22 +61,13 @@ development:
|
|||
watch_options:
|
||||
ignored: '**/node_modules/**'
|
||||
|
||||
|
||||
test:
|
||||
<<: *default
|
||||
compile: true
|
||||
|
||||
# Compile test packs to a separate directory
|
||||
public_output_path: packs-test
|
||||
|
||||
production:
|
||||
<<: *default
|
||||
|
||||
# Production depends on precompilation of packs prior to booting for performance.
|
||||
compile: false
|
||||
|
||||
# Extract and emit a css file
|
||||
extract_css: true
|
||||
|
||||
# Cache manifest.json for performance
|
||||
cache_manifest: true
|
||||
cache_manifest: true
|
16
db/migrate/20250126120000_add_fields_to_pastes.rb
Normal file
16
db/migrate/20250126120000_add_fields_to_pastes.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
class AddFieldsToPastes < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :pastes, :title, :string
|
||||
add_column :pastes, :language, :string
|
||||
add_column :pastes, :paste_key, :string
|
||||
add_column :pastes, :content, :text
|
||||
add_column :pastes, :size, :integer
|
||||
add_column :pastes, :hits, :integer, default: 0
|
||||
add_column :pastes, :created_by, :string
|
||||
add_column :pastes, :expires_at, :datetime
|
||||
|
||||
add_index :pastes, :paste_key, unique: true
|
||||
add_index :pastes, :language
|
||||
add_index :pastes, :created_at
|
||||
end
|
||||
end
|
274
db/seeds.rb
274
db/seeds.rb
|
@ -1,7 +1,267 @@
|
|||
# This file should contain all the record creation needed to seed the database with its default values.
|
||||
# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
|
||||
# Character.create(name: 'Luke', movie: movies.first)
|
||||
# Sample paste data for testing the web interface
|
||||
|
||||
puts "Creating sample pastes..."
|
||||
|
||||
# Python sample
|
||||
Paste.create!(
|
||||
title: "Hello World Python",
|
||||
content: <<~PYTHON,
|
||||
def hello_world():
|
||||
"""A simple greeting function"""
|
||||
name = input("What's your name? ")
|
||||
print(f"Hello, {name}!")
|
||||
return name
|
||||
|
||||
if __name__ == "__main__":
|
||||
hello_world()
|
||||
PYTHON
|
||||
language: "python"
|
||||
)
|
||||
|
||||
# JavaScript sample
|
||||
Paste.create!(
|
||||
title: "React Component",
|
||||
content: <<~JAVASCRIPT,
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const UserProfile = ({ userId }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/users/${userId}`)
|
||||
.then(response => response.json())
|
||||
.then(userData => {
|
||||
setUser(userData);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching user:', error);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [userId]);
|
||||
|
||||
if (loading) return <div>Loading...</div>;
|
||||
if (!user) return <div>User not found</div>;
|
||||
|
||||
return (
|
||||
<div className="user-profile">
|
||||
<h1>{user.name}</h1>
|
||||
<p>{user.email}</p>
|
||||
<p>Joined: {new Date(user.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfile;
|
||||
JAVASCRIPT
|
||||
language: "javascript"
|
||||
)
|
||||
|
||||
# Ruby sample
|
||||
Paste.create!(
|
||||
title: "Rails Migration",
|
||||
content: <<~RUBY,
|
||||
class CreateUsersTable < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
create_table :users do |t|
|
||||
t.string :name, null: false
|
||||
t.string :email, null: false, index: { unique: true }
|
||||
t.string :password_digest
|
||||
t.text :bio
|
||||
t.boolean :active, default: true
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :users, :name
|
||||
add_index :users, :created_at
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
language: "ruby"
|
||||
)
|
||||
|
||||
# SQL sample
|
||||
Paste.create!(
|
||||
title: "Database Query",
|
||||
content: <<~SQL,
|
||||
SELECT
|
||||
u.name,
|
||||
u.email,
|
||||
COUNT(p.id) as post_count,
|
||||
AVG(p.views) as avg_views
|
||||
FROM users u
|
||||
LEFT JOIN posts p ON u.id = p.user_id
|
||||
WHERE u.active = true
|
||||
AND u.created_at >= '2024-01-01'
|
||||
GROUP BY u.id, u.name, u.email
|
||||
HAVING COUNT(p.id) > 5
|
||||
ORDER BY avg_views DESC
|
||||
LIMIT 20;
|
||||
SQL
|
||||
language: "sql"
|
||||
)
|
||||
|
||||
# HTML sample
|
||||
Paste.create!(
|
||||
title: "Landing Page Template",
|
||||
content: <<~HTML,
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>My Awesome App</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="hero">
|
||||
<nav class="navbar">
|
||||
<div class="logo">MyApp</div>
|
||||
<ul class="nav-links">
|
||||
<li><a href="#features">Features</a></li>
|
||||
<li><a href="#pricing">Pricing</a></li>
|
||||
<li><a href="#contact">Contact</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="hero-content">
|
||||
<h1>Build Amazing Things</h1>
|
||||
<p>The fastest way to create and deploy your applications</p>
|
||||
<button class="cta-button">Get Started</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section id="features" class="features">
|
||||
<h2>Why Choose Us?</h2>
|
||||
<div class="feature-grid">
|
||||
<div class="feature">
|
||||
<h3>Fast</h3>
|
||||
<p>Lightning-fast performance</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>Secure</h3>
|
||||
<p>Enterprise-grade security</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<h3>Scalable</h3>
|
||||
<p>Grows with your business</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
language: "html"
|
||||
)
|
||||
|
||||
# CSS sample
|
||||
Paste.create!(
|
||||
title: "Modern CSS Grid Layout",
|
||||
content: <<~CSS,
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
CSS
|
||||
language: "css"
|
||||
)
|
||||
|
||||
# JSON sample
|
||||
Paste.create!(
|
||||
title: "API Response Example",
|
||||
content: <<~JSON,
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"users": [
|
||||
{
|
||||
"id": 1,
|
||||
"username": "john_doe",
|
||||
"email": "john@example.com",
|
||||
"profile": {
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"avatar": "https://example.com/avatars/john.jpg",
|
||||
"bio": "Software developer passionate about clean code"
|
||||
},
|
||||
"preferences": {
|
||||
"theme": "dark",
|
||||
"notifications": {
|
||||
"email": true,
|
||||
"push": false,
|
||||
"sms": false
|
||||
},
|
||||
"privacy": {
|
||||
"profilePublic": true,
|
||||
"showEmail": false
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"postsCount": 42,
|
||||
"followersCount": 156,
|
||||
"followingCount": 89
|
||||
},
|
||||
"createdAt": "2024-01-15T10:30:00Z",
|
||||
"lastLoginAt": "2024-01-26T14:45:23Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"currentPage": 1,
|
||||
"totalPages": 10,
|
||||
"totalItems": 100,
|
||||
"itemsPerPage": 10
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": "2024-01-26T15:00:00Z",
|
||||
"version": "1.0",
|
||||
"requestId": "req_123456789"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
language: "json"
|
||||
)
|
||||
|
||||
puts "Created #{Paste.count} sample pastes!"
|
||||
puts "You can now browse them at http://localhost:3000"
|
||||
|
|
|
@ -26,6 +26,8 @@ services:
|
|||
- redis
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- DISABLE_SPRING=1
|
||||
networks:
|
||||
- elastic
|
||||
elasticsearch:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class RedisHelper
|
||||
attr_accessor :connection
|
||||
|
||||
def initialize(host: host, db: db)
|
||||
def initialize(host: nil, db: nil)
|
||||
@host = host
|
||||
@db = db
|
||||
@connection = Redis.new(host: host, db: db)
|
||||
|
|
Loading…
Add table
Reference in a new issue