TailwindDemoBlog Tailwind Templates

Tailwinddemoblog

Walkthrough

Init App

rails new Demo --database=postgresql --skip-action-mailer --skip-action-mailbox --skip-test --skip-system-test --skip-jbuilder --css=tailwind

cd Demo

bundle install

bin/rails db:create

Open a New Terminal Tab & run:

bin/rails tailwindcss:watch

Now back to the Terminal Tab you began run the server to test that everything is behaving as supposed

bin/rails s
^C Exiting

Device & Active Storage

Let's install devise:

bundle add devise
bundle install

bin/rails g devise:install

Follow device instructions

Install devise views

bin/rails g devise:views

Let's enable and install Active storage:

Uncomment the following line

./Gemfile

gem "image_processing", "~> 1.2"

Now run:

bin/rails active_storage:install
bin/rails db:migrate

Now let's install an Admin model for devise

bin/rails g devise Author birthday:date nickname biography:text background:decimal

Uncomment the trakable information from the migration generated by devise

t.integer  :sign_in_count, default: 0, null: false
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string   :current_sign_in_ip
t.string   :last_sign_in_ip

Now migrate it

bin/rails db:migrate

Generate The admin main page and derivates

bin/rails g controller Authors control_panel profile settings --no-helper

Add an avatar image to the writer and enable trackable

class Author < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :trackable

  has_one_attached :avatar
end

Force author to authenticate on the author controller Add the line to the controller:

before_action :authenticate_author!

Now let's test it:

bin/rails s

Layout / Authentication Forms

Enable a custom layout for devise: https://github.com/heartcombo/devise/wiki/How-To:-Create-custom-layouts

class ApplicationController < ActionController::Base
  layout :layout_by_resource

  private

  def layout_by_resource
    if devise_controller?
      "devise"
    else
      "application"
    end
  end
end

Now create a layout: layouts/devise.html.erb

Customize your font if you want Follow the instructions here: https://tailwindcss.com/docs/font-family#customizing-your-theme

If you download the font, you can create a directory under app/assets/fonts Then drop the font directory in that path

Then add the font with the following: (Let Exo be my font of choice)

@layer base {
  @font-face {
    font-family: 'Exo Regular';
    font-optical-sizing: auto;
    font-display: swap;
    src: url('Exo/static/Exo-Regular.ttf');
  }
}

Now let's customize our devise layout. To enable dark mode, add the following to the tailwind config:

module.exports = {
  darkMode: 'selector',
  // ...
}

And now let's use this configuration to our advantage. Basically we can add the class "dark" to our html element and it will implement the dark mode. First let's create a stimulus controller that allows us to setup and toggle the themes.

app/javascript/controllers/theme_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    // Loading 
    this.setTheme()
    // Finished loading
  }

  enableLightTheme() {
    localStorage.theme = 'light'
    this.setTheme()
  }

  enableDarkTheme() {
    localStorage.theme = 'dark'
    this.setTheme()
  }

  removeTheme() {
    localStorage.removeItem('theme')
    this.setTheme()
  }

  setTheme() {
    console.log("Setting theme...")
    if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
      document.documentElement.classList.add('dark')
    } else {
      document.documentElement.classList.remove('dark')
    }
  }
}

Taking the following example let's create our new session: https://tailwindui.com/components/application-ui/forms/sign-in-forms

<!DOCTYPE html>
<html class="h-full">
  <head>
    <title>Login/Register</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="icon" type="image/x-icon" href="<%= asset_path("icon.ico") %>">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>

  <body
    class="h-full bg-white dark:bg-gray-800"
    data-controller="theme"
  >
  <div class="flex min-h-full">
    <div class="flex-1 flex min-h-full flex-col justify-center px-4 py-8 lg:px-8">
      <!-- Logo and title -->
      <div class="sm:mx-auto sm:w-full sm:max-w-sm">
        <img class="mx-auto h-14 w-auto hidden dark:block" src="<%= asset_path("icons/logo-dark.svg") %>" alt="Your Company">
        <img class="mx-auto h-14 w-auto block dark:hidden" src="<%= asset_path("icons/logo-light.svg") %>" alt="Your Company">
        <h2 class="mt-6 text-center text-2xl font-bold leading-9 tracking-tight text-gray-900 dark:text-gray-300"><%= yield(:devise_title) %></h2>
      </div>
      <!-- Content -->
      <main class="mt-2 sm:mx-auto sm:w-full sm:max-w-sm">
        <%= yield %>
      </main>
      <!-- Footer -->
      <div class="mt-10 flex justify-center items-center gap-2">
        <div class="bg-indigo-500 rounded-full p-1 flex items-center justify-between gap-4">
          <button
            class="w-8 h-auto bg-gray-100 dark:bg-gray-400 hover:bg-gray-600 dark:hover:none inline-block rounded-full p-1"
            data-action="click->theme#enableDarkTheme"
          >
            <img src="<%= asset_path("icons/dark-mode.svg") %>">
          </button>
          <button
            class="w-8 h-auto dark:bg-gray-100 bg-gray-400 dark:hover:bg-gray-600 inline-block rounded-full p-1"
            data-action="click->theme#enableLightTheme"
          >
            <img src="<%= asset_path("icons/light-mode.svg") %>">
          </button>
        </div>
      </div>
    </div>
    <!-- Large Devices Curtain -->
    <div class="flex-1 bg-white bg-cover bg-center bg-no-repeat hidden md:block" style="background-image: url(<%= asset_path('notebook-bg.jpg') %>);">
      <div class="h-full w-full bg-gray-500 dark:bg-gray-800 opacity-70"></div>
    </div>
  </div>
  </body>
