Skip to content

Commit 18b006e

Browse files
committed
feat: Add /work section with story-driven project showcase
Implemented story-based work portfolio featuring: - Work model with slug-based routing - Markdown rendering for narrative project stories - Featured/published scopes for content management - Clean, minimal UI with NFT-inspired warm theme - Responsive layouts for index (list) and show (detail) pages - Added redcarpet gem for markdown processing - Database schema includes: title, slug, story, summary, category, status, technologies (json), dates, GitHub metadata - Routes: /work (index), /work/:slug (detail) First story: CORE - the Rails 8 monolith journey
1 parent 87810ca commit 18b006e

File tree

14 files changed

+358
-1
lines changed

14 files changed

+358
-1
lines changed

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ gem "thruster", require: false
4242
# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
4343
gem "image_processing", "~> 1.2"
4444

45+
# Markdown rendering for work stories
46+
gem "redcarpet"
47+
4548
group :development, :test do
4649
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
4750
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"

Gemfile.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ GEM
262262
erb
263263
psych (>= 4.0.0)
264264
tsort
265+
redcarpet (3.6.1)
265266
regexp_parser (2.11.3)
266267
reline (0.6.2)
267268
io-console (~> 0.5)
@@ -388,6 +389,7 @@ DEPENDENCIES
388389
propshaft
389390
puma (>= 5.0)
390391
rails (~> 8.1.1)
392+
redcarpet
391393
rubocop-rails-omakase
392394
selenium-webdriver
393395
solid_cable
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class WorksController < ApplicationController
2+
def index
3+
@works = Work.published.recent
4+
@featured_works = Work.featured.published.recent
5+
end
6+
7+
def show
8+
@work = Work.find_by!(slug: params[:id])
9+
end
10+
end

app/helpers/works_helper.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
module WorksHelper
2+
def markdown(text)
3+
return "" if text.blank?
4+
5+
require "redcarpet"
6+
7+
renderer = Redcarpet::Render::HTML.new(
8+
hard_wrap: true,
9+
link_attributes: { target: "_blank", rel: "noopener" }
10+
)
11+
12+
markdown = Redcarpet::Markdown.new(renderer,
13+
autolink: true,
14+
tables: true,
15+
fenced_code_blocks: true,
16+
strikethrough: true,
17+
superscript: true
18+
)
19+
20+
markdown.render(text).html_safe
21+
end
22+
end

app/models/work.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
class Work < ApplicationRecord
2+
# Validations
3+
validates :title, presence: true
4+
validates :slug, presence: true, uniqueness: true
5+
validates :story, presence: true
6+
7+
# Callbacks
8+
before_validation :generate_slug, if: -> { slug.blank? && title.present? }
9+
10+
# Scopes
11+
scope :featured, -> { where(featured: true) }
12+
scope :published, -> { where.not(status: "Draft") }
13+
scope :by_category, ->(category) { where(category: category) }
14+
scope :recent, -> { order(created_at: :desc) }
15+
16+
# Instance methods
17+
def to_param
18+
slug
19+
end
20+
21+
private
22+
23+
def generate_slug
24+
self.slug = title.parameterize
25+
end
26+
end

