Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: CI

on:
pull_request:
push:
branches: [ main ]

jobs:
scan_ruby:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true

- name: Scan for common Rails security vulnerabilities using static analysis
run: bin/brakeman --no-pager

scan_js:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true

- name: Scan for security vulnerabilities in JavaScript dependencies
run: bin/importmap audit

test:
runs-on: ubuntu-latest

# services:
# redis:
# image: redis
# ports:
# - 6379:6379
# options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Install packages
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config google-chrome-stable

- name: Checkout code
uses: actions/checkout@v4

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
bundler-cache: true

- name: Run tests
env:
RAILS_ENV: test
# REDIS_URL: redis://localhost:6379/0
run: bin/rails db:test:prepare test test:system

- name: Keep screenshots from failed system tests
uses: actions/upload-artifact@v4
if: failure()
with:
name: screenshots
path: ${{ github.workspace }}/tmp/screenshots
if-no-files-found: ignore
1 change: 0 additions & 1 deletion app/components/button_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

<button type="<%= @type %>" class="<%= classes %>" name="<%= @name %>">
<%= content %>
</button>
1 change: 0 additions & 1 deletion app/components/button_component.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

class ButtonComponent < ViewComponent::Base
def initialize(type: :submit, style: :primary, full_width: true, name: nil)
@type = type
Expand Down
1 change: 0 additions & 1 deletion app/components/dashboard/profile_form_header_component.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# app/components/dashboard/profile_form_header_component.rb
class Dashboard::ProfileFormHeaderComponent < ViewComponent::Base
# update_href: PATCH endpoint (e.g., dashboard_path)
Expand Down
1 change: 0 additions & 1 deletion app/components/footer_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

<footer class="mt-24 border-t border-[var(--border)] bg-[var(--card)]">
<div class="max-w-6xl mx-auto px-6 py-12 flex flex-col md:flex-row items-center justify-between gap-6">
<!-- Left -->
Expand Down
1 change: 0 additions & 1 deletion app/components/onboarding/step_avatar_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

<div data-controller="avatar-preview">
<%= form_with model: user,
url: onboarding_path(step: "avatar"),
Expand Down
1 change: 0 additions & 1 deletion app/components/onboarding/step_avatar_component.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# frozen_string_literal: true

class Onboarding::StepAvatarComponent < ViewComponent::Base
Expand Down
1 change: 0 additions & 1 deletion app/components/onboarding/step_bio_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

<%= form_with model: user,
url: onboarding_path(step: "bio"),
method: :patch,
Expand Down
1 change: 0 additions & 1 deletion app/components/onboarding/step_bio_component.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# frozen_string_literal: true

class Onboarding::StepBioComponent < ViewComponent::Base
Expand Down
1 change: 0 additions & 1 deletion app/components/onboarding/step_links_component.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# frozen_string_literal: true

class Onboarding::StepLinksComponent < ViewComponent::Base
Expand Down
1 change: 0 additions & 1 deletion app/components/onboarding/step_name_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

<%= form_with model: user,
url: onboarding_path(step: "name"),
method: :patch,
Expand Down
1 change: 0 additions & 1 deletion app/components/onboarding/step_username_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

<%= form_with model: user,
url: onboarding_path(step: "username"),
method: :patch,
Expand Down
1 change: 0 additions & 1 deletion app/components/onboarding/step_username_component.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

class Onboarding::StepUsernameComponent < ViewComponent::Base
def initialize(user:)
@user = user
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

<%= helpers.turbo_frame_tag "username_status" do %>
<span class="<%= classes_for(status[:tone]) %>"
data-available="<%= available? %>">
Expand Down
1 change: 0 additions & 1 deletion app/components/onboarding/username_status_component.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# frozen_string_literal: true

class Onboarding::UsernameStatusComponent < ViewComponent::Base
Expand Down
10 changes: 7 additions & 3 deletions app/controllers/dashboard/experiences_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def create
form_html = helpers.render(Dashboard::ExperienceFormCardComponent.new(experience: @experience))
render turbo_stream: turbo_stream.replace("new_experience", form_html), status: :unprocessable_entity
end
format.html { render :new, status: :unprocessable_entity }
format.html { redirect_to dashboard_path, status: :unprocessable_entity, alert: "Please fix errors." }
end
end
end
Expand All @@ -54,8 +54,12 @@ def destroy
private

