diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 288b9ab7..b62e5f6f 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -13,3 +13,58 @@ *= require_tree . *= require_self */ + +/* Miniature CSS reset */ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: system-ui, sans-serif; +} + +#wrapper { + display: flex; + flex-direction: column; + margin: 0 auto; + max-width: 500px; + margin-top: 20px; + gap: 10px; + padding: 10px; +} + +/* Site-wide HTML element and class styles */ +a { + text-decoration: none; +} + +input, textarea { + padding: 3px; + font-size: 16px; + margin-bottom: 10px; + margin-top: 4px; + width: 100%; +} + +input[type="submit"] { + padding: 5px; +} + +textarea { + max-width: 100%; +} + +li { + list-style-type: none; + margin-bottom: 15px; +} + +.form-error { + background: rgba(255, 0, 0, 0.5); + padding: 10px; + margin-top: 10px; + margin-bottom: 10px; + border-radius: 4px; +} diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb new file mode 100644 index 00000000..6e672459 --- /dev/null +++ b/app/controllers/articles_controller.rb @@ -0,0 +1,65 @@ +class ArticlesController < ApplicationController + # GET /articles + def index + # Filter by the search query, if present + @articles = if params.key?(:query) + Article.search(params[:query]) + else + Article.all + end + end + + # GET /articles/:id + def show + @article = Article.find(params[:id]) + end + + # GET /articles/new + def new + @article = Article.new(date: Date.today) + end + + # POST /articles + def create + @article = Article.new(article_parameters) + + if @article.save + redirect_to @article + else + render :new, status: :unprocessable_entity + end + end + + # GET /articles/:id/edit + def edit + @article = Article.find(params[:id]) + end + + # POST /articles/:id + def update + @article = Article.find(params[:id]) + + if @article.update(article_parameters) + redirect_to @article + else + render :edit, status: :unprocessable_entity + end + end + + # DELETE /articles/:id + def destroy + @article = Article.find(params[:id]) + @article.destroy + + # Redirect the user to the list of all articles after deletion + redirect_to articles_path, status: :see_other + end + + # Ensure the hash passed to `create` or `edit`contains only the valid fields + # for the Article model. + private + + def article_parameters + params.require(:article).permit(:title, :content, :author, :date) + end +end diff --git a/app/models/article.rb b/app/models/article.rb new file mode 100644 index 00000000..6c8532e5 --- /dev/null +++ b/app/models/article.rb @@ -0,0 +1,16 @@ +class Article < ApplicationRecord + # Ensure articles have a non-empty title and non-empty content + validates :title, presence: true + validates :content, presence: true + + # Search articles by title and content + def self.search(query) + # We want to interpret characters like '%' and '_' in the search query + # literally, because users aren't familiar with sqlite's search syntax. + # So we use `sanitize_sql_like` before querying the database. See + # https://guides.rubyonrails.org/active_record_querying.html section 3.2.2 + # for more information on sanitizing LIKE queries. + where('title LIKE :sanitized_query OR content LIKE :sanitized_query', + { sanitized_query: "%#{sanitize_sql_like(query)}%" }) + end +end diff --git a/app/views/articles/_form.html.erb b/app/views/articles/_form.html.erb new file mode 100644 index 00000000..f0b604ae --- /dev/null +++ b/app/views/articles/_form.html.erb @@ -0,0 +1,30 @@ +<%= form_with model: article do |form| %> +
+ <%= form.label :title %> + <%= render "articles/form_errors", messages: @article.errors.full_messages_for(:title) %> + <%= form.text_field :title, placeholder: "My New Article" %> +
+ +
+ <%= form.label :author %> + <%= render "articles/form_errors", messages: @article.errors.full_messages_for(:author) %> + <%= form.text_field :author, placeholder: "John Doe" %> +
+ +
+ <%= form.label :date %> + <%= render "articles/form_errors", messages: @article.errors.full_messages_for(:date) %> + <%= form.date_field :date %> +
+ +
+ <%= form.label :content %> + <%= render "articles/form_errors", messages: @article.errors.full_messages_for(:content) %> + <%= form.text_area :content, rows: 10, placeholder: "Once upon a time…" %>
+
+ +
+ <%= form.submit %> +
+<% end %> + diff --git a/app/views/articles/_form_errors.html.erb b/app/views/articles/_form_errors.html.erb new file mode 100644 index 00000000..dbdefcf6 --- /dev/null +++ b/app/views/articles/_form_errors.html.erb @@ -0,0 +1,5 @@ +
+ <% messages.each do |message| %> +