app/views/works/index.html.erb

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<div class="max-w-4xl mx-auto px-6 py-12">
2+
<div class="mb-12">
3+
<h1 class="text-4xl font-bold mb-4 text-[#3B2C22]">Work</h1>
4+
<p class="text-lg leading-relaxed text-[#3B2C22]/80">
5+
Projects I've built, problems I've solved, and stories worth telling.
6+
</p>
7+
</div>
8+
9+
<% if @featured_works.any? %>
10+
<div class="mb-16">
11+
<h2 class="text-2xl font-semibold mb-6 text-[#3B2C22]">Featured</h2>
12+
<div class="space-y-6">
13+
<% @featured_works.each do |work| %>
14+
<%= link_to work_path(work), class: "block group" do %>
15+
<div class="bg-white border-2 border-[#3B2C22]/10 rounded-lg p-6 hover:border-[#41CFFF] transition-colors">
16+
<div class="flex items-start justify-between mb-3">
17+
<h3 class="text-2xl font-semibold text-[#3B2C22] group-hover:text-[#41CFFF] transition-colors">
18+
<%= work.title %>
19+
</h3>
20+
<% if work.status == "Live" %>
21+
<span class="px-3 py-1 bg-[#A8E063] text-[#3B2C22] text-sm font-medium rounded">Live</span>
22+
<% end %>
23+
</div>
24+
<p class="text-[#3B2C22]/70 leading-relaxed mb-4">
25+
<%= work.summary %>
26+
</p>
27+
<div class="flex items-center gap-4 text-sm text-[#3B2C22]/60">
28+
<span><%= work.category %></span>
29+
<% if work.technologies.present? %>
30+
<span></span>
31+
<span><%= work.technologies.first(3).join(", ") %></span>
32+
<% end %>
33+
</div>
34+
</div>
35+
<% end %>
36+
<% end %>
37+
</div>
38+
</div>
39+
<% end %>
40+
41+
<% if @works.where.not(featured: true).any? %>
42+
<div>
43+
<h2 class="text-2xl font-semibold mb-6 text-[#3B2C22]">All Projects</h2>
44+
<div class="space-y-4">
45+
<% @works.where.not(featured: true).each do |work| %>
46+
<%= link_to work_path(work), class: "block group" do %>
47+
<div class="bg-white border-2 border-[#3B2C22]/10 rounded-lg p-5 hover:border-[#41CFFF] transition-colors">
48+
<div class="flex items-start justify-between mb-2">
49+
<h3 class="text-xl font-semibold text-[#3B2C22] group-hover:text-[#41CFFF] transition-colors">
50+
<%= work.title %>
51+
</h3>
52+
<% if work.status %>
53+
<span class="px-2 py-1 bg-[#3B2C22]/5 text-[#3B2C22] text-xs font-medium rounded">
54+
<%= work.status %>
55+
</span>
56+
<% end %>
57+
</div>
58+
<p class="text-[#3B2C22]/70 text-sm leading-relaxed">
59+
<%= work.summary %>
60+
</p>
61+
</div>
62+
<% end %>
63+
<% end %>
64+
</div>
65+
</div>
66+
<% end %>
67+
68+
<% if @works.empty? %>
69+
<div class="text-center py-12">
70+
<p class="text-[#3B2C22]/50">No work published yet. Check back soon!</p>
71+
</div>
72+
<% end %>
73+
</div>