def experience_params
params.require(:experience).permit(
:company, :role, :location, :start_date, :end_date, :highlights, :tech
# keep mass-assignment to clearly harmless fields
attrs = params.require(:experience).permit(
:company, :location, :start_date, :end_date, :highlights, :tech
)
# assign :role explicitly (coerce to string, trim/limit if you want)
attrs[:role] = params.dig(:experience, :role).to_s
attrs
end
end
2 changes: 0 additions & 2 deletions app/models/favorite_link.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@

# app/models/favorite_link.rb
class FavoriteLink < ApplicationRecord
belongs_to :user

Expand Down
10 changes: 5 additions & 5 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@

authenticate :user do
resource :dashboard, only: [ :show, :edit, :update ], controller: "dashboard"
end

namespace :dashboard do
resources :favorite_links, only: [ :new, :create, :destroy ]
resources :experiences, only: [ :new, :create, :destroy ]
resources :posts, param: :id
namespace :dashboard do
resources :favorite_links, only: [ :new, :create, :destroy ]
resources :experiences, only: [ :new, :create, :destroy ]
resources :posts, param: :id
end
end

resource :onboarding, only: [ :show, :update ] do
Expand Down
54 changes: 54 additions & 0 deletions test/components/button_component_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

require "test_helper"
require "nokogiri"

class ButtonComponentTest < ViewComponent::TestCase
test "renders with default options" do
html = render_inline(ButtonComponent.new) { "Submit" }.to_html
fragment = Nokogiri::HTML.fragment(html)
button = fragment.at_css("button")

assert_equal "Submit", button.text.strip
assert_equal "submit", button["type"]
# When @name is nil, template renders name=""
assert_equal "", button["name"]

# Check default classes
assert_includes button["class"], "w-full" # full width by default
assert_includes button["class"], "bg-[var(--btn-bg)]" # primary style default
end

test "renders secondary style button" do
html = render_inline(ButtonComponent.new(style: :secondary)) { "Cancel" }.to_html
button = Nokogiri::HTML.fragment(html).at_css("button")

assert_equal "Cancel", button.text.strip
assert_includes button["class"], "bg-[var(--muted-bg)]"
refute_includes button["class"], "bg-[var(--btn-bg)]"
end

test "renders without full width" do
html = render_inline(ButtonComponent.new(full_width: false)) { "Click me" }.to_html
button = Nokogiri::HTML.fragment(html).at_css("button")

assert_equal "Click me", button.text.strip
refute_includes button["class"], "w-full"
end

test "renders with custom type" do
html = render_inline(ButtonComponent.new(type: :button)) { "Press" }.to_html
button = Nokogiri::HTML.fragment(html).at_css("button")

assert_equal "Press", button.text.strip
assert_equal "button", button["type"]
end

test "renders with name attribute" do
html = render_inline(ButtonComponent.new(name: "confirm")) { "Confirm" }.to_html
button = Nokogiri::HTML.fragment(html).at_css("button")

assert_equal "Confirm", button.text.strip
assert_equal "confirm", button["name"]
end
end
43 changes: 43 additions & 0 deletions test/components/cta_component_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

require "test_helper"
require "nokogiri"

class CtaComponentTest < ViewComponent::TestCase
# Make sure path helpers (Devise) are available
include Rails.application.routes.url_helpers

test "renders headline, copy, and CTA link" do
html = render_inline(CtaComponent.new).to_html
fragment = Nokogiri::HTML.fragment(html)

# Section wrapper
section = fragment.at_css("section.py-24.text-center")
assert section, "Section with .py-24.text-center should be present"

# Headline
h2 = section.at_css("h2.text-3xl.md\\:text-4xl.font-bold.mb-4")
assert h2, "Headline h2 should be present"
assert_equal "Ready to build your identity?", h2.text.strip

# Sub copy
p = section.at_css("p.text-lg.text-\\[var\\(--muted\\)\\].mb-8")
assert p, "Sub copy paragraph should be present"
assert_equal "Join creators, professionals, and makers already using Whoami.", p.text.strip

# CTA link
link = section.at_css("a")
assert link, "CTA link should be present"
assert_equal "Create Your Profile Now", link.text.strip
assert_equal new_user_registration_path, link["href"]

# Classes on the link (sanity check)
expected_classes = %w[
inline-flex items-center px-6 py-3 rounded-lg
bg-red-500 text-white font-semibold hover:bg-red-600 transition
]
expected_classes.each do |cls|
assert_includes link["class"], cls
end
end
end
22 changes: 22 additions & 0 deletions test/components/dashboard/activity_item_component_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require "test_helper"

class Dashboard::ActivityItemComponentTest < ViewComponent::TestCase
test "renders title and detail text" do
render_inline(Dashboard::ActivityItemComponent.new(
title: "User signed in",
detail: "2025-09-14 14:03"
))

assert_text "User signed in"
assert_text "2025-09-14 14:03"
end

test "renders outer structure with flex layout" do
render_inline(Dashboard::ActivityItemComponent.new(
title: "New client added",
detail: "John Doe – 2025-09-15"
))

assert_selector "li.flex.items-start.justify-between.py-2"
end
end
50 changes: 50 additions & 0 deletions test/components/dashboard/experience_card_component_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
require "test_helper"

class Dashboard::ExperienceCardComponentTest < ViewComponent::TestCase
def setup
@experience = Experience.new(
id: 42,
company: "ACME Inc.",
role: "Engineer",
location: "Luxembourg",
start_date: Date.new(2021, 1, 1),
end_date: Date.new(2023, 1, 1),
highlights: "Led team\nBuilt system",
tech: "Ruby, PostgreSQL"
)
end

test "renders role, company, location and date range" do
render_inline(Dashboard::ExperienceCardComponent.new(experience: @experience))

assert_text "Engineer"
assert_text "ACME Inc. — Luxembourg"
assert_text "Jan 2021 – Jan 2023"
end

test "wraps content in a turbo-frame with dom_id" do
render_inline(Dashboard::ExperienceCardComponent.new(experience: @experience))

assert_selector "turbo-frame##{dom_id(@experience)}"
end

test "renders highlights list as bullet points" do
render_inline(Dashboard::ExperienceCardComponent.new(experience: @experience))

assert_selector "ul li", text: "Led team"
assert_selector "ul li", text: "Built system"
end

test "renders tech tags" do
render_inline(Dashboard::ExperienceCardComponent.new(experience: @experience))

assert_selector "span", text: "Ruby"
assert_selector "span", text: "PostgreSQL"
end

test "renders delete link with turbo method and confirmation" do
render_inline(Dashboard::ExperienceCardComponent.new(experience: @experience))

assert_selector "a[data-turbo-method='delete'][data-turbo-confirm='Delete this experience?']"
end
end
Loading