v0-rails
Version:
Convert React/JSX + Tailwind UI code from v0.dev to Rails ViewComponent classes and ERB templates with automatic slot detection, icon handling, and route generation
428 lines (376 loc) • 17.5 kB
Markdown
### Converting the Landing Page to Rails ViewComponents
To implement this landing page in a Rails application using ViewComponents, you'll need to restructure the React components into Ruby ViewComponents. Here's how you can approach it:
## 1. Setting up ViewComponents in Rails
First, make sure you have the ViewComponent gem installed:
```ruby file="Gemfile"
...
```
Run `bundle install` and then set up Tailwind CSS:
```shellscript
bin/rails tailwindcss:install
```
## 2. Creating the ViewComponents Structure
Here's how you would structure the landing page using ViewComponents:
```ruby file="app/components/button_component.rb"
...
# app/components/button_component.rb
class ButtonComponent < ViewComponent::Base
attr_reader :variant, :size, :class_name
def initialize(variant: "default", size: "default", class_name: "")
@variant = variant
@size = size
@class_name = class_name
end
private
def classes
base = "inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
variant_classes = {
"default" => "bg-primary text-primary-foreground shadow hover:bg-primary/90",
"outline" => "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
"ghost" => "hover:bg-accent hover:text-accent-foreground"
}
size_classes = {
"default" => "h-9 px-4 py-2 text-sm",
"sm" => "h-8 px-3 text-xs",
"lg" => "h-10 px-8 text-base"
}
[base, variant_classes[variant], size_classes[size], class_name].join(" ")
end
end
```
```plaintext file="app/components/button_component.html.erb"
...
<button class="<%= classes %>">
<%= content %>
</button>
```
```ruby file="app/components/card_component.rb"
...
# app/components/card_component.rb
class CardComponent < ViewComponent::Base
renders_one :header
renders_one :content
renders_one :footer
end
```
```plaintext file="app/components/card_component.html.erb"
...
<div class="rounded-lg border bg-card text-card-foreground shadow-sm">
<% if header %>
<div class="flex flex-col space-y-1.5 p-6">
<%= header %>
</div>
<% end %>
<% if content %>
<div class="p-6 pt-0">
<%= content %>
</div>
<% end %>
<% if footer %>
<div class="flex items-center p-6 pt-0">
<%= footer %>
</div>
<% end %>
</div>
```
```ruby file="app/components/icon_component.rb"
...
# app/components/icon_component.rb
class IconComponent < ViewComponent::Base
attr_reader :name, :class_name
def initialize(name:, class_name: "")
@name = name
@class_name = class_name
end
end
```
```plaintext file="app/components/icon_component.html.erb"
...
<% case name %>
<% when "rocket" %>
<svg xmlns="http://www.w3.org/2000/svg" class="<%= class_name %>" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"></path><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"></path><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"></path><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"></path></svg>
<% when "zap" %>
<svg xmlns="http://www.w3.org/2000/svg" class="<%= class_name %>" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>
<% when "code" %>
<svg xmlns="http://www.w3.org/2000/svg" class="<%= class_name %>" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>
<% when "globe" %>
<svg xmlns="http://www.w3.org/2000/svg" class="<%= class_name %>" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="2" x2="22" y1="12" y2="12"></line><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
<% when "check-circle" %>
<svg xmlns="http://www.w3.org/2000/svg" class="<%= class_name %>" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>
<% when "arrow-right" %>
<svg xmlns="http://www.w3.org/2000/svg" class="<%= class_name %>" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg>
<% end %>
```
## 3. Creating the Landing Page Component
```ruby file="app/components/landing_page_component.rb"
...
# app/components/landing_page_component.rb
class LandingPageComponent < ViewComponent::Base
def current_year
Time.current.year
end
end
```
```plaintext file="app/components/landing_page_component.html.erb"
...
<div class="flex min-h-screen flex-col">
<header class="sticky top-0 z-40 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div class="container flex h-16 items-center justify-between">
<div class="flex items-center gap-2 font-bold">
<%= render IconComponent.new(name: "rocket", class_name: "h-5 w-5 text-primary") %>
<span>DevStack</span>
</div>
<nav class="hidden md:flex gap-6">
<a href="#features" class="text-sm font-medium transition-colors hover:text-primary">Features</a>
<a href="#pricing" class="text-sm font-medium transition-colors hover:text-primary">Pricing</a>
<a href="#testimonials" class="text-sm font-medium transition-colors hover:text-primary">Testimonials</a>
<a href="#contact" class="text-sm font-medium transition-colors hover:text-primary">Contact</a>
</nav>
<div class="flex items-center gap-4">
<%= render(ButtonComponent.new(variant: "outline", size: "sm", class_name: "hidden md:flex")) do %>
Log in
<% end %>
<%= render(ButtonComponent.new(size: "sm")) do %>
Get Started
<% end %>
</div>
</div>
</header>
<main class="flex-1">
<section class="py-20 md:py-28">
<div class="container px-4 md:px-6">
<div class="flex flex-col items-center gap-4 text-center">
<div class="space-y-2">
<h1 class="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl">
Build faster with modern <span class="text-primary">dev tools</span>
</h1>
<p class="mx-auto max-w-[700px] text-muted-foreground md:text-xl">
Streamline your development workflow with our cutting-edge tools and components designed for the
modern web.
</p>
</div>
<div class="flex flex-col gap-2 min-[400px]:flex-row">
<%= render(ButtonComponent.new(size: "lg", class_name: "gap-1")) do %>
Get Started <%= render IconComponent.new(name: "arrow-right", class_name: "h-4 w-4") %>
<% end %>
<%= render(ButtonComponent.new(size: "lg", variant: "outline")) do %>
View Documentation
<% end %>
</div>
</div>
</div>
</section>
<section id="features" class="bg-muted/50 py-20">
<div class="container px-4 md:px-6">
<div class="mb-12 flex flex-col items-center gap-4 text-center">
<div class="space-y-2">
<h2 class="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl">
Features built for developers
</h2>
<p class="mx-auto max-w-[700px] text-muted-foreground md:text-xl">
Our platform provides everything you need to build modern web applications quickly and efficiently.
</p>
</div>
</div>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<%= render CardComponent.new do |c| %>
<% c.with_header do %>
<%= render IconComponent.new(name: "zap", class_name: "h-10 w-10 text-primary mb-2") %>
<h3 class="text-xl font-bold">Lightning Fast</h3>
<p class="text-sm text-muted-foreground">
Optimized for speed and performance, ensuring your applications load quickly.
</p>
<% end %>
<% c.with_content do %>
<ul class="space-y-2 text-sm">
<li class="flex items-center gap-2">
<%= render IconComponent.new(name: "check-circle", class_name: "h-4 w-4 text-primary") %>
<span>Optimized build process</span>
</li>
<li class="flex items-center gap-2">
<%= render IconComponent.new(name: "check-circle", class_name: "h-4 w-4 text-primary") %>
<span>Efficient code splitting</span>
</li>
<li class="flex items-center gap-2">
<%= render IconComponent.new(name: "check-circle", class_name: "h-4 w-4 text-primary") %>
<span>Minimal bundle sizes</span>
</li>
</ul>
<% end %>
<% c.with_footer do %>
<%= render(ButtonComponent.new(variant: "ghost", class_name: "w-full gap-1")) do %>
Learn more <%= render IconComponent.new(name: "arrow-right", class_name: "h-4 w-4") %>
<% end %>
<% end %>
<% end %>
<%= render CardComponent.new do |c| %>
<% c.with_header do %>
<%= render IconComponent.new(name: "code", class_name: "h-10 w-10 text-primary mb-2") %>
<h3 class="text-xl font-bold">Developer Experience</h3>
<p class="text-sm text-muted-foreground">
Built with developers in mind, providing a seamless development experience.
</p>
<% end %>
<% c.with_content do %>
<ul class="space-y-2 text-sm">
<li class="flex items-center gap-2">
<%= render IconComponent.new(name: "check-circle", class_name: "h-4 w-4 text-primary") %>
<span>Hot module replacement</span>
</li>
<li class="flex items-center gap-2">
<%= render IconComponent.new(name: "check-circle", class_name: "h-4 w-4 text-primary") %>
<span>Intuitive API design</span>
</li>
<li class="flex items-center gap-2">
<%= render IconComponent.new(name: "check-circle", class_name: "h-4 w-4 text-primary") %>
<span>Comprehensive documentation</span>
</li>
</ul>
<% end %>
<% c.with_footer do %>
<%= render(ButtonComponent.new(variant: "ghost", class_name: "w-full gap-1")) do %>
Learn more <%= render IconComponent.new(name: "arrow-right", class_name: "h-4 w-4") %>
<% end %>
<% end %>
<% end %>
<%= render CardComponent.new do |c| %>
<% c.with_header do %>
<%= render IconComponent.new(name: "globe", class_name: "h-10 w-10 text-primary mb-2") %>
<h3 class="text-xl font-bold">Global Scale</h3>
<p class="text-sm text-muted-foreground">Deploy your applications globally with just a few clicks.</p>
<% end %>
<% c.with_content do %>
<ul class="space-y-2 text-sm">
<li class="flex items-center gap-2">
<%= render IconComponent.new(name: "check-circle", class_name: "h-4 w-4 text-primary") %>
<span>Edge network deployment</span>
</li>
<li class="flex items-center gap-2">
<%= render IconComponent.new(name: "check-circle", class_name: "h-4 w-4 text-primary") %>
<span>Automatic scaling</span>
</li>
<li class="flex items-center gap-2">
<%= render IconComponent.new(name: "check-circle", class_name: "h-4 w-4 text-primary") %>
<span>Global CDN integration</span>
</li>
</ul>
<% end %>
<% c.with_footer do %>
<%= render(ButtonComponent.new(variant: "ghost", class_name: "w-full gap-1")) do %>
Learn more <%= render IconComponent.new(name: "arrow-right", class_name: "h-4 w-4") %>
<% end %>
<% end %>
<% end %>
</div>
</div>
</section>
<section class="py-20">
<div class="container px-4 md:px-6">
<div class="flex flex-col items-center justify-center gap-4 text-center">
<div class="space-y-2">
<h2 class="text-3xl font-bold tracking-tighter sm:text-4xl md:text-5xl">Ready to get started?</h2>
<p class="mx-auto max-w-[700px] text-muted-foreground md:text-xl">
Join thousands of developers building better applications with our platform.
</p>
</div>
<div class="flex flex-col gap-2 min-[400px]:flex-row">
<%= render(ButtonComponent.new(size: "lg", class_name: "gap-1")) do %>
Get Started <%= render IconComponent.new(name: "arrow-right", class_name: "h-4 w-4") %>
<% end %>
<%= render(ButtonComponent.new(size: "lg", variant: "outline")) do %>
Contact Sales
<% end %>
</div>
</div>
</div>
</section>
</main>
<footer class="border-t bg-muted/50">
<div class="container flex flex-col gap-6 py-8 md:flex-row md:items-center md:justify-between md:py-12">
<div class="flex items-center gap-2 font-bold">
<%= render IconComponent.new(name: "rocket", class_name: "h-5 w-5 text-primary") %>
<span>DevStack</span>
</div>
<nav class="flex gap-4 sm:gap-6">
<a href="#" class="text-xs hover:underline underline-offset-4">Terms</a>
<a href="#" class="text-xs hover:underline underline-offset-4">Privacy</a>
<a href="#" class="text-xs hover:underline underline-offset-4">Cookies</a>
</nav>
<div class="text-xs text-muted-foreground">
© <%= current_year %> DevStack, Inc. All rights reserved.
</div>
</div>
</footer>
</div>
```
## 4. Setting up the Rails Controller and View
```ruby file="app/controllers/pages_controller.rb"
...
# app/controllers/pages_controller.rb
class PagesController < ApplicationController
def home
end
end
```
```plaintext file="app/views/pages/home.html.erb"
...
<%= render LandingPageComponent.new %>
```
```ruby file="config/routes.rb"
...
Rails.application.routes.draw do
root to: "pages#home"
end
```
## 5. Configuring Tailwind CSS
Make sure your `tailwind.config.js` file includes the necessary configuration:
```javascript file="tailwind.config.js"
...
```
## 6. Importing Tailwind in your application.css
```css file="app/assets/stylesheets/application.css"
...
module.exports = {
content: ["./app/views/**/*.html.erb", "./app/helpers/**/*.rb", "./app/components/**/*.html.erb", "./app/components/**/*.rb", "./app/javascript/**/*.js", "*.{js,ts,jsx,tsx,mdx}"],
theme: {
extend: {
colors: {
primary: {
DEFAULT: 'hsl(220, 100%, 50%)',
foreground: 'hsl(0, 0%, 100%)',
},
muted: {
DEFAULT: 'hsl(220, 15%, 95%)',
foreground: 'hsl(220, 15%, 40%)',
},
accent: {
DEFAULT: 'hsl(220, 15%, 90%)',
foreground: 'hsl(220, 15%, 20%)',
},
card: {
DEFAULT: 'hsl(0, 0%, 100%)',
foreground: 'hsl(220, 15%, 20%)',
},
background: 'hsl(0, 0%, 100%)',
ring: 'hsl(220, 15%, 80%)',
input: 'hsl(220, 15%, 80%)',
},
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
},
},
plugins: [],
}
```
## Key Differences from React Implementation
1. **Component Structure**: Rails ViewComponents use Ruby classes with ERB templates instead of JSX.
2. **Slots Pattern**: ViewComponents use the `renders_one` and `renders_many` methods to define slots (similar to React's children props).
3. **Styling**: Tailwind CSS is used the same way, but you need to configure it specifically for Rails.
4. **Icons**: Instead of importing from a library like Lucide React, we've created an IconComponent that renders SVG based on the icon name.
5. **Rendering Components**: In Rails, you use `<%= render ComponentName.new(...) do %>` instead of JSX's `<ComponentName>`.
This implementation maintains the same visual design and structure as the React version but adapts it to Rails' component architecture.