Skip to content

Commit 0d29802

Browse files
committed
Add basic crud for snippets
1 parent 8bb8b7d commit 0d29802

File tree

23 files changed

+284
-102
lines changed

23 files changed

+284
-102
lines changed

app/controllers/snippets_controller.rb

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
class SnippetsController < ApplicationController
2-
before_action :set_snippet, only: %i[show edit update destroy]
3-
42
# GET /snippets
53
def index
64
@snippets = Snippet.all
75
end
86

97
# GET /snippets/1
108
def show
9+
@snippet = Snippet.find(params[:id])
1110
end
1211

1312
# GET /snippets/new
1413
def new
15-
@snippet = Snippet.new
14+
@snippet = Snippet.new(snippet_params)
1615
end
1716

1817
# GET /snippets/1/edit
1918
def edit
19+
@snippet = Snippet.find(params[:id])
20+
@snippet.assign_attributes(snippet_params)
2021
end
2122

2223
# POST /snippets
@@ -32,6 +33,7 @@ def create
3233

3334
# PATCH/PUT /snippets/1
3435
def update
36+
@snippet = Snippet.find(params[:id])
3537
if @snippet.update(snippet_params)
3638
redirect_to @snippet, notice: "Snippet was successfully updated.", status: :see_other
3739
else
@@ -41,19 +43,15 @@ def update
4143

4244
# DELETE /snippets/1
4345
def destroy
46+
@snippet = Snippet.find(params[:id])
4447
@snippet.destroy!
4548
redirect_to snippets_url, notice: "Snippet was successfully destroyed.", status: :see_other
4649
end
4750

4851
private
4952

50-
# Use callbacks to share common setup or constraints between actions.
51-
def set_snippet
52-
@snippet = Snippet.find(params[:id])
53-
end
54-
5553
# Only allow a list of trusted parameters through.
5654
def snippet_params
57-
params.fetch(:snippet, {})
55+
params.fetch(:snippet, {}).permit(:filename, :source, :url, :language)
5856
end
5957
end

app/javascript/controllers/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import TableOfContents from './table-of-contents';
1717

1818
import FrameForm from './forms/frame';
1919
import SyntaxHighlightPreview from './syntax-highlight/preview';
20+
import SnippetPreview from './snippets/preview';
2021

2122
application.register('analytics', AnalyticsCustomEvent);
2223
application.register('code-example', CodeExample);
@@ -29,4 +30,5 @@ application.register('pwa-web-push-demo', PwaWebPushDemo);
2930
application.register('table-of-contents', TableOfContents);
3031

3132
application.register('frame-form', FrameForm);
33+
application.register('snippet-preview', SnippetPreview);
3234
application.register('syntax-highlight-preview', SyntaxHighlightPreview);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
3+
import debug from '../../utils/debug';
4+
5+
const console = debug('app:javascript:controllers:snippets:preview');
6+
7+
export default class extends Controller {
8+
static targets = ['source', 'previewButton'];
9+
idleTimeout = null;
10+
11+
connect() {
12+
console.log('Connect!');
13+
// this.sourceTarget.addEventListener('keydown', this.handleKeyDown);
14+
}
15+
16+
disconnect() {
17+
// this.sourceTarget.removeEventListener('keydown', this.handleKeyDown);
18+
clearTimeout(this.idleTimeout);
19+
}
20+
21+
preview = () => {
22+
console.log('Start preview timer!');
23+
clearTimeout(this.idleTimeout);
24+
this.idleTimeout = setTimeout(this.clickPreviewButton, 500);
25+
};
26+
27+
// handleKeyDown = () => {
28+
// clearTimeout(this.idleTimeout);
29+
// this.idleTimeout = setTimeout(this.clickPreviewButton, 500);
30+
// };
31+
32+
clickPreviewButton = () => {
33+
console.log('Click preview button!');
34+
this.previewButtonTarget.click();
35+
};
36+
}

app/javascript/css/components/code.scss

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
}
77