<%= message %>

+ <% end %> +
diff --git a/app/views/articles/_header.html.erb b/app/views/articles/_header.html.erb new file mode 100644 index 00000000..cbc608d4 --- /dev/null +++ b/app/views/articles/_header.html.erb @@ -0,0 +1,6 @@ +
+ <% if back_button == true %> + <%= link_to "Home", articles_path, id: "home-link" %> + <% end %> + <%= yield %> +
diff --git a/app/views/articles/edit.html.erb b/app/views/articles/edit.html.erb new file mode 100644 index 00000000..1e29bc62 --- /dev/null +++ b/app/views/articles/edit.html.erb @@ -0,0 +1,5 @@ +<%= render "articles/header", back_button: true do %> +

Edit ‘<%= @article.title %>’

+<% end %> + +<%= render "form", article: @article %> diff --git a/app/views/articles/index.html.erb b/app/views/articles/index.html.erb new file mode 100644 index 00000000..a2d1a7b9 --- /dev/null +++ b/app/views/articles/index.html.erb @@ -0,0 +1,34 @@ +<%= render "articles/header", back_button: false do %> +

Articles

+<% end %> + +<%# Search Form %> +<%= form_with url: "/articles", method: :get do |form| %> + <%= form.text_field :query, value: params[:query], placeholder: "Search titles or content" %> + <%= form.submit "Search" %> +<% end %> + +<%# Results Count %> +<% if params.has_key?(:query) && params[:query] != '' %> +

+ <%= @articles.count %> + <%= 'article'.pluralize(@articles.count) %> found for query + ‘<%= params[:query] %>’ +

+<% end %> + +<%# Articles List %> + + +<%# Actions %> +

You can <%= link_to "create a new article", new_article_path, class: "link-button" %>.

diff --git a/app/views/articles/new.html.erb b/app/views/articles/new.html.erb new file mode 100644 index 00000000..fa7c39aa --- /dev/null +++ b/app/views/articles/new.html.erb @@ -0,0 +1,5 @@ +<%= render "articles/header", back_button: true do %> +

New Article

+<% end %> + +<%= render "form", article: @article %> diff --git a/app/views/articles/show.html.erb b/app/views/articles/show.html.erb new file mode 100644 index 00000000..63e05ed1 --- /dev/null +++ b/app/views/articles/show.html.erb @@ -0,0 +1,21 @@ +<%= render "articles/header", back_button: true do %> +

<%= @article.title %>

+

+ <% if @article.author? %> + <%= @article.author %> • + <% end %> + + <% if @article.date? %> + <%= @article.date %> + <% end %> +

+<% end %> + +

<%= @article.content %>

+ +

You can <%= link_to "edit", edit_article_path(@article) %> or + <%= link_to "delete", article_path(@article), data: { + turbo_method: :delete, + turbo_confirm: "Are you sure you want to delete ’#{@article.title}? This can't be undone." + } %> this article. +

diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 812bfb90..14e919d6 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -11,6 +11,8 @@ - <%= yield %> +
+ <%= yield %> +
diff --git a/config/routes.rb b/config/routes.rb index a125ef08..7eb4f422 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,5 +6,7 @@ get "up" => "rails/health#show", as: :rails_health_check # Defines the root path route ("/") - # root "posts#index" + root "articles#index" + + resources :articles end diff --git a/db/migrate/20240130000720_create_articles.rb b/db/migrate/20240130000720_create_articles.rb new file mode 100644 index 00000000..84add4e4 --- /dev/null +++ b/db/migrate/20240130000720_create_articles.rb @@ -0,0 +1,12 @@ +class CreateArticles < ActiveRecord::Migration[7.1] + def change + create_table :articles do |t| + t.string :title + t.text :content + t.string :author + t.date :date + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 00000000..a3fdd6e2 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,23 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.1].define(version: 2024_01_30_000720) do + create_table "articles", force: :cascade do |t| + t.string "title" + t.text "content" + t.string "author" + t.date "date" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + +end