flavor dlavor (#110)

This commit is contained in:
nora 2025-12-17 12:23:08 -05:00 committed by GitHub
parent 48f5e080b2
commit d2dcc70e82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 207 additions and 46 deletions

View file

@ -1,15 +1,17 @@
class Components::AuthWelcome < Components::Base
include Phlex::Rails::Helpers::DistanceOfTimeInWordsToNow
def initialize(headline:, subtitle:, return_to: nil, login_hint: nil)
def initialize(headline:, subtitle:, return_to: nil, login_hint: nil, logo_path: nil)
@headline = headline
@subtitle = subtitle
@return_to = return_to
@login_hint = login_hint
@logo_path = logo_path
end
def view_template
div(class: "auth-container") do
render_brand if @logo_path
div(class: "auth-card") do
render_header
render_actions
@ -20,6 +22,14 @@ class Components::AuthWelcome < Components::Base
private
def render_brand
div(class: "auth-brand") do
vite_image_tag "images/hc-square.png", alt: "Hack Club logo", class: "brand-logo"
span(class: "brand-plus") { "×" }
vite_image_tag @logo_path, alt: "Logo", class: "brand-logo brand-logo--custom"
end
end
def render_header
header do
h1 { @headline }

View file

@ -1,6 +1,7 @@
class Components::Brand < Components::Base
def initialize(identity:)
def initialize(identity:, logo_path: nil)
@identity = identity
@logo_path = logo_path
end
def view_template
@ -20,6 +21,12 @@ class Components::Brand < Components::Base
end
def logo
vite_image_tag "images/hc-square.png", alt: "Hack Club logo", class: "brand-logo"
div(class: "brand-logos") do
vite_image_tag "images/hc-square.png", alt: "Hack Club logo", class: "brand-logo"
if @logo_path
span(class: "brand-plus") { "×" }
vite_image_tag @logo_path, alt: "Logo", class: "brand-logo brand-logo--custom"
end
end
end
end

View file

@ -4,6 +4,7 @@ module PortalFlow
included do
before_action :validate_portal_return_url, only: [ :start, :portal ]
before_action :store_return_url, only: [ :start, :portal ]
helper_method :portal_onboarding_scenario
end
private
@ -12,6 +13,16 @@ module PortalFlow
session[:portal_return_to] || params[:return_to]
end
def portal_program
@portal_program ||= Program.find_by_redirect_uri_host(portal_return_url)
end
def portal_onboarding_scenario
return @portal_onboarding_scenario if defined?(@portal_onboarding_scenario)
scenario_class = portal_program&.onboarding_scenario_class
@portal_onboarding_scenario = scenario_class&.new(current_identity)
end
def store_return_url
session[:portal_return_to] = validated_return_url if params[:return_to].present?
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View file

@ -12,23 +12,43 @@
@include dark-mode {
background: #1a1d23;
}
&.has-background {
background: transparent;
}
}
.auth-brand {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-bottom: 1rem;
margin-bottom: 1.25rem;
img {
width: 32px;
height: 32px;
.brand-logo {
width: 36px;
height: 36px;
border-radius: 8px;
object-fit: contain;
&--custom {
border: 1px solid var(--surface-2-border);
background: var(--surface-1);
}
}
span {
.brand-plus {
font-size: 0.95rem;
font-weight: 500;
color: var(--text-muted);
opacity: 0.5;
}
.brand-text {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-strong);
margin-left: 0.25rem;
}
}

View file

@ -1,15 +1,48 @@
.brand {
&>h1 {
display: inline;
margin-left: 1rem;
vertical-align: middle;
font-size: 29px;
}
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1.5rem;
h1 {
font-size: 1.5rem;
font-weight: 600;
margin: 0;
color: var(--text-strong);
}
}
& img {
width: 50px;
display: inline;
}
.portal-container {
min-height: 100vh;
padding-top: 2rem;
padding-bottom: 3rem;
> .brand {
margin-bottom: 1.5rem;
}
}
margin-bottom: 1rem;
}
.brand-logos {
display: flex;
align-items: center;
gap: 0.5rem;
}
.brand-logo {
width: 40px;
height: 40px;
border-radius: 8px;
object-fit: contain;
&--custom {
border: 1px solid var(--surface-2-border);
background: var(--surface-1);
}
}
.brand-plus {
font-size: 1rem;
font-weight: 500;
color: var(--text-muted);
opacity: 0.6;
}

View file