app/views/works/show.html.erb

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<div class="max-w-3xl mx-auto px-6 py-12">
2+
<div class="mb-8">
3+
<%= link_to "← Back to Work", works_path, class: "text-[#41CFFF] hover:text-[#E58C2E] transition-colors" %>
4+
</div>
5+
6+
<article class="prose prose-lg max-w-none">
7+
<header class="mb-8 pb-8 border-b-2 border-[#3B2C22]/10">
8+
<h1 class="text-4xl font-bold mb-4 text-[#3B2C22]"><%= @work.title %></h1>
9+
10+
<div class="flex flex-wrap items-center gap-4 text-sm text-[#3B2C22]/60 mb-4">
11+
<span class="px-3 py-1 bg-[#A8E063] text-[#3B2C22] font-medium rounded"><%= @work.status %></span>
12+
<span><%= @work.category %></span>
13+
<% if @work.started_at %>
14+
<span></span>
15+
<span>Started <%= @work.started_at.strftime("%B %Y") %></span>
16+
<% end %>
17+
<% if @work.launched_at %>
18+
<span></span>
19+
<span>Launched <%= @work.launched_at.strftime("%B %Y") %></span>
20+
<% end %>
21+
</div>
22+
23+
<% if @work.technologies.present? %>
24+
<div class="flex flex-wrap gap-2 mb-4">
25+
<% @work.technologies.each do |tech| %>
26+
<span class="px-2 py-1 bg-[#3B2C22]/5 text-[#3B2C22] text-xs font-medium rounded">
27+
<%= tech %>
28+
</span>
29+
<% end %>
30+
</div>
31+
<% end %>
32+
33+
<div class="flex gap-4">
34+
<% if @work.github_url.present? %>
35+
<%= link_to @work.github_url, target: "_blank", rel: "noopener", class: "text-[#41CFFF] hover:text-[#E58C2E] transition-colors" do %>
36+
GitHub →
37+
<% end %>
38+
<% end %>
39+
<% if @work.live_url.present? %>
40+
<%= link_to @work.live_url, target: "_blank", rel: "noopener", class: "text-[#41CFFF] hover:text-[#E58C2E] transition-colors" do %>
41+
Live Site →
42+
<% end %>
43+
<% end %>
44+
</div>
45+
</header>
46+
47+
<div class="story-content text-[#3B2C22] leading-relaxed">
48+
<%= raw markdown(@work.story) %>
49+
</div>
50+
</article>
51+
</div>
52+
53+
<style>
54+
.story-content h1, .story-content h2, .story-content h3 {
55+
font-weight: 700;
56+
color: #3B2C22;
57+
margin-top: 2rem;
58+
margin-bottom: 1rem;
59+
}
60+
61+
.story-content h1 { font-size: 2rem; }
62+
.story-content h2 { font-size: 1.5rem; }
63+
.story-content h3 { font-size: 1.25rem; }
64+
65+
.story-content p {
66+
margin-bottom: 1.5rem;
67+
line-height: 1.875;
68+
}
69+
70+
.story-content a {
71+
color: #41CFFF;
72+
text-decoration: underline;
73+
}
74+
75+
.story-content a:hover {
76+
color: #E58C2E;
77+
}
78+
79+
.story-content ul, .story-content ol {
80+
margin-bottom: 1.5rem;
81+
padding-left: 2rem;
82+
}
83+
84+
.story-content li {
85+
margin-bottom: 0.5rem;
86+
}
87+
88+
.story-content code {
89+
background-color: #3B2C22;
90+
color: #FFF7E1;
91+
padding: 0.125rem 0.375rem;
92+
border-radius: 0.25rem;
93+
font-size: 0.875em;
94+
}
95+
96+
.story-content pre {
97+
background-color: #3B2C22;
98+
color: #FFF7E1;
99+
padding: 1rem;
100+
border-radius: 0.5rem;
101+
overflow-x: auto;
102+
margin-bottom: 1.5rem;
103+
}
104+
105+
.story-content pre code {
106+
background-color: transparent;
107+
padding: 0;
108+
}
109+
110+
.story-content blockquote {
111+
border-left: 4px solid #41CFFF;
112+
padding-left: 1rem;
113+
font-style: italic;
114+
color: #3B2C22;
115+
opacity: 0.8;
116+
margin-bottom: 1.5rem;
117+
}
118+
</style>

config/routes.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
# RECTOR LABS - Homepage
33
root "pages#home"
44

5+
# Work section - story-driven project showcase
6+
resources :works, path: "work", only: [ :index, :show ]
7+
58
# Health check
69
get "up" => "rails/health#show", as: :rails_health_check
710

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
class CreateWorks < ActiveRecord::Migration[8.1]
2+
def change
3+
create_table :works do |t|
4+
t.string :title
5+
t.string :slug
6+
t.string :github_url
7+
t.string :live_url
8+
t.string :repo_name
9+
t.text :story
10+
t.text :summary
11+
t.string :category
12+
t.string :status
13+
t.date :started_at
14+
t.date :launched_at
15+
t.boolean :featured
16+
t.integer :github_stars
17+
t.integer :github_forks
18+
19+
t.timestamps
20+
end
21+
end
22+
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class AddTechnologiesToWorks < ActiveRecord::Migration[8.1]
2+
def change
3+
add_column :works, :technologies, :json
4+
end
5+
end

0 commit comments

Comments
 (0)