</html>

Notes: I've included an ico so the page will have a cute icon. I've included two logos, for dark mode and light mode the icons are two svgs. You can copy and paste them from the hidden block bellow. I used a background image as well, you can use whatever image you want, I used this one:

Svgs:

Light:

PULGAMECANICA
<svg viewBox="0 0 128 64" fill="#1C274C" xmlns="http://www.w3.org/2000/svg">
  <g style="transform: scale(2) translateX(15%);">
    <path d="M14.1809 4.2755C14.581 4.3827 14.8185 4.79396 14.7113 5.19406L10.7377 20.0238C10.6304 20.4239 10.2192 20.6613 9.81909 20.5541C9.41899 20.4469 9.18156 20.0356 9.28876 19.6355L13.2624 4.80583C13.3696 4.40573 13.7808 4.16829 14.1809 4.2755Z"></path>
    <path d="M16.4425 7.32781C16.7196 7.01993 17.1938 6.99497 17.5017 7.27206L19.2392 8.8358C19.9756 9.49847 20.5864 10.0482 21.0058 10.5467C21.4468 11.071 21.7603 11.6342 21.7603 12.3295C21.7603 13.0248 21.4468 13.5881 21.0058 14.1123C20.5864 14.6109 19.9756 15.1606 19.2392 15.8233L17.5017 17.387C17.1938 17.6641 16.7196 17.6391 16.4425 17.3313C16.1654 17.0234 16.1904 16.5492 16.4983 16.2721L18.1947 14.7452C18.9826 14.0362 19.5138 13.5558 19.8579 13.1467C20.1882 12.7541 20.2603 12.525 20.2603 12.3295C20.2603 12.1341 20.1882 11.9049 19.8579 11.5123C19.5138 11.1033 18.9826 10.6229 18.1947 9.91383L16.4983 8.387C16.1904 8.10991 16.1654 7.63569 16.4425 7.32781Z"></path>
    <path d="M7.50178 8.387C7.80966 8.10991 7.83462 7.63569 7.55752 7.32781C7.28043 7.01993 6.80621 6.99497 6.49833 7.27206L4.76084 8.8358C4.0245 9.49847 3.41369 10.0482 2.99428 10.5467C2.55325 11.071 2.23975 11.6342 2.23975 12.3295C2.23975 13.0248 2.55325 13.5881 2.99428 14.1123C3.41369 14.6109 4.02449 15.1606 4.76082 15.8232L6.49833 17.387C6.80621 17.6641 7.28043 17.6391 7.55752 17.3313C7.83462 17.0234 7.80966 16.5492 7.50178 16.2721L5.80531 14.7452C5.01743 14.0362 4.48623 13.5558 4.14213 13.1467C3.81188 12.7541 3.73975 12.525 3.73975 12.3295C3.73975 12.1341 3.81188 11.9049 4.14213 11.5123C4.48623 11.1033 5.01743 10.6229 5.80531 9.91383L7.50178 8.387Z"></path>
  </g>
  <text x="50%" y="85%" dominant-baseline="middle" text-anchor="middle" style="font-family: 'Exo Regular';font-size: 12px;">PULGAMECANICA</text>
</svg>

Dark: PULGAMECANICA

<svg viewBox="0 0 128 64" fill="#e5e7eb" xmlns="http://www.w3.org/2000/svg">
  <g style="transform: scale(2) translateX(15%);">
    <path d="M14.1809 4.2755C14.581 4.3827 14.8185 4.79396 14.7113 5.19406L10.7377 20.0238C10.6304 20.4239 10.2192 20.6613 9.81909 20.5541C9.41899 20.4469 9.18156 20.0356 9.28876 19.6355L13.2624 4.80583C13.3696 4.40573 13.7808 4.16829 14.1809 4.2755Z"></path>
    <path d="M16.4425 7.32781C16.7196 7.01993 17.1938 6.99497 17.5017 7.27206L19.2392 8.8358C19.9756 9.49847 20.5864 10.0482 21.0058 10.5467C21.4468 11.071 21.7603 11.6342 21.7603 12.3295C21.7603 13.0248 21.4468 13.5881 21.0058 14.1123C20.5864 14.6109 19.9756 15.1606 19.2392 15.8233L17.5017 17.387C17.1938 17.6641 16.7196 17.6391 16.4425 17.3313C16.1654 17.0234 16.1904 16.5492 16.4983 16.2721L18.1947 14.7452C18.9826 14.0362 19.5138 13.5558 19.8579 13.1467C20.1882 12.7541 20.2603 12.525 20.2603 12.3295C20.2603 12.1341 20.1882 11.9049 19.8579 11.5123C19.5138 11.1033 18.9826 10.6229 18.1947 9.91383L16.4983 8.387C16.1904 8.10991 16.1654 7.63569 16.4425 7.32781Z"></path>
    <path d="M7.50178 8.387C7.80966 8.10991 7.83462 7.63569 7.55752 7.32781C7.28043 7.01993 6.80621 6.99497 6.49833 7.27206L4.76084 8.8358C4.0245 9.49847 3.41369 10.0482 2.99428 10.5467C2.55325 11.071 2.23975 11.6342 2.23975 12.3295C2.23975 13.0248 2.55325 13.5881 2.99428 14.1123C3.41369 14.6109 4.02449 15.1606 4.76082 15.8232L6.49833 17.387C6.80621 17.6641 7.28043 17.6391 7.55752 17.3313C7.83462 17.0234 7.80966 16.5492 7.50178 16.2721L5.80531 14.7452C5.01743 14.0362 4.48623 13.5558 4.14213 13.1467C3.81188 12.7541 3.73975 12.525 3.73975 12.3295C3.73975 12.1341 3.81188 11.9049 4.14213 11.5123C4.48623 11.1033 5.01743 10.6229 5.80531 9.91383L7.50178 8.387Z"></path>
  </g>
  <text x="50%" y="85%" dominant-baseline="middle" text-anchor="middle" style="font-family: 'Exo Regular';font-size: 12px;">PULGAMECANICA</text>
</svg>

Modify the views/devise/sessions/new.html.erb file:

<% content_for :devise_title do %>
Sign in to your account
<% end %>

<%= form_for(resource, as: resource_name, url: session_path(resource_name), html: {class: "space-y-6"}) do |f| %>
  <div>
    <%= f.label :email, class: "block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300" %>
    <div class="mt-2">
      <%= f.email_field :email, autofocus: true, autocomplete: "email", required: true, class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" %>
    </div>
  </div>

  <div>
    <%= f.label :password, class: "block text-sm font-medium leading-6 text-gray-900 dark:text-gray-300" %>
    <%= f.password_field :password, autocomplete: "current-password", required: true, class: "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" %>
  </div>

  <% if devise_mapping.rememberable? %>
    <div class="field">
      <%= f.check_box :remember_me %>
      <%= f.label :remember_me %>
    </div>
  <% end %>

  <div>
    <%= f.submit "Log in", class: "flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"%>
  </div>
<% end %>

<%= render "devise/shared/links" %>

You should have a nice login page now. We can implement some cool dynamics for the flash alert and notice.

Let's change slightly our devise layout:

<!DOCTYPE html>
<html class="h-full">
...

  <body
    class="h-full bg-white dark:bg-gray-800"
    data-controller="theme"
  >
  <div class="flex min-h-full">
    <div class="flex-1 flex relative min-h-full flex-col justify-center px-4 py-8 lg:px-8">
      <!-- Alert and Notice -->
      <div
        data-controller="hide"
        class="absolute w-full left-0 top-0 opacity-90 flex flex-col gap-1 py-3 px-1 md:px-10 justify-center items-center"
        data-action="mouseover->hide#appear mouseout->hide#disappear"
      >
        <% [[notice, 'bg-indigo-500'], [alert, 'bg-red-500']].each do |msg, bg| %>
        <% if not msg.nil? %>
        <div class="<%= bg %> rounded-full w-full flex items-center text-white text-sm font-bold px-4 py-3 justify-center" role="alert">
          <div class="flex-1 flex items-center justify-center gap-2">
            <svg class="flex-shrink-0 w-5 h-5" viewBox="0 0 24.00 24.00" fill="none">
              <path d="M12 7V13" stroke="#f6f5f4" stroke-width="2.4" stroke-linecap="round"></path>
              <circle cx="12" cy="16" r="1" fill="#f6f5f4"></circle>
              <path d="M7 3.33782C8.47087 2.48697 10.1786 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 10.1786 2.48697 8.47087 3.33782 7" stroke="#f6f5f4" stroke-width="2.4" stroke-linecap="round"></path>
            </svg>
            <p><%= msg %></p>
          </div>
          <button data-action="click->hide#close" class="w-6 h-6 rounded-full p-0 hover:bg-gray-200 flex items-center justify-center"><img src="<%= asset_path("icons/close.svg") %>"></button>
        </div>
        <% end %>
        <% end %>
      </div>
      ...
      ...
      ...
    </div>
  </div>
  </body>
</html>

As you would have guessed it, we need to create the stimulus controller which we are calling "hide"