@ -76,7 +76,7 @@ class OAuthScope
consent_fields: [
{ key: :address, value: ->(ident) {
addr = ident.primary_address
addr ? [addr.line_1, addr.city, addr.state, addr.country].compact.join(", ") : nil
addr ? [ addr.line_1, addr.city, addr.state, addr.country ].compact.join(", ") : nil
} }
]
),
@ -102,7 +102,7 @@ class OAuthScope
description: "See your legal name",
icon: "card-id",
consent_fields: [
{ key: :legal_name, value: ->(ident) { [ident.legal_first_name, ident.legal_last_name].compact.join(" ").presence } }
{ key: :legal_name, value: ->(ident) { [ ident.legal_first_name, ident.legal_last_name ].compact.join(" ").presence } }
]
),
new(
@ -191,7 +191,7 @@ class OAuthScope
scope_names.flat_map do |name|
scope = find(name)
next [] unless scope
[scope] + scope.includes.filter_map { |n| find(n) }
[ scope ] + scope.includes.filter_map { |n| find(n) }
end
end
end

View file

@ -95,6 +95,12 @@ module OnboardingScenarios
def bot_name = nil
def bot_icon_url = nil
# Branding - override to customize logo/background for welcome, OAuth, and portal pages
# logo_path: vite asset path like "images/flavortown.png"
def logo_path = nil
def background_path = nil
def dark_mode_background_path = background_path
# Custom dialogue flow hooks - override in subclasses
def before_first_message = nil
def after_promotion = nil

View file

@ -70,5 +70,9 @@ module OnboardingScenarios
when "flavortown_dino_nuggets" then :dino_nuggets
end
end
def logo_path = "images/flavortown/logo.avif"
def background_path = "images/flavortown/hero-bg.webp"
def dark_mode_background_path = "images/flavortown/bg-dark.png"
end
end

View file

@ -86,6 +86,38 @@ class Program < ApplicationRecord
def authorized_for_identity?(identity) = authorized_tokens.exists?(resource_owner: identity)
def onboarding_scenario_class
return nil if onboarding_scenario.blank?
OnboardingScenarios::Base.find_by_slug(onboarding_scenario)
end
def onboarding_scenario_instance(identity = nil)
onboarding_scenario_class&.new(identity)
end
def self.find_by_redirect_uri_host(url)
return nil if url.blank?
begin
uri = URI.parse(url)
host = uri.host
return nil unless host
find_each do |program|
program.redirect_uri.to_s.split("\n").each do |redirect_uri|
begin
redirect_host = URI.parse(redirect_uri.strip).host
return program if redirect_host == host
rescue URI::InvalidURIError
next
end
end
end
nil
rescue URI::InvalidURIError
nil
end
end
private
def validate_community_scopes

View file

@ -77,10 +77,8 @@
<section class="section-card add-address-card" x-data="{ showForm: false }">
<template x-if="!showForm">
<button @click="showForm = true" class="secondary add-address-btn">
<span class="add-icon">+</span>
<%= local_assigns[:portal] ? t("portal.addresses.manage.add_new") : t("addresses.add_new") %>
</button>
<button
<%= local_assigns[:portal] ? t("portal.addresses.manage.add_new") : t("addresses.add_new") %>>
</template>
<template x-if="showForm">
<div class="address-form" x-init="htmx.process($el)">

View file

@ -1,12 +1,30 @@
<div class="auth-container">
<%
application = Program.find_by(uid: @pre_auth.client.uid)
scenario_class = application&.onboarding_scenario_class
scenario = scenario_class&.new(current_identity)
custom_logo_path = scenario&.logo_path
light_bg = scenario&.background_path
dark_bg = scenario&.dark_mode_background_path
%>
<% if light_bg || dark_bg %>
<style>
body { background-size: cover; background-position: center; }
<% if light_bg %>html[data-theme="light"] body { background-image: url('<%= vite_asset_path(light_bg) %>'); }<% end %>
<% if dark_bg %>html[data-theme="dark"] body { background-image: url('<%= vite_asset_path(dark_bg) %>'); }<% end %>
</style>
<% end %>
<div class="auth-container<%= ' has-background' if light_bg || dark_bg %>">
<div class="auth-brand">
<%= vite_image_tag "images/hc-square.png", alt: "Hack Club logo", class: "brand-logo" %>
<span><%= t("brand") %></span>
<% if custom_logo_path %>
<span class="brand-plus">×</span>
<%= vite_image_tag custom_logo_path, alt: "Logo", class: "brand-logo brand-logo--custom" %>
<% end %>
<span class="brand-text"><%= t("brand") %></span>
</div>
<div class="auth-card oauth-consent">
<header>
<h1><%= session.dig(:stashed_data, "splash_message") || "Continue to #{@pre_auth.client.name}?" %></h1>
<% application = Program.find_by(uid: @pre_auth.client.uid) %>
<small>
<%= raw t('.prompt', client_name: content_tag(:strong) { @pre_auth.client.name }) %>
</small>

View file

@ -23,10 +23,23 @@
<%= vite_stylesheet_tag "application.css" %>
<%= vite_javascript_tag 'application' %>
</head>
<body class="<%= content_for?(:portal_wrapper) ? '' : (content_for?(:body_class) ? yield(:body_class) : 'auth-layout') %>" hx-headers='{"X-CSRF-Token": "<%= form_authenticity_token %>"}' hx-boost="false">
<%
body_style = content_for?(:body_style) ? yield(:body_style) : nil
portal_scenario = respond_to?(:portal_onboarding_scenario) ? portal_onboarding_scenario : nil
light_bg = portal_scenario&.background_path
dark_bg = portal_scenario&.dark_mode_background_path
%>
<body class="<%= content_for?(:portal_wrapper) ? '' : (content_for?(:body_class) ? yield(:body_class) : 'auth-layout') %>" hx-headers='{"X-CSRF-Token": "<%= form_authenticity_token %>"}' hx-boost="false"<%= " style=\"#{body_style}\"".html_safe if body_style %>>
<% if content_for?(:portal_wrapper) && (light_bg || dark_bg) %>
<style>
body { background-size: cover; background-position: center; }
<% if light_bg %>html[data-theme="light"] body { background-image: url('<%= vite_asset_path(light_bg) %>'); }<% end %>
<% if dark_bg %>html[data-theme="dark"] body { background-image: url('<%= vite_asset_path(dark_bg) %>'); }<% end %>
</style>
<% end %>
<% if content_for?(:portal_wrapper) %>
<main class="container portal-container">
<%= render Components::Brand.new(identity: current_identity) %>
<%= render Components::Brand.new(identity: current_identity, logo_path: portal_scenario&.logo_path) %>
<%= render "shared/flash" %>
<span id="async_flash"></span>
<%= yield %>

View file

@ -1,16 +1,19 @@
<% content_for(:portal_wrapper, true) %>
<div class="container-sm">
<div class="page-sections">
<article>
<h1>hey! <%= emoji_image "hyper-dino-wave-flip.gif" %></h1>
<p>We're excited to send you stuff!</p>
<p>First, we need to verify you're under 18.</p>
<p>In the past year, Hack Club has given out ~$1M in grants to students like you, and with that comes a lot of adults trying to slip in.</p>
<p>This quick check helps us make sure our resources go to real teenage hackers.</p>
<% if portal_onboarding_scenario&.logo_path %>
<div style="text-align: center; margin-bottom: 2rem;">
<%= vite_image_tag portal_onboarding_scenario.logo_path, alt: "Logo", style: "height: 150px;" %>
</div>
<% end %>
<article class="section-card" style="position: relative;">
<h1>hey! <%= emoji_image "hyper-dino-wave-flip.gif" %></h1>
<p>We're excited to send you stuff!</p>
<p>First, we need to verify you're under 18.</p>
<p>In the past year, Hack Club has given out ~$1M in grants to students like you, and with that comes a lot of adults trying to slip in.</p>
<p>This quick check helps us make sure our resources go to real teenage hackers.</p>
<div class="status-actions">
<%= link_to "alright! →", portal_verify_document_path, role: "button" %>
</div>
</article>
</div>
<div class="status-actions">
<%= link_to "alright! →", portal_verify_document_path, role: "button" %>
</div>
</article>
</div>

View file

@ -1,7 +1,13 @@
<% service_name = @program&.name || "the application" %>
<% scenario_class = @program&.onboarding_scenario_class %>
<% scenario = scenario_class&.new(nil) %>
<% if scenario&.background_url %>
<% content_for(:body_style) { "background-image: url('#{scenario.background_url}'); background-size: cover; background-position: center;" } %>
<% end %>
<%= render Components::AuthWelcome.new(
headline: "Continue to #{service_name}",
subtitle: "Sign in or create an account to continue",
return_to: @return_to,
login_hint: @login_hint
login_hint: @login_hint,
logo_path: scenario&.logo_path
) %>

View file

@ -53,7 +53,7 @@ module IdentityVault
end
config.to_prepare do
Doorkeeper::ApplicationController.layout "application"
Doorkeeper::ApplicationController.layout "logged_out"
Doorkeeper::ApplicationController.skip_before_action :authenticate_identity!
Backend::NoAuthController.skip_after_action :verify_authorized
end