Skip to content

Commit

Permalink
added blog post MVC
Browse files Browse the repository at this point in the history
  • Loading branch information
5andu committed Dec 30, 2023
1 parent 827276f commit 9fbfdd3
Show file tree
Hide file tree
Showing 13 changed files with 406 additions and 3 deletions.
Binary file added app/assets/images/founder-avatar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 71 additions & 0 deletions app/controllers/blog_posts_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
class BlogPostsController < ApplicationController
before_action :set_blog_post, only: %i[show edit update destroy]
before_action :require_admin!, only: %i[new edit create update destroy]
# before_action :authenticate_user!, only: %i[new create edit update destroy]

# GET /blog_posts
def index
@blog_posts = BlogPost.where(draft: false).order(created_at: :asc)
@drafts = BlogPost.where(draft: true).order(created_at: :desc)
end

# GET /blog_posts/slug
def show
end

# GET /blog_posts/new
def new
@blog_post = BlogPost.new

end

# GET /blog_posts/slug/edit
def edit
end

# POST /blog_posts
def create
@blog_post = BlogPost.new(blog_post_params)

if @blog_post.save
redirect_to blog_post_path(@blog_post.slug), notice: "Blog post was successfully created."
else
render :new, status: :unprocessable_entity
end
end

# PATCH/PUT /blog_posts/slug
def update
@blog_post.slug = params[:blog_post][:slug]
if @blog_post.save && @blog_post.update(blog_post_params)
redirect_to blog_post_path(@blog_post.slug), notice: "Blog post was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end

# DELETE /blog_posts/1
def destroy
@blog_post.destroy
redirect_to blog_posts_url, notice: "Blog post was successfully destroyed."
end

private

# Use callbacks to share common setup or constraints between actions.
def set_blog_post
slug = params[:blog_post].present? ? params[:blog_post][:slug] : params[:slug]
@blog_post = BlogPost.find_by!(slug: slug)
end

# Only allow a list of trusted parameters through, but add :body, and use slug instead of id in the URL.
def blog_post_params
params.require(:blog_post).permit(:title, :slug, :description, :body, :cover_image, :draft)
end

def require_admin!
unless current_user&.admin?
redirect_to root_path, alert: "You are not authorized to view this page."
end
end
end
25 changes: 25 additions & 0 deletions app/models/blog_post.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
class BlogPost < ApplicationRecord
has_one_attached :cover_image
has_rich_text :body
validates :slug, uniqueness: true
before_validation :generate_unique_slug
validates_presence_of :title, :slug, :body, :description

def to_param
slug
end

private

def generate_unique_slug
if new_record? || slug_changed?
base_slug = slug.blank? ? title.parameterize : slug
other = self.class.where("slug LIKE ?", "#{base_slug}%")
self.slug = if other.exists?
"#{base_slug}-#{other.count + 1}"
else
base_slug
end
end
end
end
23 changes: 23 additions & 0 deletions app/views/blog_posts/_blog_post.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<div id="<%= dom_id blog_post %>">
<article class="flex flex-col items-start justify-between hover:opacity-80 transition-opacity duration-300 ease-in-out" >

<%= link_to blog_post_path(blog_post) do %>

<div class="relative w-full">
<%= image_tag blog_post.cover_image, class:"aspect-[16/9] w-full rounded-2xl bg-gray-100 object-cover" if blog_post.cover_image.attached? %>
<div class="absolute inset-0 rounded-2xl ring-1 ring-inset ring-gray-900/10"></div>
</div>
<div class="w-full">
<div class="text-xs -mt-8 ml-auto text-right w-full pr-2.5">
<time class="relative z-10 rounded-full bg-gray-50 px-3 py-1.5 font-medium text-gray-600 border" datetime="<%= blog_post.updated_at.strftime('%m-%d-%Y') %>" class="text-gray-500"><%= blog_post.created_at.strftime('%B %d, %Y') %></time>
</div>
<div class="group relative">
<h2 class="mt-9 text-lg font-semibold leading-6 text-gray-900 group-hover:text-gray-600">
<%= blog_post.title %>
</h2>
<p class="mt-3 line-clamp-3 text-sm leading-6 text-gray-600"><%= blog_post.description %></p>
</div>

<% end %>
</article>
</div>
137 changes: 137 additions & 0 deletions app/views/blog_posts/_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<%= form_with(model: blog_post) do |form| %>
<% if blog_post.errors.any? %>
<div class="bg-red-100 text-red-700 p-4 rounded mb-4">
<h2><%= pluralize(blog_post.errors.count, "error") %> prohibited this blog_post from being saved:</h2>

<ul>
<% blog_post.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="flex flex-col space-y-6">
<% unless turbo_native_app? %>
<% end %>
<div class="bg-white border shadow px-4 py-5 lg:rounded-lg sm:p-6">
<div>