rails g stimulus hide

And create the following controller:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.disappear()
  }

  disappear() {
    this.element.style.transition = "transform 4s ease-in-out 1s"
    this.element.style.transform = "translateY(-100%)"
  }

  appear() {
    this.element.style.transition = "none"
    this.element.style.transform = "translateY(0%)"
  }

  close() {
    this.element.style.display = "none"
  }
}

Now we have a beautiful login page with alerts and message which disapear automatically.

Go ahead and create an Author:

rails c

> Author.create!(email: "[email protected]", birthday: Time.now, nickname: "pulgamecanica", biography: "Awesome Programmer", background: 1, password: "author123", password_confirmation: "author123")

Now make sure you can login with this author on the login page!

Author Page

Let's move on and continue developing the 3 pages we created before:

  • control_panel
  • profile
  • settings

Let's begin by creating an admin layout

cp app/views/layouts/application.html.erb app/views/layouts/author.html.erb

And add this layout to the author controller:

class AuthorsController < ApplicationController
  layout 'author'
  
  before_action :authenticate_author!

  def control_panel
  end

  def profile
  end

  def settings
  end
end

Spotify-like admin page

This page will be interesting if you like front-end development.

We will use Hotwire, Stimulus, and d3 to generate an page where authors can manage their posts and many more features.

We will use the gem "Fiendly ID" this gem allows us to easily map our id's to slug names for our pages. Instead of doing GET /post/1 we would do post/ruby_is_awesome_post which is more friendly for the users. And easier to visualize.

Add the following gem to your gemfile: https://github.com/norman/friendly_id

gem 'friendly_id', '~> 5.5.0'

And install it:

bundle install

Now generate the migration for the gem to work

bin/rails generate friendly_id
bin/rails db:migrate

If you look at the migration you can somehow understand how the gem works, it'w quite simple and awesome!

Let's generatte our Posts sturcture:

rails g scaffold authors/Post title description:text published_at:datetime published:boolean author:references slug:uniq --no-helper
bin/rails db:migrate

I don't like this setup just quite... If you try to visit /authors/posts you will realize that it's not useing the author layout, which is unfortunate. Also you might have noticed that the scaffold generated a file on the models authors.rb

We don't want to use namespaces for our Author resources, we want to use modules, and we want the resources which belong to the author to inherit directly it's controller. This way the resourcess will have access to the current_author which will be very convenient.

First let's modify a little our posts_controller.rb

post_controller.rb

# From this
class Authors::PostsController < ApplicationController

# To this
module Authors
  class PostsController < AuthorsController
    ...
    ...
    ...
  end
end

Now if you reload the page, you will see that it will immediately inherit from AuthorsController, therfore also implementing the author layout!

Note: Make sure to reload the server, or you will get an error: NameError: uninitialized constant Authors::Post::FriendlyId

Let's implement friendly id's for our posts slug, in the models/post.rb

class Authors::Post < ApplicationRecord
  extend FriendlyId
  friendly_id :title, use: :slugged
  
  belongs_to :author
end

And if course we must setup the Author model in order to support author.posts

models/author.rb

class Author < ApplicationRecord
  devise :database_authenticatable,
          :rememberable, :validatable, :trackable

  has_one_attached :avatar
  has_many :posts, class_name: "Authors::Post"
end

It is necessary to specify explicitly the class_name since it's a module model, see more here: https://guides.rubyonrails.org/association_basics.html#controlling-association-scope

Modify the following function on the posts_controller.rb to support friendly id's

def set_authors_post
  @authors_post = Authors::Post.friendly.find(params[:id])
end

And now the following functions also from posts_controller.rb in order to build the posts from the current author, and in the future omit the author_id

# GET /authors/posts/new
    def new
      @authors_post = current_author.posts.build
    end

    # GET /authors/posts/1/edit
    def edit
    end

    # POST /authors/posts
    def create
      @authors_post = current_author.posts.build(authors_post_params)

      if @authors_post.save
        redirect_to @authors_post, notice: "Post was successfully created."
      else
        render :new, status: :unprocessable_entity
      end
    end

Now try and create a post: It will work.

BUT WE WILL REVEAL A BUG WHICH EXISTS ON RAILSSSSS MOAHAHAHAHAHAHHA

The index will stop working because rails didn't concider the author was scoped and generated edit_post_path instead of edit_authors_post_path

but we can just easily fix that by putting the correct one: edit_authors_post_path

Now we should focus on creating an amazing spotify-like interface. Let's get rolling with that!

Start simple:

<!DOCTYPE html>
<html class="h-full">
  <head>
    <title>Authors- Pulgamecanica</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="icon" type="image/x-icon" href="<%= asset_path("icon.ico") %>">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>

  <body
    class="h-full bg-white dark:bg-gray-800"
    data-controller="theme"
  >
    <!-- Main Divides the quick bottom menu and the rest of the application-->
    <main class="flex flex-col flex-nowrap w-full h-full items-center justify-between">
      
      <!-- Application main section-->
      <section class="flex-1 w-full h-full flex flex-wrap gap-1 items-stretch p-1">
        <div class="flex flex-col gap-1">
          <div class="w-16 bg-purple-600 rounded-lg">Home / Search</div>
          <div class="w-16 flex-1 bg-purple-600 rounded-lg">Posts / List</div>
          <div class="w-16 flex-1 bg-purple-600 rounded-lg">Projects / List</div>
        </div>

        <div class="flex-1 bg-purple-600 rounded-lg">Main Content</div>
        
        <div class="w-16 bg-purple-600 rounded-lg">Featured Content</div>
      </section>

      <!-- Bottom Menu -->
      <section class="w-full bg-black flex justify-between items-center px-5 gap-1">
<!--1--><img class="h-8 w-auto" src="<%= asset_path("icons/logo-dark.svg") %>">
<!--2--><div class="text-white">by pulgmecanica</div>
<!--3--><div class="flex justify-center items-center py-1">
          <div class="bg-gray-500 rounded-full p-1 flex items-center justify-between gap-2">
            <button
              class="w-6 h-auto bg-gray-100 dark:bg-gray-400 hover:bg-gray-600 dark:hover:none inline-block rounded-full p-1"
              data-action="click->theme#enableDarkTheme"
            >
              <img src="<%= asset_path("icons/dark-mode.svg") %>">
            </button>
            <button
              class="w-6 h-auto dark:bg-gray-100 bg-gray-400 dark:hover:bg-gray-600 inline-block rounded-full p-1"
              data-action="click->theme#enableLightTheme"
            >
              <img src="<%= asset_path("icons/light-mode.svg") %>">
            </button>
          </div>
        </div>
      </section>
    </main>
  </body>
</html>

With a simple layout divided first in 2

The Upper and Bottom parts. Upper => The whole application Bottom => Quick black horizontal menu

Then the upper is divided in 3 Horizontally, from left to right. The menus, the content of the application, and the feauted content (this later can be closed)

It's time to implement a very fun feature which I love about Spotify, the resizisable boxes.

For this we will be using d3.

Let's begin by generating our stimulus controller:

rails g stimulus resizable

Note: if you want to load the js as they appear in the DOM instead of loading every controller all the time, you must edit the import map and append preload: false to the stimulus controllers

Then on the index.js enable the lazyLoadControllersFrom and comment the eagerLoadControllersFrom

Let's add the resizers and implement the resizable data controllers:

 <!-- Application main section-->
<section class="flex-1 w-full h-full flex flex-wrap items-stretch p-1">
  <div
    class="flex"
    data-controller="resizable"
    data-resizable-storage-value="menu"
  >
    <div class="relative flex-1 flex flex-col gap-1">
      <div class="bg-purple-600 rounded-lg">Home / Search</div>
      <div class="flex-1 bg-purple-600 rounded-lg">Posts / List</div>
      <div class="flex-1 bg-purple-600 rounded-lg">Projects / List</div>
    </div>
    <div
      class="w-2 cursor-col-resize bg-black"
      data-resizable-target="resizer"
    ></div>
  </div>

  <div class="flex-1 bg-purple-600 rounded-lg">Main Content</div>
  
  <div
    class="relative flex flex-row-reverse"
    data-controller="resizable"
    data-resizable-storage-value="featured"
    data-resizable-right-to-left-value="false"
  >
    <div class="flex-1 bg-purple-600 rounded-lg">Featured Content</div>
    <div
      class="w-2 cursor-col-resize bg-black"
      data-resizable-target="resizer"
    ></div>

  </div>
</section>
...

Now let's install d3:

bin/importmap pin d3

And for each pin, append preload: false so that it wont be loaded by default

Now place the following d3 code for resizing in your controller

resize_controller.js

import { Controller } from "@hotwired/stimulus"
import { select, selectAll, drag, pointer, scaleLinear } from "d3"

// LocalStorage Helper Functions
const getLocalStorageOrDefault = (storage) => {
  const currSettings = JSON.parse(localStorage.getItem(storage));
  if (currSettings === null)
    return 100;
  else
    return currSettings;
}
function saveToLocalStorage(key, value) {
  localStorage.setItem(key, JSON.stringify(value));
}

// Scale Helpers & Functions
const SIDEBAR_WIDTH_MIN = 50
const SIDEBAR_WIDTH_MAX = 600
const myScale = scaleLinear()
  .domain([SIDEBAR_WIDTH_MIN, SIDEBAR_WIDTH_MAX])
  .range([SIDEBAR_WIDTH_MIN, SIDEBAR_WIDTH_MAX])
  .clamp(true)

// When using this controller be careful and don't give the value to your data-values
// that could conflict with another local Storage elemnt, such as 'theme'
export default class extends Controller {
  static targets = [ "resizer" ]
  static values = {
    storage: String,
    rightToLeft: { type: Boolean, default: true } 
  }

  connect() {
    // Create a d3 reference to the resizable element
    const resizable = select(this.element)
    if (window.innerWidth > 480)  { // Do not resize for small resolutions
      resizable.style("width", getLocalStorageOrDefault(this.storageValue) + "px")
    }
    // Debugggggg
    console.log(this.storageValue, resizable.style("width"))
    // Resize D3:drag component
    select(this.resizerTarget).call(drag()
        .on('drag', () => {
          // Get the width based on the pointer position
          let x = pointer(event, resizable)[0]
          if (this.rightToLeftValue == false) {
            x = window.innerWidth - x
          }
          // Resize the element
          resizable.style('width', myScale(x) + 'px')
          // sidebarIconsResponsive(x);
        })
        .on('end', () => {
          // On drag end save the width to localStorage
          let x = pointer(event, resizable)[0]
          if (this.rightToLeftValue == false) {
            x = window.innerWidth - x
          }
          saveToLocalStorage(this.storageValue, myScale(x))
        })
    );
  }
}

Now you should be able to resize freely both ends of the boxes. Now we must concider responsivity. Phones don't need to resize they don't even use a mouse. Also taking as an example spotify we can make something similar.

The follwing is more compatible with phones:

<!DOCTYPE html>
<html class="h-full">
  <head>
    <title>Authors- Pulgamecanica</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link rel="icon" type="image/x-icon" href="<%= asset_path("icon.ico") %>">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>

  <body
    class="h-full bg-white dark:bg-black"
    data-controller="theme"
  >
    <!-- Main Divides the quick bottom menu and the rest of the application-->
    <main class="w-full h-full pb-10 text-gray-800 dark:text-gray-200">

      <!-- Application main section-->
      <section class="w-full h-full flex flex-wrap md:flex-nowrap items-stretch p-1 gap-1 md:gap-0">
        <div
          class="flex w-full md:w-auto"
          data-controller="resizable"
          data-resizable-storage-value="menu"
        >
          <div class="flex-1 block overflow-hidden break-words">
            <div class="flex h-full flex-col gap-1">
              <!-- Home / Search -->
              <div class="bg-gray-200 dark:bg-gray-800 rounded-[0.5rem]">
                <% [[authors_control_panel_path, "HOME", asset_path("icons/home-light.svg"), asset_path("icons/home-dark.svg")],[authors_control_panel_path, "SEARCH", asset_path("icons/search-light.svg"), asset_path("icons/search-dark.svg")]].each do |link, text, light_icon_path, dark_icon_path| %>
                <%= link_to link, class: "flex w-full items-center gap-2 group p-2" do %>
                <div
                  class="opacity-60 group-hover:opacity-100 w-8 h-auto hidden dark:inline-block rounded-full p-1"
                >
                  <img src="<%= light_icon_path %>">
                </div>
                <div
                  class="opacity-60 group-hover:opacity-100 w-8 h-auto inline-block dark:hidden rounded-full p-1"
                >
                  <img src="<%= dark_icon_path %>">
                </div>
                <h1 class="group-hover:underline group-hover:text-gray-800 dark:group-hover:text-white text-gray-600 dark:text-gray-400 font-bold"><%= text %></h1>
                <% end %>
                <% end %>
              </div>
              <!-- Posts -->
              <div class="flex-1 bg-gray-200 dark:bg-gray-800 rounded-[0.5rem] flex flex-nowrap flex-col">
                <div class="flex flex-row items-center justify-between">
                  <%= link_to authors_posts_path, class: "flex-1 flex w-full items-center gap-2 group p-2" do %>
                  <div
                    class="opacity-60 group-hover:opacity-100 w-8 h-auto hidden dark:inline-block rounded-full p-1"
                    data-action="click->theme#enableDarkTheme"
                  >
                    <img src="<%= asset_path("icons/library-light.svg") %>">
                  </div>
                  <div
                    class="opacity-60 group-hover:opacity-100 w-8 h-auto inline-block dark:hidden rounded-full p-1"
                    data-action="click->theme#enableLightTheme"
                  >
                    <img src="<%= asset_path("icons/library-dark.svg") %>">
                  </div>
                  <h1 class="group-hover:underline group-hover:text-gray-800 dark:group-hover:text-white text-gray-600 dark:text-gray-400 font-bold">POSTS</h1>
                  <% end %>

                  <%= link_to new_authors_post_path, class: "group w-8 h-auto inline-block dark:hidden rounded-full p-1" do %>
                    <img class="opacity-60 group-hover:opacity-100" src="<%= asset_path("icons/add-dark.svg") %>">
                  <% end %>

                  <%= link_to new_authors_post_path, class: "group w-8 h-auto hidden dark:inline-block rounded-full p-1" do %>
                    <img class="opacity-60 group-hover:opacity-100" src="<%= asset_path("icons/add-light.svg") %>">
                  <% end %>
                </div>
                <button class="flex group justify-end items-center gap-2">
                  <p class="group-hover:underline group-hover:text-gray-800 dark:group-hover:text-white text-gray-600 dark:text-gray-400 text-sm">Recents</p>
                  <img class="hidden dark:inline-block h-4 w-4 opacity-60 group-hover:opacity-100" src="<%= asset_path("icons/filter-light.svg") %>">
                  <img class="inline-block dark:hidden h-4 w-4 opacity-60 group-hover:opacity-100" src="<%= asset_path("icons/filter-dark.svg") %>">
                </button>
                <div class="flex-1 text-white">
                </div>
              </div>
              <!-- Projects -->
              <div class="flex-1 bg-gray-200 dark:bg-gray-800 rounded-[0.5rem] flex flex-nowrap flex-col">
                <div class="flex flex-row items-center justify-between">
                  <%= link_to authors_posts_path, class: "flex-1 flex w-full items-center gap-2 group p-2" do %>
                  <div
                    class="opacity-60 group-hover:opacity-100 w-8 h-auto hidden dark:inline-block rounded-full p-1"
                    data-action="click->theme#enableDarkTheme"
                  >
                    <img src="<%= asset_path("icons/library-light.svg") %>">
                  </div>
                  <div
                    class="opacity-60 group-hover:opacity-100 w-8 h-auto inline-block dark:hidden rounded-full p-1"
                    data-action="click->theme#enableLightTheme"
                  >
                    <img src="<%= asset_path("icons/library-dark.svg") %>">
                  </div>
                  <h1 class="group-hover:underline group-hover:text-gray-800 dark:group-hover:text-white text-gray-600 dark:text-gray-400">POSTS</h1>
                  <% end %>

                  <%= link_to new_authors_post_path, class: "group w-8 h-auto inline-block dark:hidden rounded-full p-1" do %>
                    <img class="opacity-60 group-hover:opacity-100" src="<%= asset_path("icons/add-dark.svg") %>">
                  <% end %>

                  <%= link_to new_authors_post_path, class: "group w-8 h-auto hidden dark:inline-block rounded-full p-1" do %>
                    <img class="opacity-60 group-hover:opacity-100" src="<%= asset_path("icons/add-light.svg") %>">
                  <% end %>
                </div>
                <button class="flex group justify-end items-center gap-2">
                  <p class="group-hover:underline group-hover:text-gray-800 dark:group-hover:text-white text-gray-600 dark:text-gray-400 text-sm">Recents</p>
                  <img class="hidden dark:inline-block h-4 w-4 opacity-60 group-hover:opacity-100" src="<%= asset_path("icons/filter-light.svg") %>">
                  <img class="inline-block dark:hidden h-4 w-4 opacity-60 group-hover:opacity-100" src="<%= asset_path("icons/filter-dark.svg") %>">
                </button>
                <div class="flex-1 text-white">
                </div>
              </div>
            </div>
          </div>
          <div
            class="hidden md:block w-1 cursor-col-resize hover:bg-gray-300 dark:hover:bg-gray-700"
            data-resizable-target="resizer"
          ></div>
        </div>

        <div class="w-full md:w-auto flex-1 flex-col flex bg-gray-200 dark:bg-gray-800 rounded-[0.5rem] overflow-y-scroll p-2">
          <div class="flex justify-end gap-1">
            <!-- Light svg, Dark svg, contrller-target-open-->
            <% [ [
                  asset_path("icons/bell-light.svg"),
                  asset_path("icons/bell-dark.svg"),
                  "notifications"
                ], [
                  asset_path("icons/profile-light.svg"),
                  asset_path("icons/profile-dark.svg"),
                  "profile_links"
                ]
            ].each do |light_icon_path, dark_icon_path, partial| %>
            <div
              data-controller="toggle-display"
              data-toggle-display-display-value="flex"
              class="relative font-bold text-gray-700 dark:text-gray-300"
            >
              <button
                class="p-2 flex dark:hidden bg-gray-100 justify-center items-center rounded-full group"
                data-action="click->toggle-display#toggle"
              >
                <img class="opacity-80 group-hover:opacity-100 h-5 w-5" src="<%= dark_icon_path %>">
              </button>
              <button
                class="p-2 hidden dark:flex bg-gray-900 justify-center items-center rounded-full group"
                data-action="click->toggle-display#toggle"
              >
                <img class="opacity-80 group-hover:opacity-100 h-5 w-5" src="<%= light_icon_path %>">
              </button>
              <div
                data-toggle-display-target="toggleableBox"
                class="absolute right-0 top-10 bg-gray-100 dark:bg-gray-900 rounded-sm p-1 flex flex-col hidden shadow-lg shadow-slate-900"
              >
                <%= render partial %>
              </div>
            </div>
          <% end %>
          </div>
          <div class="flex-1"><%= yield %></div>
        </div>
        
        <div
          class="w-full md:w-auto flex flex flex-row-reverse break-words"
          data-controller="resizable"
          data-resizable-storage-value="featured"
          data-resizable-right-to-left-value="false"
        >
          <div class="flex-1 bg-gray-200 dark:bg-gray-800 rounded-[0.5rem] overflow-hidden inline-block">
           <!-- Featured Content Here -->
          </div>

          <div
            class="hidden md:block w-1 cursor-col-resize hover:bg-gray-300 dark:hover:bg-gray-700"
            data-resizable-target="resizer"
          ></div>

        </div>
      </section>
      <!-- Bottom Menu -->
      <section class="w-full bg-black fixed md:relative bottom-0 left-0 flex justify-between items-center px-5 gap-1">
<!--1--><img class="h-8 w-auto" src="<%= asset_path("icons/logo-dark.svg") %>">
<!--2--><div class="text-white">by pulgmecanica</div>
<!--3--><div class="flex justify-center items-center py-1">
          <div class="bg-gray-500 rounded-full p-1 flex items-center justify-between gap-2">
            <button
              class="w-6 h-auto bg-gray-100 dark:bg-gray-400 hover:bg-gray-600 dark:hover:none inline-block rounded-full p-1"
              data-action="click->theme#enableDarkTheme"
            >
              <img src="<%= asset_path("icons/dark-mode.svg") %>">
            </button>
            <button
              class="w-6 h-auto dark:bg-gray-100 bg-gray-400 dark:hover:bg-gray-600 inline-block rounded-full p-1"
              data-action="click->theme#enableLightTheme"
            >
              <img src="<%= asset_path("icons/light-mode.svg") %>">
            </button>
          </div>
        </div>
      </section>
    </main>
  </body>
</html>

You need to generate a controller to handle toggle display boxes

rails g stimulus toggleDisplay
rails g stimulus worldSphereMap

toggle_display_controller.js

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="toggle-display"
export default class extends Controller {
  static targets = [ "toggleableBox" ]
  static values = {
    display: {type: String, default: "block"}
  }

