1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-02 20:15:22 +02:00

User Onboarding + Bug Fixes (#1352)

* Bump min supported date to 20 years

* Add basic onboarding

* User onboarding

* Complete onboarding flow

* Cleanup, add user profile update test
This commit is contained in:
Zach Gollwitzer 2024-10-23 11:20:55 -04:00 committed by GitHub
parent 73e184ad3d
commit 1d20de770f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 1088 additions and 300 deletions

View file

@ -0,0 +1,7 @@
<footer class="p-6">
<div class="space-y-2 text-center text-xs text-gray-500">
<p>&copy; <%= Date.current.year %>, Maybe Finance, Inc.</p>
<p><%= link_to t(".privacy_policy"), "https://maybe.co/privacy", class: "underline hover:text-gray-600" %> &bull; <%= link_to t(".terms_of_service"), "https://maybe.co/tos", class: "underline hover:text-gray-600" %></p>
</div>
</footer>

View file

@ -4,24 +4,16 @@
<% end %>
<div id="user-menu" data-controller="menu">
<button data-menu-target="button">
<% profile_image_attached = Current.user.profile_image.attached? %>
<% if profile_image_attached %>
<div class="text-white w-9 h-9">
<%= render "shared/user_profile_image", user: Current.user %>
</div>
<% else %>
<div class="text-white w-9 h-9 bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= Current.user.initial %></div>
<% end %>
<div class="w-9 h-9">
<%= render "settings/user_avatar", user: Current.user %>
</div>
</button>
<div data-menu-target="content" class="hidden absolute w-[240px] z-10 left-[255px] top-[72px] bg-white rounded-sm shadow-xs border border-alpha-black-25">
<div class="p-3 flex items-center gap-3">
<% if profile_image_attached %>
<div class="text-white shrink-0 w-9 h-9">
<%= render "shared/user_profile_image", user: Current.user %>
</div>
<% else %>
<div class="text-white shrink-0 w-9 h-9 bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= Current.user.initial %></div>
<% end %>
<div class="w-9 h-9 shrink-0">
<%= render "settings/user_avatar", user: Current.user %>
</div>
<div class="overflow-hidden text-ellipsis">
<span class="text-gray-900 font-medium text-sm"><%= Current.user.display_name %></span>
<% if Current.user.display_name != Current.user.email %>

View file

@ -44,7 +44,7 @@
<%= render "shared/confirm_modal" %>
<% if self_hosted? %>
<% if self_hosted? && Current.user&.onboarded_at.present? %>
<%= render "shared/app_version" %>
<% end %>
</body>

View file

@ -1,30 +1,34 @@
<%= content_for :content do %>
<div class="flex flex-col justify-center min-h-full px-6 py-12">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<%= render "shared/logo" %>
<div class="flex flex-col h-screen px-6 py-12 bg-gray-25">
<div class="grow flex flex-col justify-center">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<div class="flex justify-center mb-6">
<%= image_tag "logo-color.png", class: "w-16 mb-6" %>
</div>
<h2 class="mt-6 text-3xl font-semibold tracking-tight text-center font-display">
<%= content_for?(:header_title) ? yield(:header_title).html_safe : t(".your_account") %>
</h2>
<div class="space-y-2">
<h2 class="text-3xl font-medium text-gray-900 text-center">
<%= content_for?(:header_title) ? yield(:header_title).html_safe : t(".your_account") %>
</h2>
<% if controller_name == "sessions" %>
<p class="mt-2 text-sm text-center text-gray-600">
<%= t(".or") %> <%= link_to t(".sign_up"), new_registration_path, class: "font-medium text-gray-600 hover:text-gray-400 transition" %>
</p>
<% elsif controller_name == "registrations" %>
<p class="mt-2 text-sm text-center text-gray-600">
<%= t(".or") %> <%= link_to t(".sign_in"), new_session_path, class: "font-medium text-gray-600 hover:text-gray-400 transition" %>
</p>
<% end %>
<% if controller_name == "sessions" %>
<p class="text-sm text-center">
<%= tag.span t(".no_account"), class: "text-gray-500" %> <%= link_to t(".sign_up"), new_registration_path, class: "font-medium text-gray-900 hover:underline transition" %>
</p>
<% elsif controller_name == "registrations" %>
<p class="text-sm text-center text-gray-600">
<%= t(".existing_account") %> <%= link_to t(".sign_in"), new_session_path, class: "font-medium text-gray-900 hover:underline transition" %>
</p>
<% end %>
</div>
</div>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-lg">
<%= yield %>
</div>
</div>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-lg">
<%= yield %>
</div>
<div class="p-8 mt-2 text-center">
<p class="mt-6 text-sm text-black"><%= link_to t(".privacy_policy"), "https://maybe.co/privacy", class: "font-medium text-gray-600 hover:text-gray-400 transition" %> &bull; <%= link_to t(".terms_of_service"), "https://maybe.co/tos", class: "font-medium text-gray-600 hover:text-gray-400 transition" %></p>
</div>
<%= render "layouts/footer" %>
</div>
<% end %>

View file

@ -0,0 +1,8 @@
<header class="flex justify-between items-center p-4">
<%= image_tag "logo.svg", class: "h-[22px]" %>
<div class="flex items-center gap-2">
<%= lucide_icon "log-in", class: "w-5 h-5 shrink-0 text-gray-500 gap-2" %>
<%= button_to t(".sign_out"), session_path(Current.session), method: :delete, class: "text-sm text-gray-900 font-medium" %>
</div>
</header>

View file

@ -0,0 +1,88 @@
<div class="bg-gray-25 h-screen flex flex-col justify-between">
<%= render "onboardings/header" %>
<div class="grow max-w-lg w-full mx-auto bg-gray-25 flex flex-col justify-center" data-controller="onboarding">
<div>
<div class="space-y-1 mb-6">
<h1 class="text-2xl font-medium"><%= t(".title") %></h1>
<p class="text-gray-500 text-sm"><%= t(".subtitle") %></p>
</div>
<div class="p-1 mb-2">
<div class="bg-white p-4 rounded-lg flex gap-8" style="box-shadow: 0px 0px 0px 1px rgba(11, 11, 11, 0.05), 0px 1px 2px 0px rgba(11, 11, 11, 0.05);">
<div class="space-y-2">
<%= tag.p t(".example"), class: "text-gray-500 text-sm" %>
<%= tag.p "$2,323.25", class: "text-gray-900 font-medium text-2xl" %>
<p class="text-sm">
<span class="text-green-500 font-medium">+<%= format_money(Money.new(78.90, params[:currency] || @user.family.currency)) %></span>
<span class="text-green-500 font-medium">(+<%= format_money(Money.new(6.39, params[:currency] || @user.family.currency)) %>)</span>
<span class="text-gray-500">as of <%= format_date(Date.parse("2024-10-23"), :default, format_code: params[:date_format] || @user.family.date_format) %></span>
</p>
</div>
<% placeholder_series_data = [
{ date: Date.current - 14.days, value: 200 },
{ date: Date.current - 13.days, value: 200 },
{ date: Date.current - 12.days, value: 220 },
{ date: Date.current - 11.days, value: 220 },
{ date: Date.current - 10.days, value: 220 },
{ date: Date.current - 9.days, value: 220 },
{ date: Date.current - 8.days, value: 220 },
{ date: Date.current - 7.days, value: 220 },
{ date: Date.current - 6.days, value: 230 },
{ date: Date.current - 5.days, value: 230 },
{ date: Date.current - 4.days, value: 250 },
{ date: Date.current - 3.days, value: 250 },
{ date: Date.current - 2.days, value: 265 },
{ date: Date.current - 1.day, value: 265 },
{ date: Date.current, value: 265 }
] %>
<div class="flex items-center w-2/5">
<div class="h-12 w-full">
<div
id="previewChart"
class="h-full w-full"
data-controller="time-series-chart"
data-time-series-chart-data-value="<%= TimeSeries.new(placeholder_series_data).to_json %>"
data-time-series-chart-use-labels-value="false"
data-time-series-chart-use-tooltip-value="false"></div>
</div>
</div>
</div>
</div>
<p class="text-gray-500 text-xs mb-4"><%= t(".preview") %></p>
<%= styled_form_with model: @user do |form| %>
<%= form.hidden_field :onboarded_at, value: Time.current %>
<%= form.hidden_field :redirect_to, value: "home" %>
<div class="space-y-4 mb-4">
<%= form.fields_for :family do |family_form| %>
<%= family_form.select :locale,
language_options,
{ label: t(".locale"), required: true, selected: params[:locale] || @user.family.locale },
{ data: { action: "onboarding#setLocale" } } %>
<%= family_form.select :currency,
currencies_for_select.map { |currency| [ "#{currency.name} (#{currency.iso_code})", currency.iso_code ] },
{ label: t(".currency"), required: true, selected: params[:currency] || @user.family.currency },
{ data: { action: "onboarding#setCurrency" } } %>
<%= family_form.select :date_format,
date_format_options,
{ label: t(".date_format"), required: true, selected: params[:date_format] || @user.family.date_format },
{ data: { action: "onboarding#setDateFormat" } } %>
<% end %>
</div>
<%= form.submit t(".submit") %>
<% end %>
</div>
</div>
<%= render "layouts/footer" %>
</div>

View file

@ -0,0 +1,40 @@
<div class="bg-gray-25 h-screen flex flex-col justify-between">
<%= render "onboardings/header" %>
<div class="grow max-w-lg w-full mx-auto bg-gray-25 flex flex-col justify-center">
<div>
<div class="space-y-1 mb-6">
<h1 class="text-2xl font-medium"><%= t(".title") %></h1>
<p class="text-gray-500 text-sm"><%= t(".subtitle") %></p>
</div>
<%= styled_form_with model: @user do |form| %>
<%= form.hidden_field :redirect_to, value: "onboarding_preferences" %>
<div class="space-y-4 mb-4">
<p class="text-gray-500 text-xs"><%= t(".profile_image") %></p>
<%= render "settings/user_avatar_field", form: form, user: @user %>
</div>
<div class="flex justify-between items-center gap-4 mb-4">
<%= form.text_field :first_name, placeholder: t(".first_name"), label: t(".first_name"), container_class: "bg-white w-1/2", required: true %>
<%= form.text_field :last_name, placeholder: t(".last_name"), label: t(".last_name"), container_class: "bg-white w-1/2", required: true %>
</div>
<div class="space-y-4 mb-4">
<%= form.fields_for :family do |family_form| %>
<%= family_form.text_field :name, placeholder: t(".household_name"), label: t(".household_name") %>
<%= family_form.select :country,
country_options,
{ label: t(".country") }, required: true %>
<% end %>
</div>
<%= form.submit t(".submit") %>
<% end %>
</div>
</div>
<%= render "layouts/footer" %>
</div>

View file

@ -0,0 +1,11 @@
<div class="bg-gray-25">
<div class="h-screen flex flex-col items-center py-6">
<div class="grow flex justify-center items-center flex-col max-w-sm w-full text-center">
<%= image_tag "logo-color.png", class: "w-16 mb-6" %>
<%= tag.h1 t(".title"), class: "text-3xl font-medium mb-2" %>
<%= tag.p t(".message"), class: "text-sm text-gray-500 mb-6" %>
<%= link_to t(".setup"), profile_onboarding_path, class: "block flex justify-center items-center btn btn--primary w-full" %>
</div>
</div>
</div>

View file

@ -16,5 +16,5 @@
<% if invite_code_required? %>
<%= form.text_field :invite_code, required: "required", label: true, value: params[:invite] %>
<% end %>
<%= form.submit %>
<%= form.submit t(".submit") %>
<% end %>

View file

@ -11,5 +11,5 @@
<% end %>
<div class="mt-6 text-center">
<p class="text-sm text-gray-600"><%= t(".forgot_password") %> <%= link_to t(".reset_password"), new_password_reset_path, class: "font-medium text-gray-600 hover:text-gray-400 transition" %></p>
<%= link_to t(".forgot_password"), new_password_reset_path, class: "font-medium text-sm text-gray-900 hover:underline transition" %>
</div>

View file

@ -0,0 +1,7 @@
<%# locals: (user:) %>
<% if user.profile_image.attached? %>
<%= image_tag user.profile_image.variant(:thumbnail), class: "rounded-full w-full h-full object-cover" %>
<% else %>
<div class="text-white w-full h-full bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= user.initial %></div>
<% end %>

View file

@ -0,0 +1,52 @@
<%# locals: (form:, user:) %>
<div class="flex items-center gap-4" data-controller="profile-image-preview">
<div class="relative flex justify-center items-center bg-gray-50 w-24 h-24 rounded-full border-alpha-black-300 border border-dashed">
<%# The image preview once user has uploaded a new file %>
<div data-profile-image-preview-target="previewImage" class="h-full w-full flex justify-center items-center hidden">
<img src="" alt="Preview" class="w-full h-full rounded-full object-cover">
</div>
<%# The placeholder image for empty avatar field %>
<div data-profile-image-preview-target="placeholderImage"
class="h-full w-full flex justify-center items-center <%= user.profile_image.attached? ? "hidden" : "" %>">
<div class="h-full w-full flex justify-center items-center">
<%= lucide_icon "image-plus", class: "w-6 h-6 text-gray-500" %>
</div>
</div>
<%# The attached image if user has already uploaded one %>
<div data-profile-image-preview-target="attachedImage"
class="h-full w-full flex justify-center items-center <%= user.profile_image.attached? ? "" : "hidden" %>">
<% if user.profile_image.attached? %>
<div class="h-full w-full">
<%= render "settings/user_avatar", user: user %>
</div>
<% end %>
</div>
<button type="button"
data-profile-image-preview-target="clearBtn"
data-action="click->profile-image-preview#clearFileInput"
class="<%= user.profile_image.attached? ? "" : "hidden" %> cursor-pointer absolute bottom-0 right-0 w-8 h-8 bg-gray-50 rounded-full flex justify-center items-center border border-white border-2">
<%= lucide_icon "x", class: "w-4 h-4 text-gray-500" %>
</button>
</div>
<div>
<%= form.hidden_field :delete_profile_image, value: "0", data: { profile_image_preview_target: "deleteProfileImage" } %>
<p class="mb-3"><%= t(".accepted_formats") %></p>
<%= form.label :profile_image, t(".choose"),
class: "btn btn--outline inline-block" %>
<%= form.file_field :profile_image,
accept: "image/png, image/jpeg",
class: "hidden px-3 py-2 bg-gray-50 text-gray-900 rounded-md text-sm font-medium",
data: {
profile_image_preview_target: "input",
action: "change->profile-image-preview#showFileInputPreview"
} %>
</div>
</div>

View file

@ -5,7 +5,7 @@
<div class="space-y-4">
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
<%= settings_section title: t(".subscription_title"), subtitle: t(".subscription_subtitle") do %>
<% if Current.family.stripe_plan_id.blank? %>
<% if @user.family.stripe_plan_id.blank? %>
<%= link_to t(".subscribe_button"), new_subscription_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2", data: { turbo: false } %>
<% else %>
<%= link_to t(".manage_subscription_button"), subscription_path, class: "w-fit flex text-white text-sm font-medium items-center gap-1 bg-gray-900 rounded-lg p-2", data: { turbo: false } %>

View file

@ -6,16 +6,28 @@
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
<%= settings_section title: t(".general_title"), subtitle: t(".general_subtitle") do %>
<div>
<%= styled_form_with model: Current.user, url: settings_preferences_path, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %>
<%= form.fields_for :family_attributes do |family_form| %>
<%= styled_form_with model: @user, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %>
<%= form.hidden_field :redirect_to, value: "preferences" %>
<%= form.fields_for :family do |family_form| %>
<%= family_form.select :currency,
currencies_for_select.map { |currency| [ "#{currency.name} (#{currency.iso_code})", currency.iso_code ] },
{ label: "Currency", selected: Current.family.currency },
{ label: t(".currency") },
{ data: { auto_submit_form_target: "auto" } } %>
<%= family_form.select :locale,
I18n.available_locales,
{ label: "Locale", selected: Current.family.locale },
language_options,
{ label: t(".language") },
{ data: { auto_submit_form_target: "auto" } } %>
<%= family_form.select :date_format,
date_format_options,
{ label: t(".date_format") },
{ data: { auto_submit_form_target: "auto" } } %>
<%= family_form.select :country,
country_options,
{ label: t(".country") },
{ data: { auto_submit_form_target: "auto" } } %>
<p class="text-xs italic pl-2 text-gray-500">Please note, we are still working on translations for various languages. Please see the <%= link_to "I18n issue", "https://github.com/maybe-finance/maybe/issues/1225", target: "_blank", class: "underline" %> for more information.</p>
@ -25,7 +37,8 @@
<% end %>
<%= settings_section title: t(".theme_title"), subtitle: t(".theme_subtitle") do %>
<div>
<%= styled_form_with model: Current.user, url: settings_preferences_path, local: true, class: "flex justify-between items-center" do |form| %>
<%= styled_form_with model: @user, class: "flex justify-between items-center" do |form| %>
<%= form.hidden_field :redirect_to, value: "preferences" %>
<div class="text-center">
<%= image_tag("light-mode-preview.png", alt: "Light Theme Preview", class: "h-44 mb-4") %>
<div class="flex justify-center items-center gap-2">

View file

@ -5,35 +5,13 @@
<h1 class="text-gray-900 text-xl font-medium mb-4"><%= t(".page_title") %></h1>
<div class="space-y-4">
<%= settings_section title: t(".profile_title"), subtitle: t(".profile_subtitle") do %>
<%= styled_form_with model: Current.user, url: settings_profile_path, class: "space-y-4", data: { controller: "profile-image-preview" } do |form| %>
<div class="flex items-center gap-4">
<div class="relative flex justify-center items-center bg-gray-50 w-24 h-24 rounded-full border border-alpha-black-25">
<div data-profile-image-preview-target="imagePreview" class="h-full w-full flex justify-center items-center">
<% profile_image_attached = Current.user.profile_image.attached? %>
<% if profile_image_attached %>
<div class="h-24 w-24">
<%= render "shared/user_profile_image", user: Current.user %>
</div>
<% else %>
<%= lucide_icon "image-plus", class: "w-6 h-6 text-gray-500" %>
<% end %>
</div>
<%= lucide_icon "image-plus", class: "hidden w-6 h-6 text-gray-500", data: { profile_image_preview_target: "template" } %>
<div data-profile-image-preview-target="clearBtn" data-action="click->profile-image-preview#clear" class="<%= profile_image_attached ? "" : "hidden" %> cursor-pointer absolute bottom-0 right-0 w-8 h-8 bg-gray-50 rounded-full flex justify-center items-center border border-white border-2">
<%= lucide_icon "x", class: "w-4 h-4 text-gray-500" %>
</div>
</div>
<div class="space-y-3">
<p><%= t(".profile_image_type") %></p>
<%= form.label :profile_image, t(".profile_image_choose"), class: "inline-block cursor-pointer px-3 py-2 bg-gray-50 text-gray-900 rounded-md text-sm font-medium" %>
<%= form.file_field :profile_image, accept: "image/png, image/jpeg", class: "hidden px-3 py-2 bg-gray-50 text-gray-900 rounded-md text-sm font-medium", data: {profile_image_preview_target: "fileField", action: "change->profile-image-preview#preview"} %>
<%= form.hidden_field :delete_profile_image, value: false, data: {profile_image_preview_target: "deleteField"} %>
</div>
</div>
<%= styled_form_with model: @user, class: "space-y-4" do |form| %>
<%= render "settings/user_avatar_field", form: form, user: @user %>
<div>
<div class="grid grid-cols-2 gap-4 mt-4">
<%= form.text_field :first_name, placeholder: "First name", value: Current.user.first_name, label: true %>
<%= form.text_field :last_name, placeholder: "Last name", value: Current.user.last_name, label: true %>
<%= form.text_field :first_name, placeholder: t(".first_name"), label: t(".first_name") %>
<%= form.text_field :last_name, placeholder: t(".last_name"), label: t(".last_name") %>
</div>
<div class="flex justify-end mt-4">
<%= form.submit t(".save"), class: "bg-gray-900 hover:bg-gray-700 cursor-pointer text-white rounded-lg px-3 py-2" %>
@ -43,9 +21,13 @@
<% end %>
<%= settings_section title: t(".household_title"), subtitle: t(".household_subtitle") do %>
<div class="space-y-4">
<%= styled_form_with model: Current.user, url: settings_profile_path, class: "space-y-4", data: { controller: "auto-submit-form", "auto-submit-form-trigger-event-value": "blur" } do |form| %>
<%= form.fields_for :family_attributes do |family_fields| %>
<%= family_fields.text_field :name, placeholder: t(".household_form_input_placeholder"), value: Current.family.name, label: t(".household_form_label"), disabled: !Current.user.admin?, "data-auto-submit-form-target": "auto" %>
<%= styled_form_with model: Current.user, class: "space-y-4", data: { controller: "auto-submit-form" } do |form| %>
<%= form.fields_for :family do |family_fields| %>
<%= family_fields.text_field :name,
placeholder: t(".household_form_input_placeholder"),
label: t(".household_form_label"),
disabled: !Current.user.admin?,
"data-auto-submit-form-target": "auto" %>
<% end %>
<% end %>
<div class="bg-gray-25 rounded-xl p-1">
@ -71,7 +53,7 @@
<p class="text-gray-500 text-sm"><%= t(".delete_account_warning") %></p>
</div>
<%=
button_to t(".delete_account"), settings_profile_path, method: :delete,
button_to t(".delete_account"), user_path(@user), method: :delete,
class: "bg-red-500 text-white text-sm font-medium rounded-lg px-3 py-2",
data: { turbo_confirm: {
title: t(".confirm_delete.title"),

View file

@ -6,7 +6,12 @@
currency = Money::Currency.new(currency_value || options[:default_currency] || "USD") %>
<div class="form-field pr-0 <%= options[:container_class] %>" data-controller="money-field">
<%= form.label options[:label] || t(".label"), class: "form-field__label" %>
<%= form.label options[:label] || t(".label"), class: "form-field__label" do %>
<%= options[:label] || t(".label") %>
<% if options[:required] %>
<span class="text-red-500">*</span>
<% end %>
<% end %>
<div class="flex items-center gap-1">
<div class="flex items-center grow gap-1">

View file

@ -32,7 +32,7 @@
<div class="h-5 shrink-0">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" class="shrink-0">
<path d="M18 10C18 14.4183 14.4183 18 10 18C5.58172 18 2 14.4183 2 10C2 5.58172 5.58172 2 10 2C14.4183 2 18 5.58172 18 10ZM3.6 10C3.6 13.5346 6.46538 16.4 10 16.4C13.5346 16.4 16.4 13.5346 16.4 10C16.4 6.46538 13.5346 3.6 10 3.6C6.46538 3.6 3.6 6.46538 3.6 10Z" fill="#E5E5E5" />
<circle class="origin-center -rotate-90 animate-[stroke-fill_5s_300ms_forwards]" stroke="#141414" stroke-opacity="0.4" r="7.2" cx="10" cy="10" stroke-dasharray="43.9822971503" stroke-dashoffset="43.9822971503" />
<circle class="origin-center -rotate-90 animate-[stroke-fill_2.2s_300ms_forwards]" stroke="#141414" stroke-opacity="0.4" r="7.2" cx="10" cy="10" stroke-dasharray="43.9822971503" stroke-dashoffset="43.9822971503" />
</svg>
<div class="absolute -top-2 -right-2">
<%= lucide_icon "x", class: "w-5 h-5 p-0.5 hidden group-hover:inline-block border border-alpha-black-50 border-solid rounded-lg bg-white text-gray-400 cursor-pointer", data: { action: "click->element-removal#remove" } %>

View file

@ -1 +0,0 @@
<%= image_tag user.profile_image.variant(:thumbnail), class: "rounded-full w-full h-full object-cover" %>