<div class="mt-5 md:mt-0 md:col-span-2">
<div class="space-y-6">
<div class="sm:col-span-3">
<%= form.label :slug, class: "block text-sm font-medium text-gray-700" %>
<div class="mt-1 flex rounded-md shadow-sm">
<span class="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm">
https://yt.careers/blog/
</span>
<%= form.text_field :slug, required: true, class: "flex-1 block w-full min-w-0 rounded-none rounded-r-md sm:text-sm border-gray-300 focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-50", placeholder: "how-to-grow-a-saas" %>
</div>
</div>

<%# checkbox to consider this post a draft %>
<div class="sm:col-span-3">
<div class="flex items-start">
<div class="flex items-center h-5">
<%= form.check_box :draft, class: "focus:ring-primary-500 h-4 w-4 text-primary-600 border-gray-300 rounded" %>
</div>
<div class="ml-3 text-sm">
<%= form.label :draft, class: "font-medium text-gray-700" %>
<p class="text-gray-500">Drafts are not visible to the public</p>
</div>
</div>
</div>

<div class="sm:col-span-3">
<%= form.label :title, class: "block text-sm font-medium text-gray-700" %>
<%= form.text_field :title, required: true, class: "mt-1 focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-50 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md", placeholder: "Title of your blog post. (65 characters max)", maxlength: 65 %>
</div>

<div class="sm:col-span-3">
<%= form.label :description, class: "block text-sm font-medium text-gray-700" %>
<%= form.text_area :description, required: true, rows: 2 , class: "mt-1 focus:border-primary-500 focus:ring focus:ring-primary-500 focus:ring-opacity-50 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md", placeholder: "A snippet of text designed to summarise a page. Though not critically important, an ideal length would be 70-155 characters.", maxlength: 155 %>
</div>

<div class="col-span-full">
<label for="cover_image" class="block text-sm font-medium leading-6 text-gray-900">Cover photo</label>
<div id="drop_zone" class="mt-2 flex justify-center rounded-lg border border-dashed border-gray-900/25 px-6 py-10">
<div class="text-center">
<%= image_tag @blog_post.cover_image, id: 'cover_image_preview', data: { url: url_for(@blog_post.cover_image) } if @blog_post.cover_image.attached? %>
<img id="cover_image_preview" src="#" alt="Cover Image Preview" class="hidden"/>
<div class="mt-4 flex text-sm leading-6 text-gray-600 justify-center">
<label for="cover_image" class="relative cursor-pointer rounded-md bg-white font-semibold text-gray-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-gray-600 focus-within:ring-offset-2 hover:text-gray-500">
<span>Upload a file</span>
<%= form.file_field :cover_image, id: 'cover_image', class: 'sr-only' %>
</label>
<p class="pl-1">or drag and drop</p>
</div>
<p class="text-xs leading-5 text-gray-600">PNG, JPG, WEBP up to 1MB (1200 × 630 px recommended)</p>
</div>
</div>
</div>

<div class="field relative border rounded-md mt-2 p-2 min-h-[175px] border-gray-300 group">
<%= form.rich_text_area :body, placeholder: "Write your blog post...", required: true, class:"group-focus:ring-gray-500 group-focus:border-gray-500" %>
</div>

<div class="flex justify-center items-center mt-4">
<%= form.submit "Save", class:"group relative w-full flex justify-center py-2.5 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none cursor-pointer"%>
</div>
</div>
</div>
</div>
</div>
</div>
<% end %>

<script>
// Preview the cover image
document.getElementById('cover_image').addEventListener('change', function(e) {
var file = e.target.files[0];
var reader = new FileReader();

reader.onloadend = function() {
document.getElementById('cover_image_preview').src = reader.result;
document.getElementById('cover_image_preview').style.display = 'block';
}

if (file) {
reader.readAsDataURL(file);
} else {
document.getElementById('cover_image_preview').src = "";
}
});

document.addEventListener('DOMContentLoaded', function() {
var coverImagePreview = document.getElementById('cover_image_preview');
if (coverImagePreview) {
coverImagePreview.src = coverImagePreview.dataset.url;
coverImagePreview.style.display = 'opacity-100';
}
});

var dropZone = document.getElementById('drop_zone');
var fileInput = document.getElementById('cover_image');

dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
dropZone.classList.add('bg-gray-100');
});

dropZone.addEventListener('dragleave', function(e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-100');
});

dropZone.addEventListener('drop', function(e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-100');

var files = e.dataTransfer.files;
if (files.length > 0) {
fileInput.files = files;
// Trigger the 'change' event to update the preview
var event = new Event('change');
fileInput.dispatchEvent(event);
}
});
</script>
17 changes: 17 additions & 0 deletions app/views/blog_posts/edit.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<% meta title: "Edit blog post", description: "Edit blog post." %>

<h1 class="text-xl text-center font-bold leading-tight text-gray-900 mx-4 lg:mx-0 mt-8 lg:mt-16">Edit blog post</h1>

<div class="flex justify-center flex-col space-y-3 mt-4 text-center">
<%= link_to "Show blog post", blog_post_path(@blog_post.slug), class:"mx-auto w-fit shrink-0 mt-4 md:mt-0 inline-flex items-center justify-center sm:justify-start px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" %>
<%= link_to "Back to blog posts", blog_posts_path, class:"mx-auto w-fit shrink-0 mt-4 md:mt-0 inline-flex items-center justify-center sm:justify-start px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" %>
</div>

<div class="max-w-2xl mx-auto mt-8">
<%= render "form", blog_post: @blog_post %>
</div>

<div class="flex justify-center flex-col space-y-3 mt-4 mb-8 text-center">
<%= link_to "Show blog post", blog_post_path(@blog_post.slug), class:"mx-auto w-fit shrink-0 mt-4 md:mt-0 inline-flex items-center justify-center sm:justify-start px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" %>
<%= link_to "Back to blog posts", blog_posts_path, class:"mx-auto w-fit shrink-0 mt-4 md:mt-0 inline-flex items-center justify-center sm:justify-start px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" %>
</div>
60 changes: 60 additions & 0 deletions app/views/blog_posts/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<% meta title: "Blog posts", description: "All blog posts from #{ENV['COMPANY_NAME']}." %>
<%# Title %>
<div class="mt-16 mx-auto max-w-3xl px-4 sm:mt-24">
<div class="text-center">
<h1 class="text-2xl tracking-tight font-extrabold sm:text-3xl md:text-4xl font-display text-gray-800">
<span class="block">Blog posts</span>
</h1>
<p class="mt-3 max-w-md mx-auto text-base text-gray-500 sm:text-lg md:mt-5 md:text-xl md:max-w-3xl">
All <%= ENV['COMPANY_NAME'] %> blog posts.
</p>
</div>
</div>

<% if current_user&.admin? %>
<%# New blog post button, only accessibly by admins %>
<div class="mt-8 mx-auto max-w-fit">
<div class="group relative w-full flex justify-center py-2.5 px-4 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none">
<div class="text-base flex justify-center items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 -ml-1 mr-2" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd"></path>
</svg>
<%= link_to new_blog_post_path, class: "focus:outline-none" do %>
<span class="absolute inset-0" aria-hidden="true"></span>
New blog post
<% end %>
</div>
</div>
</div>
<% end %>

<div class="font-inter antialiased text-gray-800 tracking-tight my-16">
<main class="grow">
<section>
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<div class="mx-auto mt-16 grid max-w-2xl grid-cols-1 gap-x-8 gap-y-20 lg:mx-0 lg:max-w-none lg:grid-cols-3">
<%# Blog post list %>
<% @blog_posts.each do |blog_post| %>
<%= render blog_post %>
<% end %>
</div>
</div>

<% if current_user && current_user.admin? && @drafts.any? %>
<div class="mx-auto max-w-7xl px-6 lg:px-8 mt-16 border border-dashed bg-white py-12">
<p class="text-2xl tracking-tight font-extrabold sm:text-2xl md:text-3xl font-display text-gray-800">
Drafts
</p>
<div class="mx-auto mt-8 grid max-w-2xl grid-cols-1 gap-x-8 gap-y-20 lg:mx-0 lg:max-w-none lg:grid-cols-3">
<%# Drafts %>
<% @drafts.each do |blog_post| %>
<%= render blog_post %>
<% end %>
</div>
</div>
<% end %>
</section>
</main>

</div>
15 changes: 15 additions & 0 deletions app/views/blog_posts/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<% meta title: "Create a new blog post", description: "New blog post for #{ENV['COMPANY_NAME']}." %>

<h1 class="text-xl text-center font-bold leading-tight text-gray-900 mx-4 lg:mx-0 mt-8 lg:mt-16">New blog post</h1>

<div class="flex justify-center flex-col space-y-3 mt-4 text-center">
<%= link_to "Back to blog posts", blog_posts_path, class:"mx-auto w-fit shrink-0 mt-4 md:mt-0 inline-flex items-center justify-center sm:justify-start px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" %>
</div>

<div class="max-w-2xl mx-auto mt-8">
<%= render "form", blog_post: @blog_post %>
</div>

<div class="flex justify-center flex-col space-y-3 mt-4 mb-8 text-center">
<%= link_to "Back to blog posts", blog_posts_path, class:"mx-auto w-fit shrink-0 mt-4 md:mt-0 inline-flex items-center justify-center sm:justify-start px-4 py-2 border border-gray-300 shadow-sm text-base font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500" %>
</div>
Loading

0 comments on commit 9fbfdd3

Please sign in to comment.