  toggle() {
    if (this.toggleableBoxTarget.style.display === this.displayValue)
      this.toggleableBoxTarget.style.display = "none"
    else
      this.toggleableBoxTarget.style.display = this.displayValue
  }
}

world_sphere_controller.js

import { Controller } from "@hotwired/stimulus"
import { select, geoOrthographic, geoPath, drag, zoom, json } from "d3"

const WIDTH = 200
const HEIGHT = 200

// Connects to data-controller="world-sphere-map"
export default class extends Controller {

  connect() {
    console.log("Enabling Map")
    let width = WIDTH
    let height = HEIGHT
    const sensitivity = 75

    let projection = geoOrthographic()
      .scale(100)
      .center([0, 0])
      .rotate([0,-30])
      .translate([width / 2, height / 2])

    const initialScale = projection.scale()

    let path = geoPath().projection(projection)

    let svg = select(this.element)
      .attr("width", width)
      .attr("height", height)

    let globe = svg.append("circle")
      .attr("fill", "#EEE")
      .attr("stroke", "#000")
      .attr("stroke-width", "0.2")
      .attr("cx", width/2)
      .attr("cy", height/2)
      .attr("r", initialScale)

    svg.call(drag().on('drag', (event) => {
      event.stopPropagation
      const rotate = projection.rotate()
      const k = sensitivity / projection.scale()
      projection.rotate([
        rotate[0] + event.dx * k,
        rotate[1] - event.dy * k
      ])
      path = geoPath().projection(projection)
      svg.selectAll("path").attr("d", path)
      }))
      .call(zoom().on('zoom', (event) => {
        event.stopPropagation
        if(event.transform.k > 0.3) {
          projection.scale(initialScale * event.transform.k)
          path = geoPath().projection(projection)
          svg.selectAll("path").attr("d", path)
          globe.attr("r", projection.scale())
        }
        else {
          event.transform.k = 0.3
        }
      }))
      // .on("dblclick.zoom", null)
      // .on("mousewheel.zoom", null)
      // .on("DOMMouseScroll.zoom",null)

    let map = svg.append("g")

    json("/world.json").then(data => {
       map.append("g")
        .attr("class", "countries" )
        .selectAll("path")
        .data(data.features)
        .enter().append("path")
        .attr("class", d => "country_" + d.properties.name.replace(" ","_"))
        .attr("d", path)
        .attr("fill", "gray")
        .style('stroke', 'gray')
        .style('stroke-width', 0)
        .style("opacity",0.8)
    })
  }
}

Now you should have a very responsive design.

We will implement a featured content. We can do this with hotwire or stimulus. With stimulus you can fetch a json response and render however you want to an .erb In this case I decided to try out the modern framework which comes for free and built-in Turbo + HotWire!!!

Since we only want to change the content of the featured box and not the whole page (or the URL) we must implement turbo frames.

Let's now add our turbo frame:

On the author layout change the following line

<!-- Featured Content Here -->

For this:

<%= turbo_frame_tag :featured, src: authors_profile_path do %>

<% end %>

Top categories

Loading Svelte Themes