88
@media screen and (min-width: $screen-md) {
9-
.code-wrapper {
10-
margin-inline-start: calc(var(--space-m) * -1);
11-
margin-inline-end: calc(var(--space-m) * -1);
9+
.article-content {
10+
& .code-wrapper {
11+
margin-inline-start: calc(var(--space-m) * -1);
12+
margin-inline-end: calc(var(--space-m) * -1);
13+
}
1214
}
1315
}
1416

@@ -17,17 +19,28 @@
1719
flex-direction: row;
1820
justify-content: space-between;
1921
overflow-x: auto;
22+
position: relative;
2023

21-
pre {
24+
& pre {
2225
line-height: 1.7777778;
2326
margin-top: 0;
2427
margin-bottom: 0;
2528
border-radius: 0.5rem;
29+
background-color: inherit;
30+
}
31+
32+
& pre,
33+
& textarea {
2634
padding-top: var(--space-m);
2735
padding-inline-end: var(--space-m);
2836
padding-bottom: var(--space-m);
2937
padding-inline-start: var(--space-m);
30-
background-color: inherit;
38+
}
39+
40+
textarea {
41+
width: 100%;
42+
font-family: var(--monospace);
43+
border: none;
3144
}
3245
}
3346

@@ -154,3 +167,13 @@
154167
}
155168
}
156169
}
170+
171+
.snippet {
172+
.code-body {
173+
& code {
174+
word-wrap: break-word;
175+
white-space: pre-wrap;
176+
word-break: normal;
177+
}
178+
}
179+
}

app/javascript/css/layout.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ main > *,
2121
.section-content,
2222
.footer-content,
2323
.main-content,
24+
.grid-content,
2425
.article-content {
2526
display: grid;
2627
}

app/models/snippet.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,36 @@
11
class Snippet < ApplicationRecord
2+
validates :language, presence: true
3+
4+
validate :recognized_language
5+
before_validation :auto_detect_language, if: :auto_detecting?
6+
7+
attr_reader :auto_detecting
8+
9+
def language=(value)
10+
if value == "auto"
11+
@auto_detecting = true
12+
super(guess_language)
13+
else
14+
super(value.to_s.downcase)
15+
end
16+
end
17+
18+
def auto_detect_language
19+
self.language = guess_language
20+
end
21+
22+
def guess_language
23+
Rouge::Lexer.guesses({filename: filename, source: source}.compact).first&.tag
24+
end
25+
26+
private
27+
28+
def auto_detecting? = !!@auto_detecting
29+
30+
def recognized_language
31+
return if Rouge::Lexer.find(language)
32+
33+
message = auto_detecting? ? "could not be auto-detected from filename or source" : "is not a recognized language"
34+
errors.add(:language, message)
35+
end
236
end

app/views/components/code_block/article.rb

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,41 @@
11
class CodeBlock::Article < Phlex::HTML
22
include InlineSvg::ActionView::Helpers
33
include Phlex::DeferredRender
4+
include Phlex::Rails::Helpers::ClassNames
45

5-
def initialize(source = "", language: nil, filename: nil, run: false, show_header: true)
6-
@source = source
7-
@language = language
8-
@filename = filename
6+
def initialize(
7+
source = "",
8+
language: nil,
9+
filename: nil,
10+
run: false,
11+
show_header: true,
12+
enable_copy: true,
13+
**options
14+
)
15+
@source = source || ""
16+
@language = language.presence
17+
@filename = filename.presence
918
@enable_run = run
1019
@show_header = show_header
20+
@enable_copy = enable_copy
21+
@options = options
1122
end
1223

1324
def view_template
1425
div(
15-
class: "code-wrapper highlight language-#{language}",
26+
class: class_names("code-wrapper", "highlight", "language-#{language}", *options[:class]),
1627
data: code_example_data.keep_if { enable_run }.merge(data)
1728
) do
1829
if @show_header
1930
div(class: "code-header") do
20-
plain inline_svg_tag("app-dots.svg", class: "app-dots")
31+
plain inline_svg_tag("app-dots.svg", class: "app-dots mr-4")
2132
span(class: "code-title", &title_content)
2233
end
2334
end
2435

2536
div(class: "code-body") do
2637
render CodeBlock::Basic.new(source, language: language, data: {code_example_target: "source"})
27-
render ClipboardCopy.new(text: source)
38+
render ClipboardCopy.new(text: source) if enable_copy
2839
end
2940

3041
if enable_run
@@ -62,7 +73,7 @@ def title_content
6273

6374
private
6475

65-
attr_reader :source, :language, :filename, :enable_run
76+
attr_reader :source, :language, :filename, :enable_run, :enable_copy, :options
6677

6778
def data = {language: language, lines:}
6879

app/views/snippets/_form.html.erb

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
<%= form_with(model: snippet) do |form| %>
1+
<%= form_with(
2+
model: snippet,
3+
data: { controller: "snippet-preview", action: "input->snippet-preview#preview" },
4+
class: "grid-content"
5+
) do |form| %>
26
<% if snippet.errors.any? %>
37
<div style="color: red">
48
<h2><%= pluralize(snippet.errors.count, "error") %> prohibited this snippet from being saved:</h2>
@@ -11,7 +15,42 @@
1115
</div>
1216
<% end %>
1317

14-
<div>
15-
<%= form.submit %>
18+
<div class="flex items-start flex-col space-col-4 md:items-center md:flex-row md:space-row-4">
19+
<fieldset class="flex-grow">
20+
<%= form.label :filename, class: "block" %>
21+
<%= form.text_field :filename, class: "flex-1 rounded bg-white/5 focus-ring focus:ring-0 ring-1 ring-inset ring-white/10 w-full lg:min-w-[36ch] " %>
22+
</fieldset>
23+
24+
<fieldset>
25+
<%= form.label :language, class: "block" %>
26+
<%= form.select :language, [["Auto", "auto"]] + Rouge::Lexer.all.map { |lexer| [lexer.title, lexer.tag] }, class: "flex-1 rounded bg-white/5 focus-ring focus:ring-0 ring-1 ring-inset ring-white/10 w-full lg:min-w-[36ch]" %>
27+
</fieldset>
1628
</div>
29+
30+
<fieldset>
31+
<%= form.label :source, class: "block" %>
32+
<code>
33+
<%= form.text_area :source,
34+
# data: { action: "input->snippet-preview#handleChange" },
35+
class: "flex-1 rounded bg-white/5 focus-ring focus:ring-0 ring-1 ring-inset ring-white/10 w-full lg:min-w-[36ch]" %>
36+
</code>
37+
</fieldset>
38+
39+
<fieldset>
40+
<%= form.submit class: "button primary" %>
41+
<%= form.submit "Preview", class: "button secondary hidden",
42+
formaction: (snippet.persisted? ? edit_snippet_path(snippet) : new_snippet_path),
43+
formmethod: "get",
44+
formnovalidate: true,
45+
data: {
46+
snippet_preview_target: "previewButton",
47+
turbo_frame: dom_id(snippet, :code_block)
48+
} %>
49+
</fieldset>
1750
<% end %>
51+
52+
<div class="grid-content">
53+
<%= turbo_frame_tag dom_id(snippet, :code_block) do %>
54+
<%= render snippet %>
55+
<% end %>
56+
</div>

app/views/snippets/_snippet.html.erb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
1-
<div id="<%= dom_id snippet %>">
1+
<div id="<%= dom_id(snippet, :code_block) %>">
2+
<%= render CodeBlock::Article.new(
3+
snippet.source || "",
4+
language: snippet.language,
5+
filename: snippet.filename,
6+
) %>
27
</div>

app/views/snippets/edit.html.erb

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
<h1>Editing snippet</h1>
1+
<%= render Pages::Header.new(title: "Edit snippet") %>
2+
<div class="section-content container py-gap">
3+
<%= render "form", snippet: @snippet %>
24

3-
<%= render "form", snippet: @snippet %>
5+
<br>
46

5-
<br>
6-
7-
<div>
8-
<%= link_to "Show this snippet", @snippet %> |
9-
<%= link_to "Back to snippets", snippets_path %>
7+
<div>
8+
<%= link_to "Show this snippet", @snippet %> |
9+
<%= link_to "Back to snippets", snippets_path %>
10+
</div>
1011
</div>

0 commit comments

Comments
 (0)