From b85e2352765ae9c4bb99021ac3e999a1151d17f1 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 8 May 2025 10:59:37 +0100 Subject: [PATCH 01/44] Add Tailwind successfully, without interfering with the TW3 used by ActiveAdmin (for now) --- Gemfile | 4 ++++ Gemfile.lock | 6 ++++++ app/assets/tailwind/application.css | 1 + app/views/home/anon_home.html.haml | 2 +- app/views/layouts/_head.html.haml | 1 + bin/dev | 2 -- config/puma.rb | 3 +++ 7 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 app/assets/tailwind/application.css delete mode 100755 bin/dev diff --git a/Gemfile b/Gemfile index 1ae2e5e..b0dd162 100644 --- a/Gemfile +++ b/Gemfile @@ -105,3 +105,7 @@ group :production do gem 'aws-sdk-s3', '~> 1.183' gem 'rails_12factor', '~> 0.0.3' end + +gem "tailwindcss-ruby", "~> 4.1" + +gem "tailwindcss-rails", "~> 4.2" diff --git a/Gemfile.lock b/Gemfile.lock index c41337b..21cac46 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -511,6 +511,10 @@ GEM activesupport (>= 6.1) sprockets (>= 3.0.0) stringio (3.1.7) + tailwindcss-rails (4.2.3) + railties (>= 7.0.0) + tailwindcss-ruby (~> 4.0) + tailwindcss-ruby (4.1.5) temple (0.10.3) thor (1.3.2) tilt (2.6.0) @@ -616,6 +620,8 @@ DEPENDENCIES spinach spinach-rails sprockets-rails (~> 3.5) + tailwindcss-rails (~> 4.2) + tailwindcss-ruby (~> 4.1) timecop turbo-rails (~> 2.0) web-console (>= 3.3.0) diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/app/assets/tailwind/application.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/app/views/home/anon_home.html.haml b/app/views/home/anon_home.html.haml index 62a1eeb..774e3e6 100644 --- a/app/views/home/anon_home.html.haml +++ b/app/views/home/anon_home.html.haml @@ -1,5 +1,5 @@ %section.hero - .hero-body + .hero-body.bg-red-100 .container.has-text-centered %h1.home-title .home-title__image= image_tag 'logo.svg', alt: t('icon_alt') diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index f353b51..03a380e 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -7,4 +7,5 @@ = javascript_include_tag 'application', defer: true %link{rel: 'preconnect', href: 'https://fonts.googleapis.com', crossorigin: true} = stylesheet_link_tag 'application', media: 'all' += stylesheet_link_tag 'tailwind', media: 'all' = render 'layouts/ga' diff --git a/bin/dev b/bin/dev deleted file mode 100755 index 5f91c20..0000000 --- a/bin/dev +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env ruby -exec "./bin/rails", "server", *ARGV diff --git a/config/puma.rb b/config/puma.rb index a248513..5f3c613 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -36,6 +36,9 @@ # Run the Solid Queue supervisor inside of Puma for single-server deployments plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] +# Live rebuild for Tailwind +plugin :tailwindcss if ENV.fetch("RAILS_ENV", "development") == "development" + # Specify the PID file. Defaults to tmp/pids/server.pid in development. # In other environments, only set the PID file if requested. pidfile ENV["PIDFILE"] if ENV["PIDFILE"] From 33b84870fb7424046cdfce6c94be4d3061e2491e Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 8 May 2025 13:02:52 +0100 Subject: [PATCH 02/44] Get RubyUI installed, albeit not able to share a page with Bulma --- Gemfile | 9 +- Gemfile.lock | 12 ++ app/assets/tailwind/application.css | 153 ++++++++++++++++++++++++ app/components/base.rb | 16 +++ app/components/ruby_ui/base.rb | 22 ++++ app/components/ruby_ui/button/button.rb | 97 +++++++++++++++ app/views/base.rb | 9 ++ app/views/home/anon_home.html.haml | 2 +- app/views/layouts/_head.html.haml | 1 - config/initializers/phlex.rb | 16 +++ config/initializers/ruby_ui.rb | 18 +++ package.json | 5 +- yarn.lock | 47 +++++++- 13 files changed, 399 insertions(+), 8 deletions(-) create mode 100644 app/components/base.rb create mode 100644 app/components/ruby_ui/base.rb create mode 100644 app/components/ruby_ui/button/button.rb create mode 100644 app/views/base.rb create mode 100644 config/initializers/phlex.rb create mode 100644 config/initializers/ruby_ui.rb diff --git a/Gemfile b/Gemfile index b0dd162..cdc0c0b 100644 --- a/Gemfile +++ b/Gemfile @@ -52,12 +52,16 @@ gem "inline_svg", "~> 1.10" gem 'meta-tags', '~> 2.13' gem 'mini_magick', '~> 5.2' gem 'ostruct', '~> 0.6.1' # dependency of gaffe +gem 'phlex-rails', '~> 2.2' gem 'pundit', '~> 2.1' gem 'rails_semantic_logger', '~> 4.6' gem 'rswag-api', '~> 2.4' gem 'rswag-ui', '~> 2.4' gem 'simple_form', '~> 5.0' gem 'spicy-proton', '~> 2.1' +gem 'tailwindcss-rails', '~> 4.2' +gem 'tailwindcss-ruby', '~> 4.1' +gem 'tailwind_merge', '~> 1.2' gem 'wicked', '~> 2.0' # gems for buttondown - eventually move this to its own gem @@ -86,6 +90,7 @@ group :development do gem 'better_errors' gem 'foreman' gem 'haml-rails' + gem 'ruby_ui', require: false end group :test do @@ -105,7 +110,3 @@ group :production do gem 'aws-sdk-s3', '~> 1.183' gem 'rails_12factor', '~> 0.0.3' end - -gem "tailwindcss-ruby", "~> 4.1" - -gem "tailwindcss-rails", "~> 4.2" diff --git a/Gemfile.lock b/Gemfile.lock index 21cac46..33138d7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -361,6 +361,11 @@ GEM orm_adapter (0.5.0) ostruct (0.6.1) pg (1.5.9) + phlex (2.2.1) + zeitwerk (~> 2.7) + phlex-rails (2.2.0) + phlex (~> 2.2.1) + railties (>= 7.1, < 9) pp (0.6.2) prettyprint prettyprint (0.2.0) @@ -487,12 +492,14 @@ GEM ruby-vips (2.2.3) ffi (~> 1.12) logger + ruby_ui (1.0.1) securerandom (0.4.1) semantic_logger (4.16.1) concurrent-ruby (~> 1.0) simple_form (5.3.1) actionpack (>= 5.2) activemodel (>= 5.2) + sin_lru_redux (2.5.2) spicy-proton (2.1.15) bindata (~> 2.3) spinach (0.12.0) @@ -511,6 +518,8 @@ GEM activesupport (>= 6.1) sprockets (>= 3.0.0) stringio (3.1.7) + tailwind_merge (1.2.0) + sin_lru_redux (~> 2.5) tailwindcss-rails (4.2.3) railties (>= 7.0.0) tailwindcss-ruby (~> 4.0) @@ -601,6 +610,7 @@ DEPENDENCIES mini_magick (~> 5.2) ostruct (~> 0.6.1) pg (>= 0.18, < 2.0) + phlex-rails (~> 2.2) pry-rails pry-rescue puma (~> 6.0) @@ -615,11 +625,13 @@ DEPENDENCIES rswag-api (~> 2.4) rswag-specs (~> 2.4) rswag-ui (~> 2.4) + ruby_ui simple_form (~> 5.0) spicy-proton (~> 2.1) spinach spinach-rails sprockets-rails (~> 3.5) + tailwind_merge (~> 1.2) tailwindcss-rails (~> 4.2) tailwindcss-ruby (~> 4.1) timecop diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index f1d8c73..8735fe6 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -1 +1,154 @@ @import "tailwindcss"; + +@plugin "@tailwindcss/forms"; +@plugin "@tailwindcss/typography"; + + +@import "tw-animate-css"; + + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + + /* ruby_ui specific */ + --warning: hsl(38 92% 50%); + --warning-foreground: hsl(0 0% 100%); + --success: hsl(87 100% 37%); + --success-foreground: hsl(0 0% 100%); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); + + /* ruby_ui specific */ + --warning: hsl(38 92% 50%); + --warning-foreground: hsl(0 0% 100%); + --success: hsl(84 81% 44%); + --success-foreground: hsl(0 0% 100%); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + /* ruby_ui specific */ + --color-warning: var(--warning); + --color-warning-foreground: var(--warning-foreground); + --color-success: var(--success); + --color-success-foreground: var(--success-foreground); +} + +/* Container settings */ +@utility container { + margin-inline: auto; + padding-inline: 2rem; + max-width: 1400px; +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/components/base.rb b/app/components/base.rb new file mode 100644 index 0000000..1e5ee4e --- /dev/null +++ b/app/components/base.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class Components::Base < Phlex::HTML + include Components + include RubyUI + + # Include any helpers you want to be available across all components + include Phlex::Rails::Helpers::Routes + + if Rails.env.development? + def before_template + comment { "Before #{self.class.name}" } + super + end + end +end diff --git a/app/components/ruby_ui/base.rb b/app/components/ruby_ui/base.rb new file mode 100644 index 0000000..a60b8bb --- /dev/null +++ b/app/components/ruby_ui/base.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "tailwind_merge" + +module RubyUI + class Base < Phlex::HTML + TAILWIND_MERGER = ::TailwindMerge::Merger.new.freeze unless defined?(TAILWIND_MERGER) + + attr_reader :attrs + + def initialize(**user_attrs) + @attrs = mix(default_attrs, user_attrs) + @attrs[:class] = TAILWIND_MERGER.merge(@attrs[:class]) if @attrs[:class] + end + + private + + def default_attrs + {} + end + end +end diff --git a/app/components/ruby_ui/button/button.rb b/app/components/ruby_ui/button/button.rb new file mode 100644 index 0000000..9636fa2 --- /dev/null +++ b/app/components/ruby_ui/button/button.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +module RubyUI + class Button < Base + def initialize(type: :button, variant: :primary, size: :md, icon: false, **attrs) + @type = type + @variant = variant.to_sym + @size = size.to_sym + @icon = icon + super(**attrs) + end + + def view_template(&) + button(**attrs, &) + end + + private + + def size_classes + if @icon + case @size + when :sm then "h-6 w-6" + when :md then "h-9 w-9" + when :lg then "h-10 w-10" + when :xl then "h-12 w-12" + end + else + case @size + when :sm then "px-3 py-1.5 h-8 text-xs" + when :md then "px-4 py-2 h-9 text-sm" + when :lg then "px-4 py-2 h-10 text-base" + when :xl then "px-6 py-3 h-12 text-base" + end + end + end + + def primary_classes + [ + "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90", + size_classes + ] + end + + def link_classes + [ + "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 text-primary underline-offset-4 hover:underline", + size_classes + ] + end + + def secondary_classes + [ + "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-secondary text-secondary-foreground hover:bg-opacity-80", + size_classes + ] + end + + def destructive_classes + [ + "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + size_classes + ] + end + + def outline_classes + [ + "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + size_classes + ] + end + + def ghost_classes + [ + "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground", + size_classes + ] + end + + def default_classes + case @variant + when :primary then primary_classes + when :link then link_classes + when :secondary then secondary_classes + when :destructive then destructive_classes + when :outline then outline_classes + when :ghost then ghost_classes + end + end + + def default_attrs + { + type: @type, + class: default_classes + } + end + end +end diff --git a/app/views/base.rb b/app/views/base.rb new file mode 100644 index 0000000..0e4f1b3 --- /dev/null +++ b/app/views/base.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Views::Base < Components::Base + # The `Views::Base` is an abstract class for all your views. + + # By default, it inherits from `Components::Base`, but you + # can change that to `Phlex::HTML` if you want to keep views and + # components independent. +end diff --git a/app/views/home/anon_home.html.haml b/app/views/home/anon_home.html.haml index 774e3e6..62a1eeb 100644 --- a/app/views/home/anon_home.html.haml +++ b/app/views/home/anon_home.html.haml @@ -1,5 +1,5 @@ %section.hero - .hero-body.bg-red-100 + .hero-body .container.has-text-centered %h1.home-title .home-title__image= image_tag 'logo.svg', alt: t('icon_alt') diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 03a380e..f353b51 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -7,5 +7,4 @@ = javascript_include_tag 'application', defer: true %link{rel: 'preconnect', href: 'https://fonts.googleapis.com', crossorigin: true} = stylesheet_link_tag 'application', media: 'all' -= stylesheet_link_tag 'tailwind', media: 'all' = render 'layouts/ga' diff --git a/config/initializers/phlex.rb b/config/initializers/phlex.rb new file mode 100644 index 0000000..44b6e85 --- /dev/null +++ b/config/initializers/phlex.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Views +end + +module Components + extend Phlex::Kit +end + +Rails.autoloaders.main.push_dir( + Rails.root.join("app/views"), namespace: Views +) + +Rails.autoloaders.main.push_dir( + Rails.root.join("app/components"), namespace: Components +) diff --git a/config/initializers/ruby_ui.rb b/config/initializers/ruby_ui.rb new file mode 100644 index 0000000..a5f251f --- /dev/null +++ b/config/initializers/ruby_ui.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module RubyUI + extend Phlex::Kit +end + +# Allow using RubyUI instead RubyUi +Rails.autoloaders.main.inflector.inflect( + "ruby_ui" => "RubyUI" +) + +# Allow using RubyUI::ComponentName instead Components::RubyUI::ComponentName +Rails.autoloaders.main.push_dir( + Rails.root.join("app/components/ruby_ui"), namespace: RubyUI +) + +# Allow using RubyUI::ComponentName instead RubyUI::ComponentName::ComponentName +Rails.autoloaders.main.collapse(Rails.root.join("app/components/ruby_ui/*")) diff --git a/package.json b/package.json index 6bb6e33..d09a028 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "@hotwired/turbo": "^8.0.13", "@kollegorna/cocoon-vanilla-js": "^1.0.7", "@rails/activestorage": "^8.0.200", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.16", "@uppy/core": "^4.3.1", "@uppy/dashboard": "^4.1.3", "@uppy/image-editor": "^3.2.1", @@ -23,7 +25,8 @@ "luminous-lightbox": "^2.3.2", "sass": "^1.60.0", "sortablejs": "^1.15.0", - "tailwindcss": "3" + "tailwindcss": "3", + "tw-animate-css": "^1.2.9" }, "devDependencies": { "@types/sortablejs": "^1.15.8", diff --git a/yarn.lock b/yarn.lock index a87fea8..9505fdb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -336,6 +336,23 @@ resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.1.400.tgz#b2a76bdccb5197b9e866954536106386c87cfab5" integrity sha512-YwvXm3BR5tn+VCAKYGycLejMRVZE3Ionj5gFjEeGXCZnI0Rpi+7dKpmyu90kdUY7dRUFpHTdu9zZceEzFLl38w== +"@tailwindcss/forms@^0.5.10": + version "0.5.10" + resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.5.10.tgz#0a1cd67b6933402f1985a04595bd24f9785aa302" + integrity sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw== + dependencies: + mini-svg-data-uri "^1.2.3" + +"@tailwindcss/typography@^0.5.16": + version "0.5.16" + resolved "https://registry.yarnpkg.com/@tailwindcss/typography/-/typography-0.5.16.tgz#a926c8f44d5c439b2915e231cad80058850047c6" + integrity sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA== + dependencies: + lodash.castarray "^4.4.0" + lodash.isplainobject "^4.0.6" + lodash.merge "^4.6.2" + postcss-selector-parser "6.0.10" + "@transloadit/prettier-bytes@^0.3.4": version "0.3.5" resolved "https://registry.yarnpkg.com/@transloadit/prettier-bytes/-/prettier-bytes-0.3.5.tgz#0cca83975293e3f4990229914942c69714122ede" @@ -934,6 +951,21 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +lodash.castarray@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz#c02513515e309daddd4c24c60cfddcf5976d9115" + integrity sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q== + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -1005,7 +1037,7 @@ mime-match@^1.0.2: dependencies: wildcard "^1.1.0" -mini-svg-data-uri@^1.4.3: +mini-svg-data-uri@^1.2.3, mini-svg-data-uri@^1.4.3: version "1.4.4" resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939" integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg== @@ -1170,6 +1202,14 @@ postcss-nested@^6.2.0: dependencies: postcss-selector-parser "^6.1.1" +postcss-selector-parser@6.0.10: + version "6.0.10" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d" + integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-selector-parser@^6.1.1, postcss-selector-parser@^6.1.2: version "6.1.2" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" @@ -1465,6 +1505,11 @@ tslib@^2.7.0, tslib@^2.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== +tw-animate-css@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/tw-animate-css/-/tw-animate-css-1.2.9.tgz#b25d5fb31fd3c3ec6d91c1d1842c0d7c0fdbd999" + integrity sha512-9O4k1at9pMQff9EAcCEuy1UNO43JmaPQvq+0lwza9Y0BQ6LB38NiMj+qHqjoQf40355MX+gs6wtlR6H9WsSXFg== + typescript@^5.4.5: version "5.8.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" From c95199545cf5832e4b9e29c4cab5d8cdc538b085 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 8 May 2025 15:55:00 +0100 Subject: [PATCH 03/44] Add Literal as recommended --- Gemfile | 1 + Gemfile.lock | 3 +++ app/components/base.rb | 1 + 3 files changed, 5 insertions(+) diff --git a/Gemfile b/Gemfile index cdc0c0b..ddb42ce 100644 --- a/Gemfile +++ b/Gemfile @@ -49,6 +49,7 @@ gem 'high_voltage', '~> 4.0' gem 'image_processing', '~> 1.11' gem "importmap-rails", "~> 2.1" gem "inline_svg", "~> 1.10" +gem "literal", "~> 1.7" gem 'meta-tags', '~> 2.13' gem 'mini_magick', '~> 5.2' gem 'ostruct', '~> 0.6.1' # dependency of gaffe diff --git a/Gemfile.lock b/Gemfile.lock index 33138d7..a17d0bb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -321,6 +321,8 @@ GEM listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) + literal (1.7.1) + zeitwerk logger (1.7.0) loofah (2.24.0) crass (~> 1.0.2) @@ -606,6 +608,7 @@ DEPENDENCIES inline_svg (~> 1.10) jsbundling-rails (~> 1.1) listen + literal (~> 1.7) meta-tags (~> 2.13) mini_magick (~> 5.2) ostruct (~> 0.6.1) diff --git a/app/components/base.rb b/app/components/base.rb index 1e5ee4e..c2b349e 100644 --- a/app/components/base.rb +++ b/app/components/base.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Components::Base < Phlex::HTML + extend Literal::Properties include Components include RubyUI From 364bb105e2d3b473bf536cd3edf9abc5782a7fd9 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 8 May 2025 17:18:08 +0100 Subject: [PATCH 04/44] Add a layout for the Tailwind pages - currently has nothing in it --- app/components/base.rb | 1 + app/components/layout.rb | 47 ++++++++++++++++++++++++++++++ app/controllers/home_controller.rb | 6 ++-- app/views/home/anon.rb | 10 +++++++ app/views/layouts/_ga.html.erb | 4 +-- 5 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 app/components/layout.rb create mode 100644 app/views/home/anon.rb diff --git a/app/components/base.rb b/app/components/base.rb index c2b349e..1125975 100644 --- a/app/components/base.rb +++ b/app/components/base.rb @@ -7,6 +7,7 @@ class Components::Base < Phlex::HTML # Include any helpers you want to be available across all components include Phlex::Rails::Helpers::Routes + include Phlex::Rails::Helpers::T if Rails.env.development? def before_template diff --git a/app/components/layout.rb b/app/components/layout.rb new file mode 100644 index 0000000..3d95655 --- /dev/null +++ b/app/components/layout.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +class Components::Layout < Components::Base + include Phlex::Rails::Layout + prop :title, String + + def view_template + doctype + html(lang: 'en') do + render_head + head do + title { @title } + end + + body do + yield + end + end + end + + protected + + def render_head + head do + helpers.display_meta_tags default_meta_tags + csrf_meta_tags + csp_meta_tag + favicon_link_tag + favicon_link_tag 'apple-touch-icon.png', rel: 'apple-touch-icon', type: 'image/png' + link(rel: 'manifest', href: manifest_path(format: :json)) + javascript_include_tag 'application', data_turbo_track: 'reload', type: 'module' + stylesheet_link_tag 'tailwind', media: 'all', data_turbo_track: 'reload' + render partial('layouts/ga') + end + end + + def default_meta_tags + { + site: t('product_name'), + reverse: true, + viewport: 'width=device-width, initial-scale=1.0', + 'theme-color' => '#2e333d', + 'view-transition': 'same-origin', + 'turbo-refresh-method': 'morph', + } + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index fd74da9..7623607 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -2,14 +2,16 @@ class HomeController < ApplicationController protect_from_forgery except: :service_worker skip_after_action :verify_policy_scoped skip_after_action :verify_authorized + + layout false def index if user_signed_in? # Refuse to show the dashboard to people who haven't completed step 1 return redirect_to(profile_index_path) if !current_user.basic_names_complete? - render 'user_home' + render 'user_home', layout: 'application' else - render 'anon_home' + render Views::Home::Anon end end diff --git a/app/views/home/anon.rb b/app/views/home/anon.rb new file mode 100644 index 0000000..9f2fd4f --- /dev/null +++ b/app/views/home/anon.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Views::Home::Anon < Views::Base + def view_template + Layout(title: "Home") do + h1 { "This is the homepage" } + Button { "Hello" } + end + end +end diff --git a/app/views/layouts/_ga.html.erb b/app/views/layouts/_ga.html.erb index a49ad3d..8b29f7e 100644 --- a/app/views/layouts/_ga.html.erb +++ b/app/views/layouts/_ga.html.erb @@ -9,13 +9,13 @@ From f335185b567df0633d3fee377cf9dc90cd54a722 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 8 May 2025 18:02:49 +0100 Subject: [PATCH 05/44] Add the start of the navbar --- app/assets/tailwind/application.css | 6 +- app/components/base.rb | 3 + app/components/layout.rb | 1 + app/components/page_header.rb | 39 ++++++++++++ app/components/ruby_ui/link/link.rb | 97 +++++++++++++++++++++++++++++ 5 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 app/components/page_header.rb create mode 100644 app/components/ruby_ui/link/link.rb diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index 8735fe6..d3b54df 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -16,10 +16,10 @@ --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); + --primary: oklch(0.57 0.2077 22.63); --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.32 0.0192 264.26); + --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.97 0 0); --muted-foreground: oklch(0.556 0 0); --accent: oklch(0.97 0 0); diff --git a/app/components/base.rb b/app/components/base.rb index 1125975..972c2a5 100644 --- a/app/components/base.rb +++ b/app/components/base.rb @@ -9,6 +9,9 @@ class Components::Base < Phlex::HTML include Phlex::Rails::Helpers::Routes include Phlex::Rails::Helpers::T + register_output_helper :icon + register_output_helper :inline_svg_tag + if Rails.env.development? def before_template comment { "Before #{self.class.name}" } diff --git a/app/components/layout.rb b/app/components/layout.rb index 3d95655..63412c3 100644 --- a/app/components/layout.rb +++ b/app/components/layout.rb @@ -13,6 +13,7 @@ def view_template end body do + PageHeader { } yield end end diff --git a/app/components/page_header.rb b/app/components/page_header.rb new file mode 100644 index 0000000..70507f6 --- /dev/null +++ b/app/components/page_header.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class Components::PageHeader < Components::Base + # From https://www.hyperui.dev/components/marketing/headers + + def view_template + header(class: 'bg-secondary text-secondary-foreground') do + div(class: 'mx-auto flex h-16 max-w-screen-xl items-center gap-8 px-4 sm:px-6 lg:px-8') do + render_logo + div(class: 'flex flex-1 items-center justify-end md:justify-between') do + render_links + end + end + end + end + + protected + + def render_logo + a(class: 'flex justify-start items-center gap-2', href: root_path) do + inline_svg_tag('logo.svg', class: 'h-8', size: "100%") + span(class: 'text-lg font-bold') { t('product_name') } + end + end + + def render_links + links = [ + {text: 'About', href: page_path('about')} + ] + nav(aria_label: 'Global', class: 'hidden md:block') do + ul(class: 'flex items-center gap-6 text-sm') do + links.each do |l| + Link(class: 'text-current', href: l[:href]) { l[:text] } + end + end + end + end + +end diff --git a/app/components/ruby_ui/link/link.rb b/app/components/ruby_ui/link/link.rb new file mode 100644 index 0000000..528ea50 --- /dev/null +++ b/app/components/ruby_ui/link/link.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +module RubyUI + class Link < Base + def initialize(href: "#", variant: :link, size: :md, icon: false, **attrs) + @href = href + @variant = variant.to_sym + @size = size.to_sym + @icon = icon + super(**attrs) + end + + def view_template(&) + a(href: @href, **attrs, &) + end + + private + + def size_classes + if @icon + case @size + when :sm then "h-6 w-6" + when :md then "h-9 w-9" + when :lg then "h-10 w-10" + when :xl then "h-12 w-12" + end + else + case @size + when :sm then "px-3 py-1.5 h-8 text-xs" + when :md then "px-4 py-2 h-9 text-sm" + when :lg then "px-4 py-2 h-10 text-base" + when :xl then "px-6 py-3 h-12 text-base" + end + end + end + + def primary_classes + [ + "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90", + size_classes + ] + end + + def link_classes + [ + "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 text-primary underline-offset-4 hover:underline", + size_classes + ] + end + + def secondary_classes + [ + "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-secondary text-secondary-foreground hover:bg-opacity-80", + size_classes + ] + end + + def destructive_classes + [ + "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + size_classes + ] + end + + def outline_classes + [ + "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + size_classes + ] + end + + def ghost_classes + [ + "whitespace-nowrap inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground", + size_classes + ] + end + + def default_classes + case @variant + when :primary then primary_classes + when :link then link_classes + when :secondary then secondary_classes + when :destructive then destructive_classes + when :outline then outline_classes + when :ghost then ghost_classes + end + end + + def default_attrs + { + type: "button", + class: default_classes + } + end + end +end From 3eedc4c1935ce397dae5fe275cf859166429bd8e Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Fri, 9 May 2025 10:44:19 +0100 Subject: [PATCH 06/44] Some work on page header, but it's time to change direction --- Gemfile | 1 + Gemfile.lock | 3 ++ app/assets/images/icons/menu.svg | 1 + app/assets/images/icons/weather-night.svg | 1 + .../images/icons/white-balance-sunny.svg | 1 + app/assets/tailwind/application.css | 1 - app/components/base.rb | 5 +++ app/components/layout.rb | 8 ++-- app/components/page_header.rb | 44 +++++++++++++++++-- .../ruby_ui/theme_toggle/set_dark_mode.rb | 16 +++++++ .../ruby_ui/theme_toggle/set_light_mode.rb | 16 +++++++ .../ruby_ui/theme_toggle/theme_toggle.rb | 9 ++++ app/components/shell_layout.rb | 14 ++++++ .../controllers/{index.ts => index.js} | 41 ++++++++++++----- .../ruby_ui/theme_toggle_controller.js | 30 +++++++++++++ app/views/home/anon.rb | 2 +- config/locales/views.en.yml | 3 ++ 17 files changed, 175 insertions(+), 21 deletions(-) create mode 100644 app/assets/images/icons/menu.svg create mode 100644 app/assets/images/icons/weather-night.svg create mode 100644 app/assets/images/icons/white-balance-sunny.svg create mode 100644 app/components/ruby_ui/theme_toggle/set_dark_mode.rb create mode 100644 app/components/ruby_ui/theme_toggle/set_light_mode.rb create mode 100644 app/components/ruby_ui/theme_toggle/theme_toggle.rb create mode 100644 app/components/shell_layout.rb rename app/javascript/controllers/{index.ts => index.js} (79%) create mode 100644 app/javascript/controllers/ruby_ui/theme_toggle_controller.js diff --git a/Gemfile b/Gemfile index ddb42ce..d3beea2 100644 --- a/Gemfile +++ b/Gemfile @@ -60,6 +60,7 @@ gem 'rswag-api', '~> 2.4' gem 'rswag-ui', '~> 2.4' gem 'simple_form', '~> 5.0' gem 'spicy-proton', '~> 2.1' +gem 'stimulus-rails', '~> 1.3' gem 'tailwindcss-rails', '~> 4.2' gem 'tailwindcss-ruby', '~> 4.1' gem 'tailwind_merge', '~> 1.2' diff --git a/Gemfile.lock b/Gemfile.lock index a17d0bb..2c0e83b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -519,6 +519,8 @@ GEM actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) + stimulus-rails (1.3.4) + railties (>= 6.0.0) stringio (3.1.7) tailwind_merge (1.2.0) sin_lru_redux (~> 2.5) @@ -634,6 +636,7 @@ DEPENDENCIES spinach spinach-rails sprockets-rails (~> 3.5) + stimulus-rails (~> 1.3) tailwind_merge (~> 1.2) tailwindcss-rails (~> 4.2) tailwindcss-ruby (~> 4.1) diff --git a/app/assets/images/icons/menu.svg b/app/assets/images/icons/menu.svg new file mode 100644 index 0000000..cad83c2 --- /dev/null +++ b/app/assets/images/icons/menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/icons/weather-night.svg b/app/assets/images/icons/weather-night.svg new file mode 100644 index 0000000..63175c9 --- /dev/null +++ b/app/assets/images/icons/weather-night.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/icons/white-balance-sunny.svg b/app/assets/images/icons/white-balance-sunny.svg new file mode 100644 index 0000000..604e41d --- /dev/null +++ b/app/assets/images/icons/white-balance-sunny.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index d3b54df..9612542 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -6,7 +6,6 @@ @import "tw-animate-css"; - @custom-variant dark (&:is(.dark *)); :root { diff --git a/app/components/base.rb b/app/components/base.rb index 972c2a5..0eaa9b3 100644 --- a/app/components/base.rb +++ b/app/components/base.rb @@ -18,4 +18,9 @@ def before_template super end end + + def container_classes + %w[mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8] + end + end diff --git a/app/components/layout.rb b/app/components/layout.rb index 63412c3..8564683 100644 --- a/app/components/layout.rb +++ b/app/components/layout.rb @@ -2,6 +2,9 @@ class Components::Layout < Components::Base include Phlex::Rails::Layout + + register_output_helper :display_meta_tags + prop :title, String def view_template @@ -12,8 +15,7 @@ def view_template title { @title } end - body do - PageHeader { } + body(class: 'bg-background text-foreground') do yield end end @@ -23,7 +25,7 @@ def view_template def render_head head do - helpers.display_meta_tags default_meta_tags + display_meta_tags default_meta_tags csrf_meta_tags csp_meta_tag favicon_link_tag diff --git a/app/components/page_header.rb b/app/components/page_header.rb index 70507f6..6e5ba0c 100644 --- a/app/components/page_header.rb +++ b/app/components/page_header.rb @@ -5,10 +5,11 @@ class Components::PageHeader < Components::Base def view_template header(class: 'bg-secondary text-secondary-foreground') do - div(class: 'mx-auto flex h-16 max-w-screen-xl items-center gap-8 px-4 sm:px-6 lg:px-8') do + div(class: container_classes + %w[flex h-16 items-center gap-8]) do render_logo div(class: 'flex flex-1 items-center justify-end md:justify-between') do render_links + render_actions end end end @@ -16,6 +17,12 @@ def view_template protected + def links + [ + {text: t('.about'), href: page_path('about')}, + ] + end + def render_logo a(class: 'flex justify-start items-center gap-2', href: root_path) do inline_svg_tag('logo.svg', class: 'h-8', size: "100%") @@ -24,9 +31,6 @@ def render_logo end def render_links - links = [ - {text: 'About', href: page_path('about')} - ] nav(aria_label: 'Global', class: 'hidden md:block') do ul(class: 'flex items-center gap-6 text-sm') do links.each do |l| @@ -36,4 +40,36 @@ def render_links end end + def render_actions + div(class: 'flex items-center gap-4') do + # Links that are visible on mobile and desktop + div(class: 'sm:flex items-center sm:gap-4') do + Link(variant: :primary, href: new_user_session_path) { "Login" } + end + # Links that are hidden behind the burger on mobile + div(class: 'hidden sm:flex') do + Link(variant: :primary, href: new_user_registration_path) { "Sign up" } + render_theme_toggle + end + # The burger itself + div(class: 'block md:hidden') do + Button { icon('menu', class: 'fill-current') } + end + end + end + + def render_theme_toggle + ThemeToggle do |toggle| + SetLightMode(class: 'flex items-center') do + Button(variant: :ghost, icon: true) do + icon('white-balance-sunny', class: 'fill-current p-2') + end + end + SetDarkMode(class: 'dark:flex items-center') do + Button(variant: :ghost, icon: true) do + icon('weather-night', class: 'fill-current p-2') + end + end + end + end end diff --git a/app/components/ruby_ui/theme_toggle/set_dark_mode.rb b/app/components/ruby_ui/theme_toggle/set_dark_mode.rb new file mode 100644 index 0000000..3b30765 --- /dev/null +++ b/app/components/ruby_ui/theme_toggle/set_dark_mode.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module RubyUI + class SetDarkMode < Base + def view_template(&) + div(**attrs, &) + end + + def default_attrs + { + class: "hidden dark:inline-block", + data: {controller: "ruby-ui--theme-toggle", action: "click->ruby-ui--theme-toggle#setLightTheme"} + } + end + end +end diff --git a/app/components/ruby_ui/theme_toggle/set_light_mode.rb b/app/components/ruby_ui/theme_toggle/set_light_mode.rb new file mode 100644 index 0000000..00f88fb --- /dev/null +++ b/app/components/ruby_ui/theme_toggle/set_light_mode.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module RubyUI + class SetLightMode < Base + def view_template(&) + div(**attrs, &) + end + + def default_attrs + { + class: "dark:hidden", + data: {controller: "ruby-ui--theme-toggle", action: "click->ruby-ui--theme-toggle#setDarkTheme"} + } + end + end +end diff --git a/app/components/ruby_ui/theme_toggle/theme_toggle.rb b/app/components/ruby_ui/theme_toggle/theme_toggle.rb new file mode 100644 index 0000000..000f805 --- /dev/null +++ b/app/components/ruby_ui/theme_toggle/theme_toggle.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module RubyUI + class ThemeToggle < Base + def view_template(&) + div(**attrs, &) + end + end +end diff --git a/app/components/shell_layout.rb b/app/components/shell_layout.rb new file mode 100644 index 0000000..d1c714f --- /dev/null +++ b/app/components/shell_layout.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class Components::ShellLayout < Components::Layout + def view_template + super do + PageHeader() + main do + div(class: container_classes + %w[py-8 lg:py-12]) do + yield + end + end + end + end +end diff --git a/app/javascript/controllers/index.ts b/app/javascript/controllers/index.js similarity index 79% rename from app/javascript/controllers/index.ts rename to app/javascript/controllers/index.js index 198f6de..f4a168f 100644 --- a/app/javascript/controllers/index.ts +++ b/app/javascript/controllers/index.js @@ -1,27 +1,44 @@ +// This file is auto-generated by ./bin/rails stimulus:manifest:update +// Run that command whenever you add a new controller or create them with +// ./bin/rails generate stimulus controllerName + import { application } from "./application" import LightboxController from "./lightbox_controller" -import LikenessUploaderController from "./likeness_uploader_controller" -import MessagesController from "./messages_controller" -import ModalController from "./modal_controller" -import NapcController from "./napc_controller" -import NavbarController from "./navbar_controller" -import PlayerController from "./player_controller" -import PronounFormController from "./pronoun_form_controller" -import PronunciationFormController from "./pronunciation_form_controller" -import PublicProfileController from "./public_profile_controller" -import RecorderController from "./recorder_controller" -import UrlController from "./url_controller" - application.register("lightbox", LightboxController) + +import LikenessUploaderController from "./likeness_uploader_controller" application.register("likeness-uploader", LikenessUploaderController) + +import MessagesController from "./messages_controller" application.register("messages", MessagesController) + +import ModalController from "./modal_controller" application.register("modal", ModalController) + +import NapcController from "./napc_controller" application.register("napc", NapcController) + +import NavbarController from "./navbar_controller" application.register("navbar", NavbarController) + +import PlayerController from "./player_controller" application.register("player", PlayerController) + +import PronounFormController from "./pronoun_form_controller" application.register("pronoun-form", PronounFormController) + +import PronunciationFormController from "./pronunciation_form_controller" application.register("pronunciation-form", PronunciationFormController) + +import PublicProfileController from "./public_profile_controller" application.register("public-profile", PublicProfileController) + +import RecorderController from "./recorder_controller" application.register("recorder", RecorderController) + +import RubyUi__ThemeToggleController from "./ruby_ui/theme_toggle_controller" +application.register("ruby-ui--theme-toggle", RubyUi__ThemeToggleController) + +import UrlController from "./url_controller" application.register("url", UrlController) diff --git a/app/javascript/controllers/ruby_ui/theme_toggle_controller.js b/app/javascript/controllers/ruby_ui/theme_toggle_controller.js new file mode 100644 index 0000000..01b3b24 --- /dev/null +++ b/app/javascript/controllers/ruby_ui/theme_toggle_controller.js @@ -0,0 +1,30 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + initialize() { + this.setTheme() + } + + setTheme() { + // On page load or when changing themes, best to add inline in `head` to avoid FOUC + if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { + document.documentElement.classList.add('dark') + document.documentElement.classList.remove('light') + } else { + document.documentElement.classList.remove('dark') + document.documentElement.classList.add('light') + } + } + + setLightTheme() { + // Whenever the user explicitly chooses light mode + localStorage.theme = 'light' + this.setTheme() + } + + setDarkTheme() { + // Whenever the user explicitly chooses dark mode + localStorage.theme = 'dark' + this.setTheme() + } +} diff --git a/app/views/home/anon.rb b/app/views/home/anon.rb index 9f2fd4f..c5b027f 100644 --- a/app/views/home/anon.rb +++ b/app/views/home/anon.rb @@ -2,7 +2,7 @@ class Views::Home::Anon < Views::Base def view_template - Layout(title: "Home") do + ShellLayout(title: "Home") do h1 { "This is the homepage" } Button { "Hello" } end diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index f7adb4e..9aff196 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -1,4 +1,7 @@ en: + components: + page_header: + about: About layouts: navbar: label: main navigation From 25f39bccea07366dbdfca63b9c67cab98c7e176f Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Fri, 9 May 2025 15:36:51 +0100 Subject: [PATCH 07/44] Add all the links to the top bar --- app/components/page_header.rb | 64 ++++++++++++++-------------------- app/components/shell_layout.rb | 2 +- config/locales/views.en.yml | 1 + 3 files changed, 28 insertions(+), 39 deletions(-) diff --git a/app/components/page_header.rb b/app/components/page_header.rb index 6e5ba0c..7850ef1 100644 --- a/app/components/page_header.rb +++ b/app/components/page_header.rb @@ -1,15 +1,22 @@ # frozen_string_literal: true class Components::PageHeader < Components::Base - # From https://www.hyperui.dev/components/marketing/headers + # From https://github.com/ruby-ui/web/blob/main/app/components/shared/navbar.rb + register_value_helper :user_signed_in? + register_output_helper :current_user def view_template - header(class: 'bg-secondary text-secondary-foreground') do - div(class: container_classes + %w[flex h-16 items-center gap-8]) do - render_logo - div(class: 'flex flex-1 items-center justify-end md:justify-between') do - render_links - render_actions + header(class: 'supports-backdrop-blur:bg-background/80 sticky top-0 z-50 w-full border-b bg-background/80 backdrop-blur-2xl backdrop-saturate-200') do + div(class: 'px-2 sm:px-4 sm:container flex h-14 items-center justify-between') do + div(class: 'mr-4 flex items-center') do + render_logo + nav_link(page_path('about')) { t('.about') } + end + div(class: 'flex items-center gap-x-2 md:divide-x') do + div(class: 'flex items-center') do + render_user_links + render_theme_toggle + end end end end @@ -17,49 +24,30 @@ def view_template protected - def links - [ - {text: t('.about'), href: page_path('about')}, - ] + def nav_link(href, **args, &) + Link(**mix(args, {href: href, variant: :ghost, class: 'hidden md:inline-block'}), &) end def render_logo - a(class: 'flex justify-start items-center gap-2', href: root_path) do + a(class: 'mr-6 flex items-center space-x-2', href: root_path) do inline_svg_tag('logo.svg', class: 'h-8', size: "100%") span(class: 'text-lg font-bold') { t('product_name') } end end - def render_links - nav(aria_label: 'Global', class: 'hidden md:block') do - ul(class: 'flex items-center gap-6 text-sm') do - links.each do |l| - Link(class: 'text-current', href: l[:href]) { l[:text] } - end - end - end - end - - def render_actions - div(class: 'flex items-center gap-4') do - # Links that are visible on mobile and desktop - div(class: 'sm:flex items-center sm:gap-4') do - Link(variant: :primary, href: new_user_session_path) { "Login" } - end - # Links that are hidden behind the burger on mobile - div(class: 'hidden sm:flex') do - Link(variant: :primary, href: new_user_registration_path) { "Sign up" } - render_theme_toggle - end - # The burger itself - div(class: 'block md:hidden') do - Button { icon('menu', class: 'fill-current') } - end + def render_user_links + if user_signed_in? + nav_link(admin_root_path) { t('.admin') } if current_user.role_admin? + nav_link(edit_user_registration_path) { t('devise.registrations.edit.title') } + nav_link(destroy_user_session_path, data: {turbo_method: :delete}) { t('devise.sessions.destroy.title') } + else + nav_link(new_user_session_path) { t('devise.sessions.new.title') } + nav_link(new_user_registration_path) { t('devise.registrations.new.title') } end end def render_theme_toggle - ThemeToggle do |toggle| + ThemeToggle(class: 'hidden md:block') do |toggle| SetLightMode(class: 'flex items-center') do Button(variant: :ghost, icon: true) do icon('white-balance-sunny', class: 'fill-current p-2') diff --git a/app/components/shell_layout.rb b/app/components/shell_layout.rb index d1c714f..76a2969 100644 --- a/app/components/shell_layout.rb +++ b/app/components/shell_layout.rb @@ -5,7 +5,7 @@ def view_template super do PageHeader() main do - div(class: container_classes + %w[py-8 lg:py-12]) do + div(class: 'px-2 sm:px-4 sm:container py-4 sm:py-8') do yield end end diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index 9aff196..ba1351d 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -2,6 +2,7 @@ en: components: page_header: about: About + admin: Admin layouts: navbar: label: main navigation From e793591026f370bf132a27f931d423363a6f989d Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Fri, 9 May 2025 16:54:33 +0100 Subject: [PATCH 08/44] Lots of weirdness with Turbo but the top bar is functioning now --- app/components/page_header.rb | 61 +++++---- .../ruby_ui/dropdown_menu/dropdown_menu.rb | 26 ++++ .../dropdown_menu/dropdown_menu_content.rb | 22 ++++ .../dropdown_menu/dropdown_menu_item.rb | 28 ++++ .../dropdown_menu/dropdown_menu_label.rb | 17 +++ .../dropdown_menu/dropdown_menu_separator.rb | 19 +++ .../dropdown_menu/dropdown_menu_trigger.rb | 18 +++ app/components/shell_layout.rb | 15 ++- app/javascript/controllers/index.js | 3 + .../ruby_ui/dropdown_menu_controller.js | 120 ++++++++++++++++++ config/locales/views.en.yml | 2 +- package.json | 1 + yarn.lock | 20 +++ 13 files changed, 326 insertions(+), 26 deletions(-) create mode 100644 app/components/ruby_ui/dropdown_menu/dropdown_menu.rb create mode 100644 app/components/ruby_ui/dropdown_menu/dropdown_menu_content.rb create mode 100644 app/components/ruby_ui/dropdown_menu/dropdown_menu_item.rb create mode 100644 app/components/ruby_ui/dropdown_menu/dropdown_menu_label.rb create mode 100644 app/components/ruby_ui/dropdown_menu/dropdown_menu_separator.rb create mode 100644 app/components/ruby_ui/dropdown_menu/dropdown_menu_trigger.rb create mode 100644 app/javascript/controllers/ruby_ui/dropdown_menu_controller.js diff --git a/app/components/page_header.rb b/app/components/page_header.rb index 7850ef1..0178a20 100644 --- a/app/components/page_header.rb +++ b/app/components/page_header.rb @@ -2,61 +2,74 @@ class Components::PageHeader < Components::Base # From https://github.com/ruby-ui/web/blob/main/app/components/shared/navbar.rb - register_value_helper :user_signed_in? - register_output_helper :current_user + + def initialize + @main_links = [] + @action_links = [] + end - def view_template + def view_template(&) + vanish(&) header(class: 'supports-backdrop-blur:bg-background/80 sticky top-0 z-50 w-full border-b bg-background/80 backdrop-blur-2xl backdrop-saturate-200') do div(class: 'px-2 sm:px-4 sm:container flex h-14 items-center justify-between') do div(class: 'mr-4 flex items-center') do render_logo - nav_link(page_path('about')) { t('.about') } + @main_links.each { render_link(it) } end - div(class: 'flex items-center gap-x-2 md:divide-x') do + div(class: 'flex items-center gap-x-2 divide-x md:divide-none') do div(class: 'flex items-center') do - render_user_links + @action_links.each { render_link(it) } render_theme_toggle end + render_dropdown_menu end end end end - protected + def main_link(href, **args, &content) + @main_links << {href:, content:, **args} + end - def nav_link(href, **args, &) - Link(**mix(args, {href: href, variant: :ghost, class: 'hidden md:inline-block'}), &) + def action_link(href, **args, &content) + @action_links << {href:, content:, **args} end + protected + def render_logo a(class: 'mr-6 flex items-center space-x-2', href: root_path) do inline_svg_tag('logo.svg', class: 'h-8', size: "100%") span(class: 'text-lg font-bold') { t('product_name') } end end + + def render_link(link) + Link(**mix(link.except(:content), {variant: :ghost, class: 'hidden md:inline-block'}), &link[:content]) + end - def render_user_links - if user_signed_in? - nav_link(admin_root_path) { t('.admin') } if current_user.role_admin? - nav_link(edit_user_registration_path) { t('devise.registrations.edit.title') } - nav_link(destroy_user_session_path, data: {turbo_method: :delete}) { t('devise.sessions.destroy.title') } - else - nav_link(new_user_session_path) { t('devise.sessions.new.title') } - nav_link(new_user_registration_path) { t('devise.registrations.new.title') } + def render_dropdown_menu + DropdownMenu(class: 'md:hidden', options: {placement: 'bottom-end'}) do + DropdownMenuTrigger(class: 'flex items-center') do + Button(variant: :ghost, icon: true) { icon('menu', class: 'fill-current') } + end + DropdownMenuContent do + (@main_links + @action_links).each { render_dropdown_link(it) } + end end end + def render_dropdown_link(link) + DropdownMenuItem(**link.except(:content), &link[:content]) + end + def render_theme_toggle - ThemeToggle(class: 'hidden md:block') do |toggle| + ThemeToggle do |toggle| SetLightMode(class: 'flex items-center') do - Button(variant: :ghost, icon: true) do - icon('white-balance-sunny', class: 'fill-current p-2') - end + Button(variant: :ghost, icon: true) { icon('white-balance-sunny', class: 'fill-current p-2') } end SetDarkMode(class: 'dark:flex items-center') do - Button(variant: :ghost, icon: true) do - icon('weather-night', class: 'fill-current p-2') - end + Button(variant: :ghost, icon: true) { icon('weather-night', class: 'fill-current p-2') } end end end diff --git a/app/components/ruby_ui/dropdown_menu/dropdown_menu.rb b/app/components/ruby_ui/dropdown_menu/dropdown_menu.rb new file mode 100644 index 0000000..0a7564a --- /dev/null +++ b/app/components/ruby_ui/dropdown_menu/dropdown_menu.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module RubyUI + class DropdownMenu < Base + def initialize(options: {}, **attrs) + @options = options + super(**attrs) + end + + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + data: { + controller: "ruby-ui--dropdown-menu", + action: "click@window->ruby-ui--dropdown-menu#onClickOutside", + ruby_ui__dropdown_menu_options_value: @options.to_json + } + } + end + end +end diff --git a/app/components/ruby_ui/dropdown_menu/dropdown_menu_content.rb b/app/components/ruby_ui/dropdown_menu/dropdown_menu_content.rb new file mode 100644 index 0000000..44b8e16 --- /dev/null +++ b/app/components/ruby_ui/dropdown_menu/dropdown_menu_content.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module RubyUI + class DropdownMenuContent < Base + def view_template(&block) + div(data: {ruby_ui__dropdown_menu_target: "content"}, class: "hidden", style: "width: max-content; position: absolute; top: 0; left: 0;") do + div(**attrs, &block) + end + end + + private + + def default_attrs + { + data: { + state: :open + }, + class: "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-background p-1 text-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 w-56" + } + end + end +end diff --git a/app/components/ruby_ui/dropdown_menu/dropdown_menu_item.rb b/app/components/ruby_ui/dropdown_menu/dropdown_menu_item.rb new file mode 100644 index 0000000..9b401ea --- /dev/null +++ b/app/components/ruby_ui/dropdown_menu/dropdown_menu_item.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module RubyUI + class DropdownMenuItem < Base + def initialize(href: "#", **attrs) + @href = href + super(**attrs) + end + + def view_template(&) + a(**attrs, &) + end + + private + + def default_attrs + { + href: @href, + role: "menuitem", + class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + data_action: "click->ruby-ui--dropdown-menu#close", + data_ruby_ui__dropdown_menu_target: "menuItem", + tabindex: "-1", + data_orientation: "vertical" + } + end + end +end diff --git a/app/components/ruby_ui/dropdown_menu/dropdown_menu_label.rb b/app/components/ruby_ui/dropdown_menu/dropdown_menu_label.rb new file mode 100644 index 0000000..1a29ee7 --- /dev/null +++ b/app/components/ruby_ui/dropdown_menu/dropdown_menu_label.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class DropdownMenuLabel < Base + def view_template(&) + h3(**attrs, &) + end + + private + + def default_attrs + { + class: "px-2 py-1.5 text-sm font-semibold" + } + end + end +end diff --git a/app/components/ruby_ui/dropdown_menu/dropdown_menu_separator.rb b/app/components/ruby_ui/dropdown_menu/dropdown_menu_separator.rb new file mode 100644 index 0000000..030064b --- /dev/null +++ b/app/components/ruby_ui/dropdown_menu/dropdown_menu_separator.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module RubyUI + class DropdownMenuSeparator < Base + def view_template + div(**attrs) + end + + private + + def default_attrs + { + role: "separator", + aria_orientation: "horizontal", + class: "-mx-1 my-1 h-px bg-muted" + } + end + end +end diff --git a/app/components/ruby_ui/dropdown_menu/dropdown_menu_trigger.rb b/app/components/ruby_ui/dropdown_menu/dropdown_menu_trigger.rb new file mode 100644 index 0000000..0e7aab6 --- /dev/null +++ b/app/components/ruby_ui/dropdown_menu/dropdown_menu_trigger.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module RubyUI + class DropdownMenuTrigger < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + data: {ruby_ui__dropdown_menu_target: "trigger", action: "click->ruby-ui--dropdown-menu#toggle"}, + class: "inline-block" + } + end + end +end diff --git a/app/components/shell_layout.rb b/app/components/shell_layout.rb index 76a2969..ba31ca9 100644 --- a/app/components/shell_layout.rb +++ b/app/components/shell_layout.rb @@ -1,9 +1,22 @@ # frozen_string_literal: true class Components::ShellLayout < Components::Layout + register_value_helper :user_signed_in? + register_value_helper :current_user + def view_template super do - PageHeader() + PageHeader do |header| + header.main_link(page_path('about')) { t('.about') } + if user_signed_in? + header.action_link(admin_root_path) { t('.admin') } if current_user.role_admin? + header.action_link(edit_user_registration_path) { t('devise.registrations.edit.title') } + header.action_link(destroy_user_session_path, data: {turbo_method: :delete}) { t('devise.sessions.destroy.title') } + else + header.action_link(new_user_session_path) { t('devise.sessions.new.title') } + header.action_link(new_user_registration_path) { t('devise.registrations.new.title') } + end + end main do div(class: 'px-2 sm:px-4 sm:container py-4 sm:py-8') do yield diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index f4a168f..e3b3d79 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -37,6 +37,9 @@ application.register("public-profile", PublicProfileController) import RecorderController from "./recorder_controller" application.register("recorder", RecorderController) +import RubyUi__DropdownMenuController from "./ruby_ui/dropdown_menu_controller" +application.register("ruby-ui--dropdown-menu", RubyUi__DropdownMenuController) + import RubyUi__ThemeToggleController from "./ruby_ui/theme_toggle_controller" application.register("ruby-ui--theme-toggle", RubyUi__ThemeToggleController) diff --git a/app/javascript/controllers/ruby_ui/dropdown_menu_controller.js b/app/javascript/controllers/ruby_ui/dropdown_menu_controller.js new file mode 100644 index 0000000..266c0fe --- /dev/null +++ b/app/javascript/controllers/ruby_ui/dropdown_menu_controller.js @@ -0,0 +1,120 @@ +import { Controller } from "@hotwired/stimulus"; +import { computePosition, flip, shift, offset } from "@floating-ui/dom"; + +export default class extends Controller { + static targets = ["trigger", "content", "menuItem"]; + static values = { + open: { + type: Boolean, + default: false, + }, + options: { + type: Object, + default: {}, + }, + } + + connect() { + this.boundHandleKeydown = this.#handleKeydown.bind(this); // Bind the function so we can remove it later + this.selectedIndex = -1; + } + + #computeTooltip() { + computePosition(this.triggerTarget, this.contentTarget, { + placement: this.optionsValue.placement || "top", + middleware: [flip(), shift(), offset(8)], + }).then(({ x, y }) => { + Object.assign(this.contentTarget.style, { + left: `${x}px`, + top: `${y}px`, + }); + }); + } + + onClickOutside(event) { + if (!this.openValue) return; + if (this.element.contains(event.target)) return; + + event.preventDefault(); + this.close(); + } + + toggle() { + this.contentTarget.classList.contains("hidden") ? this.#open() : this.close(); + } + + #open() { + this.openValue = true; + this.#deselectAll(); + this.#addEventListeners(); + this.#computeTooltip() + this.contentTarget.classList.remove("hidden"); + } + + close() { + this.openValue = false; + this.#removeEventListeners(); + this.contentTarget.classList.add("hidden"); + } + + #handleKeydown(e) { + // return if no menu items (one line fix for) + if (this.menuItemTargets.length === 0) { return; } + + if (e.key === 'ArrowDown') { + e.preventDefault(); + this.#updateSelectedItem(1); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + this.#updateSelectedItem(-1); + } else if (e.key === 'Enter' && this.selectedIndex !== -1) { + e.preventDefault(); + this.menuItemTargets[this.selectedIndex].click(); + } + } + + #updateSelectedItem(direction) { + // Check if any of the menuItemTargets have aria-selected="true" and set the selectedIndex to that index + this.menuItemTargets.forEach((item, index) => { + if (item.getAttribute('aria-selected') === 'true') { + this.selectedIndex = index; + } + }); + + if (this.selectedIndex >= 0) { + this.#toggleAriaSelected(this.menuItemTargets[this.selectedIndex], false); + } + + this.selectedIndex += direction; + + if (this.selectedIndex < 0) { + this.selectedIndex = this.menuItemTargets.length - 1; + } else if (this.selectedIndex >= this.menuItemTargets.length) { + this.selectedIndex = 0; + } + + this.#toggleAriaSelected(this.menuItemTargets[this.selectedIndex], true); + } + + #toggleAriaSelected(element, isSelected) { + // Add or remove attribute + if (isSelected) { + element.setAttribute('aria-selected', 'true'); + } else { + element.removeAttribute('aria-selected'); + } + } + + #deselectAll() { + this.menuItemTargets.forEach(item => this.#toggleAriaSelected(item, false)); + this.selectedIndex = -1; + } + + #addEventListeners() { + document.addEventListener('keydown', this.boundHandleKeydown); + } + + #removeEventListeners() { + document.removeEventListener('keydown', this.boundHandleKeydown); + } +} diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index ba1351d..238d2d1 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -1,6 +1,6 @@ en: components: - page_header: + shell_layout: about: About admin: Admin layouts: diff --git a/package.json b/package.json index d09a028..5e390a6 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@activeadmin/activeadmin": "4.0.0-beta15", + "@floating-ui/dom": "^1.7.0", "@hotwired/stimulus": "^3.1.0", "@hotwired/turbo": "^8.0.13", "@kollegorna/cocoon-vanilla-js": "^1.0.7", diff --git a/yarn.lock b/yarn.lock index 9505fdb..5fcba28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -145,6 +145,26 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz#0b17ec8a70b2385827d52314c1253160a0b9bacc" integrity sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ== +"@floating-ui/core@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.0.tgz#1aff27a993ea1b254a586318c29c3b16ea0f4d0a" + integrity sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA== + dependencies: + "@floating-ui/utils" "^0.2.9" + +"@floating-ui/dom@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.0.tgz#f9f83ee4fee78ac23ad9e65b128fc11a27857532" + integrity sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg== + dependencies: + "@floating-ui/core" "^1.7.0" + "@floating-ui/utils" "^0.2.9" + +"@floating-ui/utils@^0.2.9": + version "0.2.9" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.9.tgz#50dea3616bc8191fb8e112283b49eaff03e78429" + integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg== + "@hotwired/stimulus@^3.1.0": version "3.2.2" resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.2.2.tgz#071aab59c600fed95b97939e605ff261a4251608" From 1a51d1622d78136d053c54df81139c73f17c1683 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Wed, 4 Jun 2025 11:38:41 +0100 Subject: [PATCH 09/44] Convert about page to phlex/tailwind --- Gemfile | 2 + Gemfile.lock | 11 ++++ app/assets/tailwind/application.css | 1 - app/components/layout.rb | 6 -- app/components/prose.rb | 11 ++++ app/controllers/application_controller.rb | 2 +- app/controllers/pages_controller.rb | 9 +++ app/helpers/pages_helper.rb | 2 + app/views/pages/about.html.haml | 39 ------------ app/views/pages/about.rb | 73 +++++++++++++++++++++++ config/routes.rb | 2 +- 11 files changed, 110 insertions(+), 48 deletions(-) create mode 100644 app/components/prose.rb create mode 100644 app/controllers/pages_controller.rb create mode 100644 app/helpers/pages_helper.rb delete mode 100644 app/views/pages/about.html.haml create mode 100644 app/views/pages/about.rb diff --git a/Gemfile b/Gemfile index d3beea2..c22fecb 100644 --- a/Gemfile +++ b/Gemfile @@ -50,6 +50,7 @@ gem 'image_processing', '~> 1.11' gem "importmap-rails", "~> 2.1" gem "inline_svg", "~> 1.10" gem "literal", "~> 1.7" +gem "markdown_views", "~> 3.1" gem 'meta-tags', '~> 2.13' gem 'mini_magick', '~> 5.2' gem 'ostruct', '~> 0.6.1' # dependency of gaffe @@ -112,3 +113,4 @@ group :production do gem 'aws-sdk-s3', '~> 1.183' gem 'rails_12factor', '~> 0.0.3' end + diff --git a/Gemfile.lock b/Gemfile.lock index 2c0e83b..2217737 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -166,6 +166,8 @@ GEM cocoon (1.2.15) coderay (1.1.3) colorize (1.1.0) + commonmarker (2.3.0) + rb_sys (~> 0.9) concurrent-ruby (1.3.5) connection_pool (2.5.3) crack (1.0.0) @@ -333,6 +335,11 @@ GEM net-pop net-smtp marcel (1.0.4) + markdown_views (3.1.0) + commonmarker (>= 1.1, < 3.0) + rails (>= 6.1, < 8.1) + rouge (~> 4) + securerandom matrix (0.4.2) meta-tags (2.22.1) actionpack (>= 6.0.0, < 8.1) @@ -440,6 +447,7 @@ GEM thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) rake (13.2.1) + rake-compiler-dock (1.9.1) ransack (4.3.0) activerecord (>= 6.1.5) activesupport (>= 6.1.5) @@ -447,6 +455,8 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) + rb_sys (0.9.116) + rake-compiler-dock (= 1.9.1) rdoc (6.13.1) psych (>= 4.0.0) regexp_parser (2.10.0) @@ -611,6 +621,7 @@ DEPENDENCIES jsbundling-rails (~> 1.1) listen literal (~> 1.7) + markdown_views (~> 3.1) meta-tags (~> 2.13) mini_magick (~> 5.2) ostruct (~> 0.6.1) diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index 9612542..5f197d6 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -3,7 +3,6 @@ @plugin "@tailwindcss/forms"; @plugin "@tailwindcss/typography"; - @import "tw-animate-css"; @custom-variant dark (&:is(.dark *)); diff --git a/app/components/layout.rb b/app/components/layout.rb index 8564683..556e737 100644 --- a/app/components/layout.rb +++ b/app/components/layout.rb @@ -5,16 +5,10 @@ class Components::Layout < Components::Base register_output_helper :display_meta_tags - prop :title, String - def view_template doctype html(lang: 'en') do render_head - head do - title { @title } - end - body(class: 'bg-background text-foreground') do yield end diff --git a/app/components/prose.rb b/app/components/prose.rb new file mode 100644 index 0000000..c20eb02 --- /dev/null +++ b/app/components/prose.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Components::Prose < Components::Base + register_output_helper :markdown + + def view_template(&) + div(class: 'prose dark:prose-invert') do + markdown(&) + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 31664c0..0902096 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -16,7 +16,7 @@ def authenticate_admin! protected def framework_controller? - devise_controller? || kind_of?(ActiveAdmin::BaseController) || kind_of?(HighVoltage::PagesController) + devise_controller? || kind_of?(ActiveAdmin::BaseController) end def user_not_authorized diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb new file mode 100644 index 0000000..d01b708 --- /dev/null +++ b/app/controllers/pages_controller.rb @@ -0,0 +1,9 @@ +class PagesController < ApplicationController + skip_after_action :verify_authorized + + layout -> { Components::ShellLayout.new } + + def about + render Views::Pages::About + end +end diff --git a/app/helpers/pages_helper.rb b/app/helpers/pages_helper.rb new file mode 100644 index 0000000..2c057fd --- /dev/null +++ b/app/helpers/pages_helper.rb @@ -0,0 +1,2 @@ +module PagesHelper +end diff --git a/app/views/pages/about.html.haml b/app/views/pages/about.html.haml deleted file mode 100644 index f0ae6e8..0000000 --- a/app/views/pages/about.html.haml +++ /dev/null @@ -1,39 +0,0 @@ -= page_section(title: 'About name.pn', page_title: true) do - %p - Like most useful products, name.pn was born out of necessity. Developer Quinn #{link_to 'name.pn/quinn-daley', 'https://name.pn/quinn-daley'} is trans & non-binary and uses they/them pronouns and needed a nice way to share how to use those pronouns with business contacts. - %p - These days we do a lot of our communicating electronically. We can see a person's name a dozen times and - %ul - %li never learn how to pronounce it the way they want it to be pronounced, - %li not know what pronouns to use when talking about them in the third person, - %li want to address them formally but not know what title to use (or even if they want to be addressed by a title), - %li hear them being called by a shortened version of their name and not know if they actually like it. - %p name.pn was created to give each person a short URL that you can put in your email signature and your social profiles that gives people all the information they need to address you correctly, including a chance to hear you pronouncing your name in your own voice. - %p It's free and always will be (although we might add some cool additional paid features in the future for businesses). It's still early in development but it works and it's ready to use now. - - unless user_signed_in? - %p.has-text-centered= link_to 'Sign up for your free name.pn page', new_user_registration_path, class: 'button is-link is-outlined is-medium' -= page_section(title: 'Working in the open') do - %p name.pn is an open source product and we are working in the open. - %p We welcome contributions to our roadmap, including bug reports, feature requests and even code changes! Follow the links below for more information. - %p.is-flex.is-justify-content-center - = link_to 'https://github.com/fishpercolator/name.pn/issues', class: 'button is-link is-outlined is-medium can-wrap' do - %span.icon= icon 'bug' - %span Report a bug or make a feature request - %p.is-flex.is-justify-content-center - = link_to 'https://github.com/orgs/fishpercolator/projects/1', class: 'button is-link is-outlined is-medium can-wrap' do - %span.icon= icon 'map' - %span See what's coming up next on our roadmap - %p.is-flex.is-justify-content-center - = link_to 'https://github.com/fishpercolator/name.pn/blob/main/CONTRIBUTING.md', class: 'button is-link is-outlined is-medium can-wrap' do - %span.icon= icon 'wrench-outline' - %span Read about how you can contribute to name.pn - %p.is-flex.is-justify-content-center - = link_to 'https://github.com/fishpercolator/name.pn', class: 'button is-link is-outlined is-medium can-wrap' do - %span.icon= icon 'github' - %span Browse our source code on GitHub -= page_section(title: 'Get in touch') do - %p If you'd like to report an issue or feature request, please use #{link_to 'our issues board', 'https://github.com/fishpercolator/name.pn/issues'}. But to get in touch for any other reason, our email address is below. - %p.is-flex.is-justify-content-center - = link_to 'mailto:support@name.pn', class: 'button is-link is-outlined is-medium can-wrap' do - %span.icon= icon 'email' - %span support@name.pn diff --git a/app/views/pages/about.rb b/app/views/pages/about.rb new file mode 100644 index 0000000..a9a7d02 --- /dev/null +++ b/app/views/pages/about.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +class Views::Pages::About < Views::Base + def view_template + Prose do <<~MD + # About name.pn + + Like most useful products, name.pn was born out of necessity. Developer **Quinn** [name.pn/quinn-daley](https://name.pn/quinn-daley) is trans & non-binary and uses they/them pronouns and needed a nice way to share how to use those pronouns with business contacts. + + These days we do a lot of our communicating electronically. We can see a person's name a dozen times and + + * never learn how to **pronounce** it the way they want it to be pronounced, + * not know what **pronouns** to use when talking about them in the third person, + * want to **address them formally** but not know what title to use (or even if they want to be addressed by a title), + * hear them being called by a shortened version of their name and not know **if they actually like it**. + + name.pn was created to give each person a **short URL** that you can put in your email signature and your social profiles that gives people all the information they need to address you correctly, including a chance to hear you pronouncing your name in your own voice. + + It's free and always will be (although we might add some cool additional paid features in the future for businesses). It's still early in development but it works and it's ready to use now. + MD + end + div(class: 'py-5') do + cta(new_user_registration_path, variant: :primary) do + 'Sign up for your free name.pn page' + end + end + Prose do <<~MD + ## Working in the open + + name.pn is an open source product and we are working in the open. + + We welcome contributions to our roadmap, including bug reports, feature requests and even code changes! Follow the links below for more information. + MD + end + div(class: 'py-5') do + cta('https://github.com/fishpercolator/name.pn/issues', icon_name: 'bug') do + 'Report a bug or make a feature request' + end + cta('https://github.com/orgs/fishpercolator/projects/1', icon_name: 'map') do + "See what's coming up next on our roadmap" + end + cta('https://github.com/fishpercolator/name.pn/blob/main/CONTRIBUTING.md', icon_name: 'wrench-outline') do + 'Read about how you can contribute to name.pn' + end + cta('https://github.com/fishpercolator/name.pn', icon_name: 'github') do + 'Browse our source code on GitHub' + end + end + Prose do <<~MD + ## Get in touch + + If you'd like to report an issue or feature request, please use [our issues board](https://github.com/fishpercolator/name.pn/issues). But to get in touch for any other reason, our email address is below. + MD + end + div(class: 'py-5') do + cta('mailto:support@name.pn', icon_name: 'email') do + 'support@name.pn' + end + end + end + + private + + def cta(href, icon_name: nil, variant: :outline, &) + div(class: 'mb-2') do + Link(href:, variant:) do + icon(icon_name, class: 'w-4 h-4 -ml-1 mr-2 fill-current') if icon_name + span(&) + end + end + end + +end diff --git a/config/routes.rb b/config/routes.rb index 5726dbf..3f527f8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,7 +15,7 @@ get 'users/mailing_list', to: 'mailing_list#show' post 'users/mailing_list/subscribe', to: 'mailing_list#subscribe' post 'users/mailing_list/unsubscribe', to: 'mailing_list#unsubscribe' - get "/pages/*id" => 'high_voltage/pages#show', as: :page, format: false + get "/pages/about" => 'pages#about', as: :page resources :profile resources :clients, only: %i[create destroy] From 7f99057c7c581384ad8b11c4a039b9127353d76e Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Wed, 4 Jun 2025 12:27:29 +0100 Subject: [PATCH 10/44] Add the terms page --- app/controllers/pages_controller.rb | 7 +- app/views/pages/terms.rb | 267 ++++++++++++++++++++++++++ config/initializers/markdown_views.rb | 1 + config/routes.rb | 2 +- 4 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 app/views/pages/terms.rb create mode 100644 config/initializers/markdown_views.rb diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index d01b708..004092f 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -3,7 +3,10 @@ class PagesController < ApplicationController layout -> { Components::ShellLayout.new } - def about - render Views::Pages::About + def show + render Views::Pages.const_get(params[:id].camelize) + rescue NameError + # If const_get fails, raise a 404 + raise AbstractController::ActionNotFound end end diff --git a/app/views/pages/terms.rb b/app/views/pages/terms.rb new file mode 100644 index 0000000..314b028 --- /dev/null +++ b/app/views/pages/terms.rb @@ -0,0 +1,267 @@ +# frozen_string_literal: true + +class Views::Pages::Terms < Views::Base + def view_template + Prose do <<~MD + # Terms and conditions of use + + ## 1. Introduction + + 1.1 These terms and conditions shall govern your use of our website. + + 1.2 By using our website, you accept these terms and conditions in full; accordingly, if you disagree with these terms and conditions or any part of these terms and conditions, you must not use our website. + + 1.3 If you register with our website, submit any material to our website or use any of our website services, we will ask you to expressly agree to these terms and conditions. + + 1.4 You must be at least 15 years of age to use our website; by using our website or agreeing to these terms and conditions, you warrant and represent to us that you are at least 15 years of age. + + ## 2. Copyright notice + + 2.1 Copyright (c) 2021 Fish Percolator Limited. + + 2.2 Subject to the express provisions of these terms and conditions: + + * (a) we, together with our licensors and other contributors to our codebase, own and control all the copyright and other intellectual property rights in our website and the material on our website; + * (b) this program is free software: you can redistribute it and/or modify it under the terms of the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.en.html) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version; and + * (c) all other copyright and other intellectual property rights in our website and the material on our website are reserved. + + ## 3. Permission to use website + + 3.1 You may: + + * (a) view pages from our website in a web browser; + * (b) download pages from our website for caching in a web browser; + * (c) print pages from our website for your own personal and non-commercial use, providing that such printing is not systematic or excessive; + * (d) stream audio files from our website using the media player on our website; + * (e) use our website services by means of a web browser; and + * (f) share your own personal content on our website, + + subject to the other provisions of these terms and conditions. + + 3.2 Except as expressly permitted by Section 3.1 or the other provisions of these terms and conditions, you must not download any material from our website or save any such material to your computer. + + 3.3 You may only use our website for your own personal and business purposes; you must not use our website for any other purposes. + + 3.4 Except as expressly permitted by these terms and conditions, you must not edit or otherwise modify any material on our website. + + 3.5 Unless you own or control the relevant rights in the material, you must not: + + * (a) republish material from our website (including republication on another website); + * (b) sell, rent or sub-license material from our website; + * (c) show any material from our website in public; + * (d) exploit material from our website for a commercial purpose; or + * (e) redistribute material from our website. + + 3.6 Notwithstanding Section 3.5, you may redistribute our newsletter in print and electronic form to any person. + + 3.7 We reserve the right to suspend or restrict access to our website, to areas of our website and/or to functionality upon our website. We may, for example, suspend access to the website during server maintenance or when we update the website. You must not circumvent or bypass, or attempt to circumvent or bypass, any access restriction measures on the website. + + ## 4. Misuse of website + + 4.1 You must not: + + * (a) use our website in any way or take any action that causes, or may cause, damage to the website or impairment of the performance, availability, accessibility, integrity or security of the website; + * (b) use our website in any way that is unlawful, illegal, fraudulent or harmful, or in connection with any unlawful, illegal, fraudulent or harmful purpose or activity; + * (c) hack or otherwise tamper with our website; + * (d) probe, scan or test the vulnerability of our website without our permission; + * (e) circumvent any authentication or security systems or processes on or relating to our website; + * (f) use our website to copy, store, host, transmit, send, use, publish or distribute any material which consists of (or is linked to) any spyware, computer virus, Trojan horse, worm, keystroke logger, rootkit or other malicious computer software; + * (g) impose an unreasonably large load on our website resources (including bandwidth, storage capacity, processing capacity and automation using the provided API); + * (h) decrypt or decipher any communications sent by or to our website without our permission; + * (i) conduct any systematic or automated data collection activities (including without limitation scraping, data mining, data extraction and data harvesting) on or in relation to our website without our express written consent; + * (j) access or otherwise interact with our website using any robot, spider or other automated means, except by using the provided API, or for the purpose of search engine indexing; + * (l) violate the directives set out in the robots meta tag on each page of our website; + * (m) use data collected from our website for any direct marketing activity (including without limitation email marketing, SMS marketing, telemarketing and direct mailing); or + * (n) do anything that interferes with the normal use of our website. + + 4.2 You must not use data collected from our website to contact individuals, companies or other persons or entities, except where users have chosen to provide links to their contact details. + + 4.3 You must ensure that all the information you supply to us through our website, or in relation to our website, is true, accurate, current, complete and non-misleading. Your name is true if it is the one you use in your day-to-day life; you are not in violation of this clause by using a true name other than the one legally recognised in your jurisdiction. + + ## 5. Registration and accounts + + 5.1 You may register for an account with our website by completing and submitting the account registration form on our website. + + 5.2 You must not allow any other person to use your account to access the website. + + 5.3 You must notify us in writing immediately if you become aware of any unauthorised use of your account. + + 5.4 You must not use any other person's account to access the website, unless you have that person's express permission to do so. + + ## 6. User login details + + 6.1 If you register for an account with our website, you will be asked to choose a login email address and password. + + 6.2 You must not use your account or user ID for or in connection with the impersonation of any person. + + 6.3 You must keep your password confidential. + + 6.4 You must notify us in writing immediately if you become aware of any disclosure of your password. + + 6.5 You are responsible for any activity on our website arising out of any failure to keep your password confidential, and may be held liable for any losses arising out of such a failure. + + ## 7. Cancellation and suspension of account + + 7.1 We may: + + * (a) suspend your account; + * (b) cancel your account; and/or + * (c) edit your account details, + + at any time in our sole discretion with or without notice to you. + + 7.2 You may cancel your account on our website using your account control panel on the website. + + ## 8. Our rights to use your content + + 8.1 In these terms and conditions, "your content" means all works and materials (including without limitation text, graphics, images, audio material, video material, audio-visual material, scripts, software and files) that you submit to us or our website for storage or publication on, processing by, or transmission via, our website. + + 8.2 You grant to us a worldwide, non-exclusive, royalty-free licence to reproduce, store and publish your content on and in relation to this website and any successor website. + + 8.3 You grant to us the right to sub-license the rights licensed under Section 8.2. + + 8.4 You grant to us the right to bring an action for infringement of the rights licensed under Section 8.2. + + 8.5 You may edit your content to the extent permitted using the editing functionality made available on our website. + + 8.6 Without prejudice to our other rights under these terms and conditions, if you breach any provision of these terms and conditions in any way, or if we reasonably suspect that you have breached these terms and conditions in any way, we may delete, unpublish or edit any or all of your content. + + ## 9. Rules about your content + + 9.1 You warrant and represent that your content will comply with these terms and conditions. + + 9.2 Your content must not be illegal or unlawful, must not infringe any person's legal rights, and must not be capable of giving rise to legal action against any person (in each case in any jurisdiction and under any applicable law). + + 9.3 Your content, and the use of your content by us in accordance with these terms and conditions, must not: + + * (a) be libellous or maliciously false; + * (b) be obscene or indecent; + * (c) infringe any copyright, moral right, database right, trade mark right, design right, right in passing off or other intellectual property right; + * (d) infringe any right of confidence, right of privacy or right under data protection legislation; + * (e) constitute negligent advice or contain any negligent statement; + * (f) constitute an incitement to commit a crime, instructions for the commission of a crime or the promotion of criminal activity; + * (g) be in contempt of any court or in breach of any court order; + * (h) be in breach of racial or religious hatred or discrimination legislation; + * (i) be in breach of official secrets legislation; + * (j) be in breach of any contractual obligation owed to any person; + * (k) depict violence; + * (l) be pornographic or sexually explicit; + * (m) be untrue, false, inaccurate or misleading; + * (n) consist of or contain any instructions, advice or other information which may be acted upon and could, if acted upon, cause illness, injury or death, or any other loss or damage; + * (o) constitute spam; + * (p) be offensive, deceptive, fraudulent, threatening, abusive, harassing, anti-social, menacing, hateful, discriminatory or inflammatory; or + * (r) cause annoyance, inconvenience or needless anxiety to any person. + + ## 10. Limited warranties + + 10.1 We do not warrant or represent: + + * (a) the completeness or accuracy of the information published on our website; + * (b) that the material on the website is up to date; + * (c) that the website will operate without fault; or + * (d) that the website or any service on the website will remain available. + + 10.2 We reserve the right to discontinue or alter any or all of our website services, and to stop publishing our website, at any time in our sole discretion without notice or explanation; and save to the extent expressly provided otherwise in these terms and conditions, you will not be entitled to any compensation or other payment upon the discontinuance or alteration of any website services, or if we stop publishing the website. + + 10.3 To the maximum extent permitted by applicable law and subject to Section 11.1, we exclude all representations and warranties relating to the subject matter of these terms and conditions, our website and the use of our website. + + ## 11. Limitations and exclusions of liability + + 11.1 Nothing in these terms and conditions will: + + * (a) limit or exclude any liability for death or personal injury resulting from negligence; + * (b) limit or exclude any liability for fraud or fraudulent misrepresentation; + * (c) limit any liabilities in any way that is not permitted under applicable law; or + * (d) exclude any liabilities that may not be excluded under applicable law. + + 11.2 The limitations and exclusions of liability set out in this Section 11 and elsewhere in these terms and conditions: + + * (a) are subject to Section 11.1; and + * (b) govern all liabilities arising under these terms and conditions or relating to the subject matter of these terms and conditions, including liabilities arising in contract, in tort (including negligence) and for breach of statutory duty, except to the extent expressly provided otherwise in these terms and conditions. + + 11.3 To the extent that our website and the information and services on our website are provided free of charge, we will not be liable for any loss or damage of any nature. + + 11.4 We will not be liable to you in respect of any losses arising out of any event or events beyond our reasonable control. + + 11.5 We will not be liable to you in respect of any business losses, including (without limitation) loss of or damage to profits, income, revenue, use, production, anticipated savings, business, contracts, commercial opportunities or goodwill. + + 11.6 We will not be liable to you in respect of any loss or corruption of any data, database or software. + + 11.7 We will not be liable to you in respect of any special, indirect or consequential loss or damage. + + 11.8 You accept that we have an interest in limiting the personal liability of our officers and employees and, having regard to that interest, you acknowledge that we are a limited liability entity; you agree that you will not bring any claim personally against our officers or employees in respect of any losses you suffer in connection with the website or these terms and conditions (this will not, of course, limit or exclude the liability of the limited liability entity itself for the acts and omissions of our officers and employees). + + ## 12. Breaches of these terms and conditions + + 12.1 Without prejudice to our other rights under these terms and conditions, if you breach these terms and conditions in any way, or if we reasonably suspect that you have breached these terms and conditions in any way, we may: + + * (a) send you one or more formal warnings; + * (b) temporarily suspend your access to our website; + * (c) permanently prohibit you from accessing our website; + * (d) block computers using your IP address from accessing our website; + * (e) contact any or all of your internet service providers and request that they block your access to our website; + * (f) commence legal action against you, whether for breach of contract or otherwise; and/or + * (g) suspend or delete your account on our website. + + 12.2 Where we suspend or prohibit or block your access to our website or a part of our website, you must not take any action to circumvent such suspension or prohibition or blocking (including without limitation creating and/or using a different account). + + ## 13. Variation + + 13.1 We may revise these terms and conditions from time to time. + + 13.2 The revised terms and conditions shall apply to the use of our website from the date of publication of the revised terms and conditions on the website, and you hereby waive any right you may otherwise have to be notified of, or to consent to, revisions of these terms and conditions. + + 13.3 If you have given your express agreement to these terms and conditions, we will ask for your express agreement to any revision of these terms and conditions; and if you do not give your express agreement to the revised terms and conditions within such period as we may specify, we will disable or delete your account on the website, and you must stop using the website. + + ## 14. Assignment + + 14.1 You hereby agree that we may assign, transfer, sub-contract or otherwise deal with our rights and/or obligations under these terms and conditions. + + 14.2 You may not without our prior written consent assign, transfer, sub-contract or otherwise deal with any of your rights and/or obligations under these terms and conditions. + + ## 15. Severability + + 15.1 If a provision of these terms and conditions is determined by any court or other competent authority to be unlawful and/or unenforceable, the other provisions will continue in effect. + + 15.2 If any unlawful and/or unenforceable provision of these terms and conditions would be lawful or enforceable if part of it were deleted, that part will be deemed to be deleted, and the rest of the provision will continue in effect. + + ## 16. Third party rights + + 16.1 A contract under these terms and conditions is for our benefit and your benefit, and is not intended to benefit or be enforceable by any third party. + + 16.2 The exercise of the parties' rights under a contract under these terms and conditions is not subject to the consent of any third party. + + ## 17. Entire agreement + + 17.1 Subject to Section 11.1, these terms and conditions, together with [our privacy and cookies policy](#{page_path 'privacy'}), shall constitute the entire agreement between you and us in relation to your use of our website and shall supersede all previous agreements between you and us in relation to your use of our website. + + ## 18. Law and jurisdiction + + 18.1 These terms and conditions shall be governed by and construed in accordance with English law. + + 18.2 Any disputes relating to these terms and conditions shall be subject to the exclusive jurisdiction of the courts of England. + + ## 19. Statutory and regulatory disclosures + + 19.1 We are registered with the [Information Commissioner's Office](https://ico.org.uk/) and our registration number is ZB138365. + + 19.2 We subscribe to the Contributor Covenant Code of Conduct, which can be consulted electronically at [contributor-covenant.org](https://www.contributor-covenant.org/). + + 19.3 Our VAT number is GB 218804800. + + ## 20. Our details + + 20.1 This website is owned and operated by [Fish Percolator Limited](https://www.fishpercolator.co.uk/). + + 20.2 We are registered in England and Wales under registration number 09617660, and our registered office is our principal place of business. + + 20.3 Our principal place of business is at International House, 14 King Street, Leeds LS1 2HL, United Kingdom. + + 20.4 You can contact us: + + * (a) by post, to the postal address given above; + * (b) by email, using support@name.pn. + MD + end + end +end diff --git a/config/initializers/markdown_views.rb b/config/initializers/markdown_views.rb new file mode 100644 index 0000000..4cf4d04 --- /dev/null +++ b/config/initializers/markdown_views.rb @@ -0,0 +1 @@ +MarkdownViews.extensions.merge! header_ids: "" diff --git a/config/routes.rb b/config/routes.rb index 3f527f8..301cf58 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,10 +15,10 @@ get 'users/mailing_list', to: 'mailing_list#show' post 'users/mailing_list/subscribe', to: 'mailing_list#subscribe' post 'users/mailing_list/unsubscribe', to: 'mailing_list#unsubscribe' - get "/pages/about" => 'pages#about', as: :page resources :profile resources :clients, only: %i[create destroy] + resources :pages, only: %i[show] namespace :api, defaults: {format: :json} do namespace :v1 do From be09a2df315f8f1923a972aa61cca45459b1c84d Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Wed, 4 Jun 2025 12:28:29 +0100 Subject: [PATCH 11/44] Add the current year to copyright --- app/views/pages/terms.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/pages/terms.rb b/app/views/pages/terms.rb index 314b028..1b87314 100644 --- a/app/views/pages/terms.rb +++ b/app/views/pages/terms.rb @@ -17,7 +17,7 @@ def view_template ## 2. Copyright notice - 2.1 Copyright (c) 2021 Fish Percolator Limited. + 2.1 Copyright (c) 2021-#{Date.today.year} Fish Percolator Limited. 2.2 Subject to the express provisions of these terms and conditions: From c51ffbcb3695fe209e6373b0ff32d409f5d630ff Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Wed, 4 Jun 2025 17:17:38 +0100 Subject: [PATCH 12/44] Ditch markdown_views in lieu of calling commonmarker directly --- Gemfile | 3 +- Gemfile.lock | 7 +- app/components/layout.rb | 6 +- app/components/prose.rb | 20 ++- app/components/shell_layout.rb | 4 + app/controllers/pages_controller.rb | 6 +- app/views/base.rb | 6 + app/views/pages/about.rb | 11 +- app/views/pages/privacy.rb | 245 ++++++++++++++++++++++++++ app/views/pages/terms.rb | 5 +- config/initializers/markdown_views.rb | 1 - 11 files changed, 288 insertions(+), 26 deletions(-) create mode 100644 app/views/pages/privacy.rb delete mode 100644 config/initializers/markdown_views.rb diff --git a/Gemfile b/Gemfile index c22fecb..81d5017 100644 --- a/Gemfile +++ b/Gemfile @@ -37,6 +37,7 @@ gem 'acts_as_list', '~> 1.0' gem 'amazing_print', '~> 1.3' gem 'blueprinter', '~> 1.0' gem 'cocoon', '~> 1.2' +gem "commonmarker", "~> 2.3" gem 'datadog', '~> 2.2', require: 'datadog/auto_instrument' gem 'devise', '~> 4.7' gem 'devise-jwt', '~> 0.12.0' @@ -50,7 +51,6 @@ gem 'image_processing', '~> 1.11' gem "importmap-rails", "~> 2.1" gem "inline_svg", "~> 1.10" gem "literal", "~> 1.7" -gem "markdown_views", "~> 3.1" gem 'meta-tags', '~> 2.13' gem 'mini_magick', '~> 5.2' gem 'ostruct', '~> 0.6.1' # dependency of gaffe @@ -113,4 +113,3 @@ group :production do gem 'aws-sdk-s3', '~> 1.183' gem 'rails_12factor', '~> 0.0.3' end - diff --git a/Gemfile.lock b/Gemfile.lock index 2217737..7080d38 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -335,11 +335,6 @@ GEM net-pop net-smtp marcel (1.0.4) - markdown_views (3.1.0) - commonmarker (>= 1.1, < 3.0) - rails (>= 6.1, < 8.1) - rouge (~> 4) - securerandom matrix (0.4.2) meta-tags (2.22.1) actionpack (>= 6.0.0, < 8.1) @@ -598,6 +593,7 @@ DEPENDENCIES byebug capybara-screenshot cocoon (~> 1.2) + commonmarker (~> 2.3) cssbundling-rails (~> 1.1) cuprite database_cleaner @@ -621,7 +617,6 @@ DEPENDENCIES jsbundling-rails (~> 1.1) listen literal (~> 1.7) - markdown_views (~> 3.1) meta-tags (~> 2.13) mini_magick (~> 5.2) ostruct (~> 0.6.1) diff --git a/app/components/layout.rb b/app/components/layout.rb index 556e737..d430177 100644 --- a/app/components/layout.rb +++ b/app/components/layout.rb @@ -7,7 +7,7 @@ class Components::Layout < Components::Base def view_template doctype - html(lang: 'en') do + html(lang: 'en', class: html_class) do render_head body(class: 'bg-background text-foreground') do yield @@ -17,6 +17,10 @@ def view_template protected + def html_class + nil + end + def render_head head do display_meta_tags default_meta_tags diff --git a/app/components/prose.rb b/app/components/prose.rb index c20eb02..702044f 100644 --- a/app/components/prose.rb +++ b/app/components/prose.rb @@ -1,11 +1,25 @@ # frozen_string_literal: true class Components::Prose < Components::Base - register_output_helper :markdown + CM_OPTIONS = { + render: { + unsafe: true, + }, + extension: { + description_lists: true, + header_ids: "", + table: true, + } + } - def view_template(&) + def initialize(md) + @md = md + end + + def view_template + rendered = Commonmarker.to_html(@md, options: CM_OPTIONS) div(class: 'prose dark:prose-invert') do - markdown(&) + raw safe(rendered) end end end diff --git a/app/components/shell_layout.rb b/app/components/shell_layout.rb index ba31ca9..d853c19 100644 --- a/app/components/shell_layout.rb +++ b/app/components/shell_layout.rb @@ -24,4 +24,8 @@ def view_template end end end + + def html_class + 'scroll-pt-15' + end end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 004092f..352bd8d 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -5,8 +5,8 @@ class PagesController < ApplicationController def show render Views::Pages.const_get(params[:id].camelize) - rescue NameError - # If const_get fails, raise a 404 - raise AbstractController::ActionNotFound + # rescue NameError + # # If const_get fails, raise a 404 + # raise AbstractController::ActionNotFound end end diff --git a/app/views/base.rb b/app/views/base.rb index 0e4f1b3..eb9b612 100644 --- a/app/views/base.rb +++ b/app/views/base.rb @@ -6,4 +6,10 @@ class Views::Base < Components::Base # By default, it inherits from `Components::Base`, but you # can change that to `Phlex::HTML` if you want to keep views and # components independent. + + # Safe version of the equivalent helper in MetaTags + def title(title = nil) + view_context.set_meta_tags(title:) unless title.nil? + view_context.meta_tags[:title] + end end diff --git a/app/views/pages/about.rb b/app/views/pages/about.rb index a9a7d02..1e67a2b 100644 --- a/app/views/pages/about.rb +++ b/app/views/pages/about.rb @@ -2,8 +2,8 @@ class Views::Pages::About < Views::Base def view_template - Prose do <<~MD - # About name.pn + Prose <<~MD + # #{title 'About name.pn'} Like most useful products, name.pn was born out of necessity. Developer **Quinn** [name.pn/quinn-daley](https://name.pn/quinn-daley) is trans & non-binary and uses they/them pronouns and needed a nice way to share how to use those pronouns with business contacts. @@ -18,20 +18,18 @@ def view_template It's free and always will be (although we might add some cool additional paid features in the future for businesses). It's still early in development but it works and it's ready to use now. MD - end div(class: 'py-5') do cta(new_user_registration_path, variant: :primary) do 'Sign up for your free name.pn page' end end - Prose do <<~MD + Prose <<~MD ## Working in the open name.pn is an open source product and we are working in the open. We welcome contributions to our roadmap, including bug reports, feature requests and even code changes! Follow the links below for more information. MD - end div(class: 'py-5') do cta('https://github.com/fishpercolator/name.pn/issues', icon_name: 'bug') do 'Report a bug or make a feature request' @@ -46,12 +44,11 @@ def view_template 'Browse our source code on GitHub' end end - Prose do <<~MD + Prose <<~MD ## Get in touch If you'd like to report an issue or feature request, please use [our issues board](https://github.com/fishpercolator/name.pn/issues). But to get in touch for any other reason, our email address is below. MD - end div(class: 'py-5') do cta('mailto:support@name.pn', icon_name: 'email') do 'support@name.pn' diff --git a/app/views/pages/privacy.rb b/app/views/pages/privacy.rb new file mode 100644 index 0000000..3c8689c --- /dev/null +++ b/app/views/pages/privacy.rb @@ -0,0 +1,245 @@ +# frozen_string_literal: true + +class Views::Pages::Privacy < Views::Base + def view_template + Prose <<~MD + # #{title 'Privacy policy'} + This is the name.pn privacy policy. We want to be clear about how we will use the information you give us and keep it safe. + + It tells you how we look after your personal data, like your name or photo, when you visit our website wherever you visit it from. It tells you about your privacy rights and how the law protects you. + + This privacy policy is provided in a layered format so you can click through to the specific areas set out below. + + name.pn puts you in control - your name, your rules. The short-URL we create and identity data is set and managed by you, and you choose what is made public by using our service. + + name.pn will not ask you for personal information about yourself except where it will be published on your own name.pn page. Apart from a few fields needed to ensure a uniform experience, most data requested is optional and you only need to provide it if you want it to be on your page. + + * [1. Important information and who we are](#1-important-information-and-who-we-are) + * [2. The data we collect about you](#2-the-data-we-collect-about-you) + * [3. How is your personal data collected?](#3-how-is-your-personal-data-collected) + * [4. How we use your personal data](#4-how-we-use-your-personal-data) + * [5. Disclosures of your personal data](#5-disclosures-of-your-personal-data) + * [6. International transfers](#6-international-transfers) + * [7. Data security](#7-data-security) + * [8. Data retention](#8-data-retention) + * [9. Your legal rights](#9-your-legal-rights) + + ## 1. Important information and who we are + + ### Purpose of this privacy policy + + This privacy policy gives you information on how name.pn collects and uses your personal data when you visit this website, including any data you may provide through this website when you use our service. + + We only use the data you provide us, and don’t collect data about you from anywhere else. + + This website is not intended for children and we do not knowingly collect data relating to children. + + It is important that you read this privacy policy together with any other information we may provide on specific occasions when we are collecting or processing personal data about you. For example, if we were to offer new services or email contact options. + + This privacy policy supplements other notices and privacy policies and is not intended to override them. + + Third parties that may be involved in processing your data are: + + * [Heroku Postgres](https://www.heroku.com/postgres): Your profile data is stored and backed up here. + * [Amazon Web Services](https://aws.amazon.com/): If you upload images or audio, they are stored here. + * [Buttondown](https://buttondown.email/): We use your email address and name preferences to send you occasional emails, if you have opted in. + * [Google Analytics](https://analytics.google.com/): We use this to understand more about who is visiting our site. + * [Datadog](https://www.datadoghq.com/): We send technical data about the functioning of the application to Datadog in order to predict and diagnose issues with the site. + + ### Controller + + name.pn is the controller and responsible for your personal data. We are referred to as name.pn, "we", "us" or "our" in this privacy policy. + + If you have any questions about this privacy policy, including any requests to exercise [your legal rights](#9-your-legal-rights), please contact us using the details set out below. + + ### Contact details + + If you have any questions about this privacy policy, you can contact us at: + + * Full name of legal entity: Fish Percolator Limited + * Email address: privacy@fishpercolator.co.uk + * Postal address: International House, 14 King Street, Leeds LS1 2HL, United Kingdom + + You have the right to make a complaint at any time to the [Information Commissioner's Office (ICO)](https://www.ico.org.uk/), the UK regulator for data protection issues. Our ICO registration is ZB138365. + + We would appreciate the chance to deal with your concerns before you approach the ICO. + + ### Changes to the privacy policy and your duty to inform us of changes + + We keep our privacy policy under regular review. This version is dated 4 June 2025. + + It is important that the personal data we hold about you is accurate and current. Our service lets you ensure the personal data we hold can be updated by you when needed. + + ### Third-party links + + This website may include links to third-party websites, plug-ins and applications. Clicking on those links or enabling those connections may allow third parties to collect or share data about you. We do not control these third-party websites and are not responsible for their privacy statements. When you leave our website, we encourage you to read the privacy policy of every website you visit. + + ## 2. The data we collect about you + + Personal data, or personal information, means any information about an individual from which that person can be identified - like your name or photo. It does not include data where the identity has been removed (anonymous data). + + We may collect, use, store and transfer different kinds of personal data about you which we have grouped together as follows: + + * Identity Data includes various forms of your personal name, your photo/likeness and your social media profiles. + * Contact Data includes email address. + * Technical Data includes internet protocol (IP) address, your login data, browser type and version, and the times of your visits. + * Usage Data includes information about how you use our website. + * Marketing and Communications Data includes your preferences in receiving marketing from us and your communication preferences. + + We also collect, use and share Aggregated Data such as statistical or demographic data for any purpose. Aggregated Data could be derived from your personal data but is not considered personal data in law as this data will not directly or indirectly reveal your identity. For example, we may aggregate your Usage Data to calculate the percentage of users accessing a specific website feature. + + However, if we combine or connect Aggregated Data with your personal data so that it can directly or indirectly identify you, we treat the combined data as personal data which will be used in accordance with this privacy policy. + + We do not collect any Special Categories of Personal Data about you (this includes details about your race or ethnicity, religious or philosophical beliefs, sex life, sexual orientation, political opinions, trade union membership, information about your health, and genetic and biometric data). Nor do we collect any information about criminal convictions and offences. + + ## 3. How is your personal data collected? + + We use different methods to collect data from and about you including: + + * Personal interactions. You may give us your Identity, Contact and Financial Data by filling in forms or by sending us an email. This includes personal data you provide when you: + * create an account on our website; + * fill out your public profile information using our website; + * subscribe to our service or publications; + * request marketing to be sent to you; + * give us feedback or contact us. + * Automated technologies or interactions. As you interact with our website, we will automatically collect Technical Data about your equipment, browsing actions and patterns. We collect this personal data by using cookies, server logs and other similar technologies. Technical Data from the following parties: + * analytics providers such as Google based outside the UK; + * advertising networks such as Instagram based outside the UK; and + * search information providers such as Google based outside the UK. + + ## 4. How we use your personal data + + We will only use your personal data as the law allows us to. We make sure we have a clear legal basis. We will use your personal data in the following circumstances: + + * Where it is necessary for our legitimate interests (or those of a third party) and your interests and fundamental rights do not override those interests. We have a legitimate interest in offering this service to allow people to better communicate their name and personal pronouns on the internet. + * Where we need to comply with a legal obligation. + + We do not use consent as a legal basis for processing your personal data, but we will get your consent before sending third party direct marketing communications to you via email or text message. + + You have the right to withdraw consent to marketing at any time by contacting us or by using [the opt-out form available on your account page](#{edit_user_registration_path}). + + ### Purposes for which we will use your personal data + + We have set out a table of all the ways we plan to use your personal data, and which of the legal bases we rely on to do so. We have also identified what our legitimate interests are where appropriate. + + We may process your personal data for more than one lawful ground depending on the specific purpose for which we are using your data. + + Please contact us if you need details about the specific legal ground we are relying on to process your personal data where more than one ground has been set out in the table below. + + | Purpose | Type of data | Lawful basis for processing | + |---------|--------------|-----------------------------| + | To register you as a new user |
  1. Identity
  2. Contact
| Legitimate interest | + + To manage our relationship with you which will include: + Notifying you about changes to our terms or privacy policy + Receiving email updates + Identity + Contact + Profile + Marketing and Communications + Legitimate interest + To administer and protect our business and this website (including troubleshooting, data analysis, testing, system maintenance, support, reporting and hosting of data) + Identity + Contact + Technical + Legitimate interest; Necessary to comply with a legal obligation + To use data analytics to improve our website, products/services, marketing, customer relationships and experiences + Technical + Usage + Necessary for our legitimate interests (to define types of customers for our products and services, to keep our website updated and relevant, to develop our business and to inform our marketing strategy) + + ### Opting out + + You can ask us or third parties to stop sending you marketing messages at any time by logging into the website and unsubscribing from [your account page](#{edit_user_registration_path}) OR by following the opt-out links on any marketing message sent to you OR by contacting us at any time. + + Where you opt out of receiving these marketing messages, this will not apply to personal data provided to us as a result of a product/service purchase, warranty registration, product/service experience or other transactions. + + ### Cookies + + You can set your browser to refuse all or some browser cookies, or to alert you when websites set or access cookies. If you disable or refuse cookies, please note that some parts of this website may become inaccessible or not function properly. + + The cookies we use are: + + _name_pn_session + : Used to identify your current browser session and store encrypted information related to whether you are logged in and what user you are logged in as. + + remember_user_token + : If you select 'remember me' on the login page, this cookie will be used to log you back in when you return to name.pn in a future session. + + _ga + : Used by Google Analytics to collect the analytics data described above. + + ### Change of purpose + + We will only use your personal data for the purposes for which we collected it, unless we reasonably consider that we need to use it for another reason and that reason is compatible with the original purpose. + + If you wish to get an explanation as to how the processing for the new purpose is compatible with the original purpose, please contact us. + + If we need to use your personal data for an unrelated purpose, we will notify you and we will explain the legal basis which allows us to do so. + + Please note that we may process your personal data without your knowledge or consent, in compliance with the above rules, where this is required or permitted by law. + + ## 5. Disclosures of your personal data + + We may share your personal data with the parties set out below for the purposes set out in the table above. + + Internal Third Parties as set out in the Glossary. + External Third Parties as set out in the Glossary. + Third parties to whom we may choose to sell, transfer or merge parts of our business or our assets. Alternatively, we may seek to acquire other businesses or merge with them. If a change happens to our business, then the new owners may use your personal data in the same way as set out in this privacy policy. + We require all third parties to respect the security of your personal data and to treat it in accordance with the law. We do not allow our third-party service providers to use your personal data for their own purposes and only permit them to process your personal data for specified purposes and in accordance with our instructions. + + ## 6. International transfers + + Many of our external third parties are based outside the UK so their processing of your personal data will involve a transfer of data outside the UK + + Whenever we transfer your personal data out of the UK, we ensure a similar degree of protection is afforded to it by ensuring at least one of the following safeguards is implemented: + + We will only transfer your personal data to countries that have been deemed to provide an adequate level of protection for personal data. + Where we use certain service providers, we may use specific contracts approved for use in the UK which give personal data the same protection it has in the UK. + Please contact us if you want further information on the specific mechanism used by us when transferring your personal data out of the UK. + + ## 7. Data security + + We have put in place appropriate security measures to prevent your personal data from being accidentally lost, used or accessed in an unauthorised way, altered by people other than you or disclosed. + + In addition, we limit access to your personal data to those employees, agents, contractors and other third parties who have a business need to know. They will only process your personal data on our instructions and they are subject to a duty of confidentiality. The duty of confidentiality does not extend to information you have made public, but does apply to private information you provide to us such as your email address and IP address. + + We have put in place procedures to deal with any suspected personal data breach and will notify you and any applicable regulator of a breach where we are legally required to do so. + + ## 8. Data retention + + How long will you use my personal data for? + We will only keep your personal data for as long as reasonably necessary to fulfil the purposes we collected it for. You can choose to keep using name.pn as long as you like, and while using it your personal data will be retained. If you want to stop using name.pn, your data will be deleted from the main application immediately and from backups in 4 weeks. + + We may keep your personal data for a longer period in the event of a complaint or if we reasonably believe there is a prospect of litigation in respect to our relationship with you. + + To determine the appropriate retention period for personal data, we consider the amount, nature and sensitivity of the personal data, the potential risk of harm from unauthorised use or disclosure of your personal data, the purposes for which we process your personal data and whether we can achieve those purposes through other means. + + In some circumstances you can ask us to delete your data: see your legal rights below for further information. + + In some circumstances we will anonymise your personal data (so that it can no longer be associated with you) for research or statistical purposes, in which case we may use this information indefinitely without further notice to you. + + ## 9. Your legal rights + + You have rights under data protection laws in relation to your personal data. Please click on the links below to find out more about these rights: + + Request access to your personal data - you can view most of your personal data by using our service. You can request a copy of any other information we hold about you. + Request correction of your personal data - you can correct most of your personal data by using our service. You can request a correction of any other information we hold about you. + Request erasure of your personal data - you can erase or delete most of your personal data by using our service. You can request erasure of any other information we hold about you. + Object to processing of your personal data - you can object to our use of your personal data where this is outside the use of our service, which you are in control of. + Request restriction of processing your personal data - you can request that we restrict the further use of your personal data. + Request transfer of your personal data - you can use our service to receive a transfer of your personal data. + If you wish to request any of the rights set out above, please contact privacy@fishpercolator.co.uk. + + No fee usually required + You will not have to pay a fee to access your personal data (or to exercise any of the other rights). However, we may charge a reasonable fee if your request is clearly unfounded, repetitive or excessive. Alternatively, we could refuse to comply with your request in these circumstances. + + What we may need from you + We may need to request specific information from you to help us confirm your identity and ensure your right to access your personal data (or to exercise any of your other rights). This is a security measure to ensure that personal data is not disclosed to any person who has no right to receive it. We may also contact you to ask you for further information in relation to your request to speed up our response. + + Time limit to respond + We try to respond to all legitimate requests within one month. Occasionally it could take us longer than a month if your request is particularly complex or you have made a number of requests. In this case, we will notify you and keep you updated. + MD + end + +end diff --git a/app/views/pages/terms.rb b/app/views/pages/terms.rb index 1b87314..fe118af 100644 --- a/app/views/pages/terms.rb +++ b/app/views/pages/terms.rb @@ -2,8 +2,8 @@ class Views::Pages::Terms < Views::Base def view_template - Prose do <<~MD - # Terms and conditions of use + Prose <<~MD + # #{title 'Terms and conditions of use'} ## 1. Introduction @@ -262,6 +262,5 @@ def view_template * (a) by post, to the postal address given above; * (b) by email, using support@name.pn. MD - end end end diff --git a/config/initializers/markdown_views.rb b/config/initializers/markdown_views.rb deleted file mode 100644 index 4cf4d04..0000000 --- a/config/initializers/markdown_views.rb +++ /dev/null @@ -1 +0,0 @@ -MarkdownViews.extensions.merge! header_ids: "" From 50284d46b6990cbf425aa7be1068c5f5d239d6a6 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Wed, 4 Jun 2025 17:26:51 +0100 Subject: [PATCH 13/44] Finish the privacy policy page --- app/views/pages/privacy.rb | 62 +++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/app/views/pages/privacy.rb b/app/views/pages/privacy.rb index 3c8689c..c8ac5cd 100644 --- a/app/views/pages/privacy.rb +++ b/app/views/pages/privacy.rb @@ -129,25 +129,10 @@ def view_template | Purpose | Type of data | Lawful basis for processing | |---------|--------------|-----------------------------| | To register you as a new user |
  1. Identity
  2. Contact
| Legitimate interest | - - To manage our relationship with you which will include: - Notifying you about changes to our terms or privacy policy - Receiving email updates - Identity - Contact - Profile - Marketing and Communications - Legitimate interest - To administer and protect our business and this website (including troubleshooting, data analysis, testing, system maintenance, support, reporting and hosting of data) - Identity - Contact - Technical - Legitimate interest; Necessary to comply with a legal obligation - To use data analytics to improve our website, products/services, marketing, customer relationships and experiences - Technical - Usage - Necessary for our legitimate interests (to define types of customers for our products and services, to keep our website updated and relevant, to develop our business and to inform our marketing strategy) - + | To manage our relationship with you which will include:
  • Notifying you about changes to our terms or privacy policy
  • Receiving email updates
|
  1. Identity
  2. Contact
  3. Profile
  4. Marketing and Communications
  5. | Legitimate interest | + | To administer and protect our business and this website (including troubleshooting, data analysis, testing, system maintenance, support, reporting and hosting of data) |
    1. Identity
    2. Contact
    3. Technical
    | Legitimate interest; Necessary to comply with a legal obligation | + | To use data analytics to improve our website, products/services, marketing, customer relationships and experiences |
    1. Technical
    2. Usage
    | Necessary for our legitimate interests (to define types of customers for our products and services, to keep our website updated and relevant, to develop our business and to inform our marketing strategy) | + ### Opting out You can ask us or third parties to stop sending you marketing messages at any time by logging into the website and unsubscribing from [your account page](#{edit_user_registration_path}) OR by following the opt-out links on any marketing message sent to you OR by contacting us at any time. @@ -183,9 +168,10 @@ def view_template We may share your personal data with the parties set out below for the purposes set out in the table above. - Internal Third Parties as set out in the Glossary. - External Third Parties as set out in the Glossary. - Third parties to whom we may choose to sell, transfer or merge parts of our business or our assets. Alternatively, we may seek to acquire other businesses or merge with them. If a change happens to our business, then the new owners may use your personal data in the same way as set out in this privacy policy. + * Internal Third Parties as set out in the [Glossary](#1-important-information-and-who-we-are). + * External Third Parties as set out in the [Glossary](#1-important-information-and-who-we-are). + * Third parties to whom we may choose to sell, transfer or merge parts of our business or our assets. Alternatively, we may seek to acquire other businesses or merge with them. If a change happens to our business, then the new owners may use your personal data in the same way as set out in this privacy policy. + We require all third parties to respect the security of your personal data and to treat it in accordance with the law. We do not allow our third-party service providers to use your personal data for their own purposes and only permit them to process your personal data for specified purposes and in accordance with our instructions. ## 6. International transfers @@ -194,8 +180,9 @@ def view_template Whenever we transfer your personal data out of the UK, we ensure a similar degree of protection is afforded to it by ensuring at least one of the following safeguards is implemented: - We will only transfer your personal data to countries that have been deemed to provide an adequate level of protection for personal data. - Where we use certain service providers, we may use specific contracts approved for use in the UK which give personal data the same protection it has in the UK. + * We will only transfer your personal data to countries that have been deemed to provide an adequate level of protection for personal data. + * Where we use certain service providers, we may use specific contracts approved for use in the UK which give personal data the same protection it has in the UK. + Please contact us if you want further information on the specific mechanism used by us when transferring your personal data out of the UK. ## 7. Data security @@ -208,14 +195,15 @@ def view_template ## 8. Data retention - How long will you use my personal data for? + ### How long will you use my personal data for? + We will only keep your personal data for as long as reasonably necessary to fulfil the purposes we collected it for. You can choose to keep using name.pn as long as you like, and while using it your personal data will be retained. If you want to stop using name.pn, your data will be deleted from the main application immediately and from backups in 4 weeks. We may keep your personal data for a longer period in the event of a complaint or if we reasonably believe there is a prospect of litigation in respect to our relationship with you. To determine the appropriate retention period for personal data, we consider the amount, nature and sensitivity of the personal data, the potential risk of harm from unauthorised use or disclosure of your personal data, the purposes for which we process your personal data and whether we can achieve those purposes through other means. - In some circumstances you can ask us to delete your data: see your legal rights below for further information. + In some circumstances you can ask us to delete your data: see [your legal rights](#9-your-legal-rights) below for further information. In some circumstances we will anonymise your personal data (so that it can no longer be associated with you) for research or statistical purposes, in which case we may use this information indefinitely without further notice to you. @@ -223,21 +211,25 @@ def view_template You have rights under data protection laws in relation to your personal data. Please click on the links below to find out more about these rights: - Request access to your personal data - you can view most of your personal data by using our service. You can request a copy of any other information we hold about you. - Request correction of your personal data - you can correct most of your personal data by using our service. You can request a correction of any other information we hold about you. - Request erasure of your personal data - you can erase or delete most of your personal data by using our service. You can request erasure of any other information we hold about you. - Object to processing of your personal data - you can object to our use of your personal data where this is outside the use of our service, which you are in control of. - Request restriction of processing your personal data - you can request that we restrict the further use of your personal data. - Request transfer of your personal data - you can use our service to receive a transfer of your personal data. + * [Request access to your personal data](https://ico.org.uk/your-data-matters/your-right-to-get-copies-of-your-data/) - you can view most of your personal data by using our service. You can request a copy of any other information we hold about you. + * [Request correction of your personal data](https://ico.org.uk/your-data-matters/your-right-to-get-your-data-corrected/) - you can correct most of your personal data by using our service. You can request a correction of any other information we hold about you. + * [Request erasure of your personal data](https://ico.org.uk/your-data-matters/your-right-to-get-your-data-deleted/) - you can erase or delete most of your personal data by using our service. You can request erasure of any other information we hold about you. + * [Object to processing of your personal data](https://ico.org.uk/your-data-matters/the-right-to-object-to-the-use-of-your-data/) - you can object to our use of your personal data where this is outside the use of our service, which you are in control of. + * [Request restriction of processing your personal data](https://ico.org.uk/your-data-matters/your-right-to-limit-how-organisations-use-your-data/) - you can request that we restrict the further use of your personal data. + * [Request transfer of your personal data](https://ico.org.uk/your-data-matters/your-right-to-data-portability/) - you can use our service to receive a transfer of your personal data. + If you wish to request any of the rights set out above, please contact privacy@fishpercolator.co.uk. - No fee usually required + ### No fee usually required + You will not have to pay a fee to access your personal data (or to exercise any of the other rights). However, we may charge a reasonable fee if your request is clearly unfounded, repetitive or excessive. Alternatively, we could refuse to comply with your request in these circumstances. - What we may need from you + ### What we may need from you + We may need to request specific information from you to help us confirm your identity and ensure your right to access your personal data (or to exercise any of your other rights). This is a security measure to ensure that personal data is not disclosed to any person who has no right to receive it. We may also contact you to ask you for further information in relation to your request to speed up our response. - Time limit to respond + ### Time limit to respond + We try to respond to all legitimate requests within one month. Occasionally it could take us longer than a month if your request is particularly complex or you have made a number of requests. In this case, we will notify you and keep you updated. MD end From 5c9cb473b69ef158950cca0e7f3f3b75ea86aebd Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 19 Jun 2025 14:21:11 +0100 Subject: [PATCH 14/44] Bump versions of dependencies where it is easy to do so --- Gemfile | 2 +- Gemfile.lock | 111 +++++----- yarn.lock | 586 +++++++++++++++++++++++++-------------------------- 3 files changed, 351 insertions(+), 348 deletions(-) diff --git a/Gemfile b/Gemfile index 1ae2e5e..4c96e44 100644 --- a/Gemfile +++ b/Gemfile @@ -32,7 +32,7 @@ gem 'bootsnap', '>= 1.4.2', require: false gem 'activeadmin', '~> 4.0.0.beta15' gem 'active_storage_base64', '~> 3.0' -gem 'active_storage_validations', '~> 2.0' +gem 'active_storage_validations', '~> 3.0' gem 'acts_as_list', '~> 1.0' gem 'amazing_print', '~> 1.3' gem 'blueprinter', '~> 1.0' diff --git a/Gemfile.lock b/Gemfile.lock index a5e5519..b323996 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -47,7 +47,7 @@ GEM active_storage_base64 (3.0.0) activestorage (> 7.0) activesupport (> 7.0) - active_storage_validations (2.0.3) + active_storage_validations (3.0.0) activejob (>= 6.1.4) activemodel (>= 6.1.4) activestorage (>= 6.1.4) @@ -95,54 +95,54 @@ GEM activesupport (>= 6.1) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) - amazing_print (1.7.2) + amazing_print (1.8.1) arbre (2.2.0) activesupport (>= 7.0) aws-actionmailer-ses (1.0.0) actionmailer (>= 7.1.0) aws-sdk-ses (~> 1, >= 1.50.0) aws-sdk-sesv2 (~> 1, >= 1.34.0) - aws-eventstream (1.3.2) - aws-partitions (1.1099.0) - aws-sdk-core (3.223.0) + aws-eventstream (1.4.0) + aws-partitions (1.1118.0) + aws-sdk-core (3.226.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) base64 jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.100.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-kms (1.105.0) + aws-sdk-core (~> 3, >= 3.225.0) aws-sigv4 (~> 1.5) aws-sdk-rails (5.1.0) aws-sdk-core (~> 3) railties (>= 7.1.0) - aws-sdk-s3 (1.185.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-s3 (1.190.0) + aws-sdk-core (~> 3, >= 3.225.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sdk-ses (1.83.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-ses (1.85.0) + aws-sdk-core (~> 3, >= 3.225.0) aws-sigv4 (~> 1.5) - aws-sdk-sesv2 (1.75.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-sesv2 (1.77.0) + aws-sdk-core (~> 3, >= 3.225.0) aws-sigv4 (~> 1.5) - aws-sigv4 (1.11.0) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) - base64 (0.2.0) + base64 (0.3.0) bcrypt (3.1.20) - benchmark (0.4.0) + benchmark (0.4.1) better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bigdecimal (3.1.9) + bigdecimal (3.2.2) bindata (2.5.1) bindex (0.8.1) binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) blueprinter (1.1.2) - bootsnap (1.18.4) + bootsnap (1.18.6) msgpack (~> 1.2) builder (3.3.0) bundler-audit (0.9.2) @@ -174,19 +174,19 @@ GEM crass (1.0.6) cssbundling-rails (1.4.3) railties (>= 6.0.0) - csv (3.3.4) - cuprite (0.15.1) + csv (3.3.5) + cuprite (0.17) capybara (~> 3.0) - ferrum (~> 0.15.0) + ferrum (~> 0.17.0) database_cleaner (2.1.0) database_cleaner-active_record (>= 2, < 3) - database_cleaner-active_record (2.2.0) + database_cleaner-active_record (2.2.1) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - datadog (2.15.0) - datadog-ruby_core_source (~> 3.4) - libdatadog (~> 16.0.1.1.0) + datadog (2.17.0) + datadog-ruby_core_source (~> 3.4, >= 3.4.1) + libdatadog (~> 18.1.0.1.0) libddwaf (~> 1.22.0.0.2) logger msgpack @@ -202,8 +202,8 @@ GEM devise-jwt (0.12.1) devise (~> 4.0) warden-jwt_auth (~> 0.10) - diff-lcs (1.6.1) - drb (2.2.1) + diff-lcs (1.6.2) + drb (2.2.3) dry-auto_inject (1.1.0) dry-core (~> 1.1) zeitwerk (~> 2.6) @@ -218,20 +218,22 @@ GEM htmlentities (~> 4.3.3) launchy (>= 2.1, < 4.0) mail (~> 2.7) + erb (5.0.1) erubi (1.13.1) - factory_bot (6.5.1) + factory_bot (6.5.4) activesupport (>= 6.1.0) - factory_bot_rails (6.4.4) + factory_bot_rails (6.5.0) factory_bot (~> 6.5) - railties (>= 5.0.0) + railties (>= 6.1.0) faraday (2.13.1) faraday-net_http (>= 2.0, < 3.5) json logger - faraday-net_http (3.4.0) + faraday-net_http (3.4.1) net-http (>= 0.5.0) - ferrum (0.15) + ferrum (0.17.1) addressable (~> 2.5) + base64 (~> 0.2) concurrent-ruby (~> 1.1) webrick (~> 1.7) websocket-driver (~> 0.7) @@ -264,7 +266,7 @@ GEM has_scope (0.8.2) actionpack (>= 5.2) activesupport (>= 5.2) - hashdiff (1.1.2) + hashdiff (1.2.0) high_voltage (4.0.0) htmlentities (4.3.4) i18n (1.14.7) @@ -293,7 +295,7 @@ GEM jmespath (1.6.2) jsbundling-rails (1.3.1) railties (>= 6.0.0) - json (2.11.3) + json (2.12.2) json-schema (5.1.1) addressable (~> 2.8) bigdecimal (~> 3.1) @@ -315,14 +317,14 @@ GEM addressable (~> 2.8) childprocess (~> 5.0) logger (~> 1.6) - libdatadog (16.0.1.1.0) + libdatadog (18.1.0.1.0) libddwaf (1.22.0.0.2) ffi (~> 1.0) listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) logger (1.7.0) - loofah (2.24.0) + loofah (2.24.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -331,7 +333,7 @@ GEM net-pop net-smtp marcel (1.0.4) - matrix (0.4.2) + matrix (0.4.3) meta-tags (2.22.1) actionpack (>= 6.0.0, < 8.1) method_source (1.1.0) @@ -339,7 +341,7 @@ GEM benchmark logger mini_mime (1.1.5) - mini_portile2 (2.8.8) + mini_portile2 (2.8.9) minitest (5.25.5) msgpack (1.8.0) multi_json (1.15.0) @@ -359,7 +361,7 @@ GEM mini_portile2 (~> 2.8.2) racc (~> 1.4) orm_adapter (0.5.0) - ostruct (0.6.1) + ostruct (0.6.2) pg (1.5.9) pp (0.6.2) prettyprint @@ -372,7 +374,7 @@ GEM pry-rescue (1.6.0) interception (>= 0.5) pry (>= 0.12.0) - psych (5.2.4) + psych (5.2.6) date stringio public_suffix (6.0.2) @@ -408,7 +410,7 @@ GEM activesupport (= 8.0.2) bundler (>= 1.15.0) railties (= 8.0.2) - rails-dom-testing (2.2.0) + rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) @@ -432,7 +434,7 @@ GEM rake (>= 12.2) thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) - rake (13.2.1) + rake (13.3.0) ransack (4.3.0) activerecord (>= 6.1.5) activesupport (>= 6.1.5) @@ -440,7 +442,8 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) - rdoc (6.13.1) + rdoc (6.14.1) + erb psych (>= 4.0.0) regexp_parser (2.10.0) reline (0.6.1) @@ -450,21 +453,21 @@ GEM railties (>= 5.2) rexml (3.4.1) rouge (4.5.2) - rspec (3.13.0) + rspec (3.13.1) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) rspec-collection_matchers (1.2.1) rspec-expectations (>= 2.99.0.beta1) - rspec-core (3.13.3) + rspec-core (3.13.4) rspec-support (~> 3.13.0) - rspec-expectations (3.13.4) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.4) + rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (8.0.0) + rspec-rails (8.0.1) actionpack (>= 7.2) activesupport (>= 7.2) railties (>= 7.2) @@ -472,7 +475,7 @@ GEM rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) - rspec-support (3.13.3) + rspec-support (3.13.4) rswag-api (2.16.0) activesupport (>= 5.2, < 8.1) railties (>= 5.2, < 8.1) @@ -484,7 +487,7 @@ GEM rswag-ui (2.16.0) actionpack (>= 5.2, < 8.1) railties (>= 5.2, < 8.1) - ruby-vips (2.2.3) + ruby-vips (2.2.4) ffi (~> 1.12) logger securerandom (0.4.1) @@ -516,7 +519,7 @@ GEM tilt (2.6.0) timecop (0.9.10) timeout (0.4.3) - turbo-rails (2.0.13) + turbo-rails (2.0.16) actionpack (>= 7.1.0) railties (>= 7.1.0) tzinfo (2.0.6) @@ -540,7 +543,7 @@ GEM crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) webrick (1.9.1) - websocket-driver (0.7.7) + websocket-driver (0.8.0) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -548,14 +551,14 @@ GEM railties (>= 3.0.7) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.7.2) + zeitwerk (2.7.3) PLATFORMS ruby DEPENDENCIES active_storage_base64 (~> 3.0) - active_storage_validations (~> 2.0) + active_storage_validations (~> 3.0) activeadmin (~> 4.0.0.beta15) acts_as_list (~> 1.0) addressable (~> 2.8) diff --git a/yarn.lock b/yarn.lock index b3e446f..c06a8ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,135 +15,135 @@ resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== -"@babel/runtime@^7.25.6", "@babel/runtime@^7.27.0", "@babel/runtime@^7.27.1": - version "7.27.1" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.1.tgz#9fce313d12c9a77507f264de74626e87fd0dc541" - integrity sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog== - -"@esbuild/aix-ppc64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz#830d6476cbbca0c005136af07303646b419f1162" - integrity sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q== - -"@esbuild/android-arm64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz#d11d4fc299224e729e2190cacadbcc00e7a9fd67" - integrity sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A== - -"@esbuild/android-arm@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.4.tgz#5660bd25080553dd2a28438f2a401a29959bd9b1" - integrity sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ== - -"@esbuild/android-x64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.4.tgz#18ddde705bf984e8cd9efec54e199ac18bc7bee1" - integrity sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ== - -"@esbuild/darwin-arm64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz#b0b7fb55db8fc6f5de5a0207ae986eb9c4766e67" - integrity sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g== - -"@esbuild/darwin-x64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz#e6813fdeba0bba356cb350a4b80543fbe66bf26f" - integrity sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A== - -"@esbuild/freebsd-arm64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz#dc11a73d3ccdc308567b908b43c6698e850759be" - integrity sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ== - -"@esbuild/freebsd-x64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz#91da08db8bd1bff5f31924c57a81dab26e93a143" - integrity sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ== - -"@esbuild/linux-arm64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz#efc15e45c945a082708f9a9f73bfa8d4db49728a" - integrity sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ== - -"@esbuild/linux-arm@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz#9b93c3e54ac49a2ede6f906e705d5d906f6db9e8" - integrity sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ== - -"@esbuild/linux-ia32@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz#be8ef2c3e1d99fca2d25c416b297d00360623596" - integrity sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ== - -"@esbuild/linux-loong64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz#b0840a2707c3fc02eec288d3f9defa3827cd7a87" - integrity sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA== - -"@esbuild/linux-mips64el@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz#2a198e5a458c9f0e75881a4e63d26ba0cf9df39f" - integrity sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg== - -"@esbuild/linux-ppc64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz#64f4ae0b923d7dd72fb860b9b22edb42007cf8f5" - integrity sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag== - -"@esbuild/linux-riscv64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz#fb2844b11fdddd39e29d291c7cf80f99b0d5158d" - integrity sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA== - -"@esbuild/linux-s390x@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz#1466876e0aa3560c7673e63fdebc8278707bc750" - integrity sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g== - -"@esbuild/linux-x64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz#c10fde899455db7cba5f11b3bccfa0e41bf4d0cd" - integrity sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA== - -"@esbuild/netbsd-arm64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz#02e483fbcbe3f18f0b02612a941b77be76c111a4" - integrity sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ== - -"@esbuild/netbsd-x64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz#ec401fb0b1ed0ac01d978564c5fc8634ed1dc2ed" - integrity sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw== - -"@esbuild/openbsd-arm64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz#f272c2f41cfea1d91b93d487a51b5c5ca7a8c8c4" - integrity sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A== - -"@esbuild/openbsd-x64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz#2e25950bc10fa9db1e5c868e3d50c44f7c150fd7" - integrity sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw== - -"@esbuild/sunos-x64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz#cd596fa65a67b3b7adc5ecd52d9f5733832e1abd" - integrity sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q== - -"@esbuild/win32-arm64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz#b4dbcb57b21eeaf8331e424c3999b89d8951dc88" - integrity sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ== - -"@esbuild/win32-ia32@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz#410842e5d66d4ece1757634e297a87635eb82f7a" - integrity sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg== - -"@esbuild/win32-x64@0.25.4": - version "0.25.4" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz#0b17ec8a70b2385827d52314c1253160a0b9bacc" - integrity sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ== +"@babel/runtime@^7.25.6", "@babel/runtime@^7.27.6": + version "7.27.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.6.tgz#ec4070a04d76bae8ddbb10770ba55714a417b7c6" + integrity sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q== + +"@esbuild/aix-ppc64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz#4e0f91776c2b340e75558f60552195f6fad09f18" + integrity sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA== + +"@esbuild/android-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz#bc766407f1718923f6b8079c8c61bf86ac3a6a4f" + integrity sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg== + +"@esbuild/android-arm@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.5.tgz#4290d6d3407bae3883ad2cded1081a234473ce26" + integrity sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA== + +"@esbuild/android-x64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.5.tgz#40c11d9cbca4f2406548c8a9895d321bc3b35eff" + integrity sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw== + +"@esbuild/darwin-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz#49d8bf8b1df95f759ac81eb1d0736018006d7e34" + integrity sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ== + +"@esbuild/darwin-x64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz#e27a5d92a14886ef1d492fd50fc61a2d4d87e418" + integrity sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ== + +"@esbuild/freebsd-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz#97cede59d638840ca104e605cdb9f1b118ba0b1c" + integrity sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw== + +"@esbuild/freebsd-x64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz#71c77812042a1a8190c3d581e140d15b876b9c6f" + integrity sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw== + +"@esbuild/linux-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz#f7b7c8f97eff8ffd2e47f6c67eb5c9765f2181b8" + integrity sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg== + +"@esbuild/linux-arm@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz#2a0be71b6cd8201fa559aea45598dffabc05d911" + integrity sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw== + +"@esbuild/linux-ia32@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz#763414463cd9ea6fa1f96555d2762f9f84c61783" + integrity sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA== + +"@esbuild/linux-loong64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz#428cf2213ff786a502a52c96cf29d1fcf1eb8506" + integrity sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg== + +"@esbuild/linux-mips64el@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz#5cbcc7fd841b4cd53358afd33527cd394e325d96" + integrity sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg== + +"@esbuild/linux-ppc64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz#0d954ab39ce4f5e50f00c4f8c4fd38f976c13ad9" + integrity sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ== + +"@esbuild/linux-riscv64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz#0e7dd30730505abd8088321e8497e94b547bfb1e" + integrity sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA== + +"@esbuild/linux-s390x@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz#5669af81327a398a336d7e40e320b5bbd6e6e72d" + integrity sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ== + +"@esbuild/linux-x64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz#b2357dd153aa49038967ddc1ffd90c68a9d2a0d4" + integrity sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw== + +"@esbuild/netbsd-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz#53b4dfb8fe1cee93777c9e366893bd3daa6ba63d" + integrity sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw== + +"@esbuild/netbsd-x64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz#a0206f6314ce7dc8713b7732703d0f58de1d1e79" + integrity sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ== + +"@esbuild/openbsd-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz#2a796c87c44e8de78001d808c77d948a21ec22fd" + integrity sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw== + +"@esbuild/openbsd-x64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz#28d0cd8909b7fa3953af998f2b2ed34f576728f0" + integrity sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg== + +"@esbuild/sunos-x64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz#a28164f5b997e8247d407e36c90d3fd5ddbe0dc5" + integrity sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA== + +"@esbuild/win32-arm64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz#6eadbead38e8bd12f633a5190e45eff80e24007e" + integrity sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw== + +"@esbuild/win32-ia32@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz#bab6288005482f9ed2adb9ded7e88eba9a62cc0d" + integrity sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ== + +"@esbuild/win32-x64@0.25.5": + version "0.25.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz#7fc114af5f6563f19f73324b5d5ff36ece0803d1" + integrity sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g== "@hotwired/stimulus@^3.1.0": version "3.2.2" @@ -352,22 +352,22 @@ integrity sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg== "@uppy/companion-client@^4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@uppy/companion-client/-/companion-client-4.4.1.tgz#ad7ae903b45f51fa60cf792dbf6e7e5e1cf0c453" - integrity sha512-ardMacShsfzaIbqHEH48YlpzWZkBj1qhAj0Dvn3r31p9d0HA5xFUvAdLYrZ6ezKvZ0RcDbf0SB5qCrQMkjscXQ== + version "4.4.2" + resolved "https://registry.yarnpkg.com/@uppy/companion-client/-/companion-client-4.4.2.tgz#4e3cd9fb71f99ed62835db837006eff3d38fea87" + integrity sha512-UZlHWItCGlZMlHxH4RgytUg43UZyuUX1JuvborNW1OzlLeZTvxL1dhjaq8pgjx3TlClkfpKLRRF8II0XPJgxzA== dependencies: - "@uppy/utils" "^6.1.1" + "@uppy/utils" "^6.1.4" namespace-emitter "^2.0.1" p-retry "^6.1.0" "@uppy/core@^4.3.1": - version "4.4.4" - resolved "https://registry.yarnpkg.com/@uppy/core/-/core-4.4.4.tgz#89e7d1933b4dd27f6c3c49479f1672f569411e28" - integrity sha512-etaG6uSrShnS8x/CGI9ME/XP23jw8FTkujzKbaag4lDTtdlvcs6PxcObtH3I5MxxJXUEWwcfVGTdAUvd/tGCaA== + version "4.4.6" + resolved "https://registry.yarnpkg.com/@uppy/core/-/core-4.4.6.tgz#9908222b175e5a1260ca22a8142531b8d31b7c48" + integrity sha512-EH5C5TSwY3tXp/gl4SPTpcxzEjBz8Ntt1o1s/v03bw8AUTl7X0lGUWRepbCh/XIUJyBTMJ1O+4Uv4/164WQSLg== dependencies: "@transloadit/prettier-bytes" "^0.3.4" "@uppy/store-default" "^4.2.0" - "@uppy/utils" "^6.1.3" + "@uppy/utils" "^6.1.5" lodash "^4.17.21" mime-match "^1.0.2" namespace-emitter "^2.0.1" @@ -375,16 +375,16 @@ preact "^10.5.13" "@uppy/dashboard@^4.1.3": - version "4.3.3" - resolved "https://registry.yarnpkg.com/@uppy/dashboard/-/dashboard-4.3.3.tgz#98a121bca32a7c47954b4f18a6c27b0bd29cf124" - integrity sha512-bZzzyzWzyoCsXxLUKsjT+X6AWNlFjRU1Ui+28Hp0C7A43zm8y0pb8BbwECl8a3NJH10tKNOHwT8IiVwbiGwXPQ== + version "4.3.4" + resolved "https://registry.yarnpkg.com/@uppy/dashboard/-/dashboard-4.3.4.tgz#2b63c5424833cf311a396d4b35d6cc0dfab43c45" + integrity sha512-SMPa5K3jZ2qNf110Hf8adN/cEAQLdpvXGjgl+R9c8AnUdpKE5f4XxaWSukdW6N7YYWmoBrLGesFvwRSPKZzCOw== dependencies: "@transloadit/prettier-bytes" "^0.3.4" "@uppy/informer" "^4.2.1" - "@uppy/provider-views" "^4.4.2" + "@uppy/provider-views" "^4.4.3" "@uppy/status-bar" "^4.1.3" "@uppy/thumbnail-generator" "^4.1.1" - "@uppy/utils" "^6.1.3" + "@uppy/utils" "^6.1.4" classnames "^2.2.6" lodash "^4.17.21" memoize-one "^6.0.0" @@ -393,11 +393,11 @@ shallow-equal "^3.0.0" "@uppy/image-editor@^3.2.1": - version "3.3.2" - resolved "https://registry.yarnpkg.com/@uppy/image-editor/-/image-editor-3.3.2.tgz#7e26b957bd8b7dd4c92a1960854a546c8f51b9f2" - integrity sha512-Kqe8RfH7Jns9Xs7bcyPrDajBnZ7rL5KHuIIwt2KbuWS2if5sypZlbQCqn1d1AU2kKMuNpmko8K1SierTfpKArg== + version "3.3.3" + resolved "https://registry.yarnpkg.com/@uppy/image-editor/-/image-editor-3.3.3.tgz#ecb32046b4e1c69463779e283b253491cbf392b6" + integrity sha512-1jfApC1nDYdvcOsWuRA+ZqlS5CxeW9HYZE8xOXCmnDNkmNYmemro0f6MU5tZrrPw8MncH6JOvzjuffPYQL0mzg== dependencies: - "@uppy/utils" "^6.1.3" + "@uppy/utils" "^6.1.4" cropperjs "^1.6.2" preact "^10.5.13" @@ -409,12 +409,12 @@ "@uppy/utils" "^6.1.1" preact "^10.5.13" -"@uppy/provider-views@^4.4.2": - version "4.4.2" - resolved "https://registry.yarnpkg.com/@uppy/provider-views/-/provider-views-4.4.2.tgz#bb5a2d662c05fd81ced263dc4e6f55add23df254" - integrity sha512-YGrPJuydrksmMCjvo7Ty7/lDLNo/Y8zsOgWgWmVbXB0V5aRvqY49LeKY8HDlOXclKmn6dl5CeQFf7p46txRNGQ== +"@uppy/provider-views@^4.4.3": + version "4.4.4" + resolved "https://registry.yarnpkg.com/@uppy/provider-views/-/provider-views-4.4.4.tgz#a9409422e58fb9a2fc7d747b2f521100ff9d6035" + integrity sha512-smqtb/HP0jd0PzLjpVfkGZ82z1ztHHB94qcGMTTaDlsTLGdG/bWdN+f+KOvHHeskXs8NUXJsqx5ISrVD6KORxA== dependencies: - "@uppy/utils" "^6.1.2" + "@uppy/utils" "^6.1.5" classnames "^2.2.6" nanoid "^5.0.9" p-queue "^8.0.0" @@ -443,20 +443,20 @@ "@uppy/utils" "^6.1.1" exifr "^7.0.0" -"@uppy/utils@^6.1.1", "@uppy/utils@^6.1.2", "@uppy/utils@^6.1.3": - version "6.1.3" - resolved "https://registry.yarnpkg.com/@uppy/utils/-/utils-6.1.3.tgz#34e18bfa9fbcad76ab3ab066eaeac3ec25c3add1" - integrity sha512-7WuTtMf0k1g962sE76mKy8aDV/kLeDrF8Wv1oTxaXQzUpmHBAoKd3FXLrQXu7TgM0XNHHRZXAckBttbVOWkKCw== +"@uppy/utils@^6.1.1", "@uppy/utils@^6.1.2", "@uppy/utils@^6.1.3", "@uppy/utils@^6.1.4", "@uppy/utils@^6.1.5": + version "6.1.5" + resolved "https://registry.yarnpkg.com/@uppy/utils/-/utils-6.1.5.tgz#161203d440325a9417ad1e9010a6a6c1690e6e97" + integrity sha512-R+3l4ir01I6MzZ80t5CSBoOGvV9mCqLZC9FoNYVTp/lsanQ3yMAZFGLN/x7JVGO7oi/m/ykmNR1jAH+JTtXdFg== dependencies: lodash "^4.17.21" preact "^10.5.13" "@uppy/webcam@^4.0.3": - version "4.1.2" - resolved "https://registry.yarnpkg.com/@uppy/webcam/-/webcam-4.1.2.tgz#28bb36c7b745a56b699c54ffe58c7536e4e92034" - integrity sha512-sbiqtrKxd+aoAkV10vTAj/Wr9PB6WjPhJyTGEz9Ff4+3gqnhUlusO4Od+cHZgGVZeqLhL4L9l6HwsFhdyK3nWg== + version "4.2.0" + resolved "https://registry.yarnpkg.com/@uppy/webcam/-/webcam-4.2.0.tgz#188f47dfe62107805528324eb16330d2c842be4d" + integrity sha512-UPJAOwPRHVYrtNkRji7+kXcjU8gNeb9M5zWO4vzKUernJNgfWVmrGG6Ndx01YyhjUCgPM4dXaWs9t6MwKh1DzA== dependencies: - "@uppy/utils" "^6.1.3" + "@uppy/utils" "^6.1.5" is-mobile "^4.0.0" preact "^10.5.13" @@ -514,11 +514,11 @@ arg@^5.0.2: integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== automation-events@^7.0.9: - version "7.1.9" - resolved "https://registry.yarnpkg.com/automation-events/-/automation-events-7.1.9.tgz#b740ecfa335e4f4e430b26972903560551d75480" - integrity sha512-nIcJo3WeNtB16TSIK2D5Ldy/5eErzmXSuOlK979sYtIQQT++iFfj/1PMjFH1XEvtqCO0+KNUEEIJ+O0l4BtncQ== + version "7.1.11" + resolved "https://registry.yarnpkg.com/automation-events/-/automation-events-7.1.11.tgz#2662c90e25b192d739b62a4030937e6051dc069c" + integrity sha512-TnclbJ0482ydRenzrR9FIbqalHScBBdQTIXv8tVunhYx8dq7E0Eq5v5CSAo67YmLXNbx5jCstHcLZDJ33iONDw== dependencies: - "@babel/runtime" "^7.27.0" + "@babel/runtime" "^7.27.6" tslib "^2.8.1" balanced-match@^1.0.0: @@ -545,15 +545,15 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -broker-factory@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/broker-factory/-/broker-factory-3.1.5.tgz#e84f401bedb97711e80dfcfa358a586e2ccc7834" - integrity sha512-/s86CAyDE4d85Q2zvlMScbUBlIWeZ1LeCtQ9JpQlNesaJdXRZ5+dnzzEzpUQDTltCXs0SfjZ5GNRcxJPtyA6dg== +broker-factory@^3.1.7: + version "3.1.7" + resolved "https://registry.yarnpkg.com/broker-factory/-/broker-factory-3.1.7.tgz#18046e36aaf0f98ca18909ea1bfef7268f2c3274" + integrity sha512-RxbMXWq/Qvw9aLZMvuooMtVTm2/SV9JEpxpBbMuFhYAnDaZxctbJ+1b9ucHxADk/eQNqDijvWQjLVARqExAeyg== dependencies: - "@babel/runtime" "^7.27.0" - fast-unique-numbers "^9.0.20" + "@babel/runtime" "^7.27.6" + fast-unique-numbers "^9.0.22" tslib "^2.8.1" - worker-factory "^7.0.41" + worker-factory "^7.0.43" "bulma-o-steps@github:octoshrimpy/bulma-o-steps#c21bf10": version "1.1.0" @@ -670,35 +670,35 @@ emoji-regex@^9.2.2: integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== esbuild@^0.25.0: - version "0.25.4" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.4.tgz#bb9a16334d4ef2c33c7301a924b8b863351a0854" - integrity sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q== + version "0.25.5" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.5.tgz#71075054993fdfae76c66586f9b9c1f8d7edd430" + integrity sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ== optionalDependencies: - "@esbuild/aix-ppc64" "0.25.4" - "@esbuild/android-arm" "0.25.4" - "@esbuild/android-arm64" "0.25.4" - "@esbuild/android-x64" "0.25.4" - "@esbuild/darwin-arm64" "0.25.4" - "@esbuild/darwin-x64" "0.25.4" - "@esbuild/freebsd-arm64" "0.25.4" - "@esbuild/freebsd-x64" "0.25.4" - "@esbuild/linux-arm" "0.25.4" - "@esbuild/linux-arm64" "0.25.4" - "@esbuild/linux-ia32" "0.25.4" - "@esbuild/linux-loong64" "0.25.4" - "@esbuild/linux-mips64el" "0.25.4" - "@esbuild/linux-ppc64" "0.25.4" - "@esbuild/linux-riscv64" "0.25.4" - "@esbuild/linux-s390x" "0.25.4" - "@esbuild/linux-x64" "0.25.4" - "@esbuild/netbsd-arm64" "0.25.4" - "@esbuild/netbsd-x64" "0.25.4" - "@esbuild/openbsd-arm64" "0.25.4" - "@esbuild/openbsd-x64" "0.25.4" - "@esbuild/sunos-x64" "0.25.4" - "@esbuild/win32-arm64" "0.25.4" - "@esbuild/win32-ia32" "0.25.4" - "@esbuild/win32-x64" "0.25.4" + "@esbuild/aix-ppc64" "0.25.5" + "@esbuild/android-arm" "0.25.5" + "@esbuild/android-arm64" "0.25.5" + "@esbuild/android-x64" "0.25.5" + "@esbuild/darwin-arm64" "0.25.5" + "@esbuild/darwin-x64" "0.25.5" + "@esbuild/freebsd-arm64" "0.25.5" + "@esbuild/freebsd-x64" "0.25.5" + "@esbuild/linux-arm" "0.25.5" + "@esbuild/linux-arm64" "0.25.5" + "@esbuild/linux-ia32" "0.25.5" + "@esbuild/linux-loong64" "0.25.5" + "@esbuild/linux-mips64el" "0.25.5" + "@esbuild/linux-ppc64" "0.25.5" + "@esbuild/linux-riscv64" "0.25.5" + "@esbuild/linux-s390x" "0.25.5" + "@esbuild/linux-x64" "0.25.5" + "@esbuild/netbsd-arm64" "0.25.5" + "@esbuild/netbsd-x64" "0.25.5" + "@esbuild/openbsd-arm64" "0.25.5" + "@esbuild/openbsd-x64" "0.25.5" + "@esbuild/sunos-x64" "0.25.5" + "@esbuild/win32-arm64" "0.25.5" + "@esbuild/win32-ia32" "0.25.5" + "@esbuild/win32-x64" "0.25.5" eventemitter3@^5.0.1: version "5.0.1" @@ -710,46 +710,46 @@ exifr@^7.0.0: resolved "https://registry.yarnpkg.com/exifr/-/exifr-7.1.3.tgz#f6218012c36dbb7d843222011b27f065fddbab6f" integrity sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw== -extendable-media-recorder-wav-encoder-broker@^7.0.117: - version "7.0.117" - resolved "https://registry.yarnpkg.com/extendable-media-recorder-wav-encoder-broker/-/extendable-media-recorder-wav-encoder-broker-7.0.117.tgz#71096b15b902372f3b2ab2f43a55794ce4620dc5" - integrity sha512-MAkd+eZ/M5szsb4hFSgD81ZfS88LxbBOzzxK9Lnz1DwLlvsf3kyxJDOegp97Dl3dozkUBKG5DjLH3F3CUJZbbQ== +extendable-media-recorder-wav-encoder-broker@^7.0.119: + version "7.0.119" + resolved "https://registry.yarnpkg.com/extendable-media-recorder-wav-encoder-broker/-/extendable-media-recorder-wav-encoder-broker-7.0.119.tgz#577667c724a749b1d5161072290ea3aeac395bc3" + integrity sha512-BLrFOnqFLpsmmNpSk/TfjNs4j6ImCSGtoryIpRlqNu5S/Avt6gRJI0s4UYvdK7h17PCi+8vaDr75blvmU1sYlw== dependencies: - "@babel/runtime" "^7.27.0" - broker-factory "^3.1.5" - extendable-media-recorder-wav-encoder-worker "^8.0.114" + "@babel/runtime" "^7.27.6" + broker-factory "^3.1.7" + extendable-media-recorder-wav-encoder-worker "^8.0.116" tslib "^2.8.1" -extendable-media-recorder-wav-encoder-worker@^8.0.114: - version "8.0.114" - resolved "https://registry.yarnpkg.com/extendable-media-recorder-wav-encoder-worker/-/extendable-media-recorder-wav-encoder-worker-8.0.114.tgz#a81b97f1a9ba357c765328e7dc8d775c42373635" - integrity sha512-LVAvpxJQFmhQbQPX1D7xyv/VjP9gOXBgbvzoEY7WdUEe+HJfSo6xfJH5nL4JuJ347Zgvwv/d2zxKEq1Ibd/MMw== +extendable-media-recorder-wav-encoder-worker@^8.0.116: + version "8.0.116" + resolved "https://registry.yarnpkg.com/extendable-media-recorder-wav-encoder-worker/-/extendable-media-recorder-wav-encoder-worker-8.0.116.tgz#e5f1471a72db26cff75c3b4076fae3a6bd7a1ef6" + integrity sha512-bJPR0B7ZHeoqi9YoSie+UXAfEYya3efQ9eLiWuyK4KcOv+SuYQvWCoyzX5kjvb6GqIBCUnev5xulfeHRlyCwvw== dependencies: - "@babel/runtime" "^7.27.0" + "@babel/runtime" "^7.27.6" tslib "^2.8.1" - worker-factory "^7.0.41" + worker-factory "^7.0.43" extendable-media-recorder-wav-encoder@^7.0.81: - version "7.0.127" - resolved "https://registry.yarnpkg.com/extendable-media-recorder-wav-encoder/-/extendable-media-recorder-wav-encoder-7.0.127.tgz#01ea7e491fc80439baed3029f3933fc1748141b6" - integrity sha512-cEhbFk/9S72eGxF+lZXlISNwSANW2pLmnEIAp5felEzRnQ3VjOyQBPtjD8rKZRHuCOaIMn9FUnT1cMEwAWEiuw== + version "7.0.129" + resolved "https://registry.yarnpkg.com/extendable-media-recorder-wav-encoder/-/extendable-media-recorder-wav-encoder-7.0.129.tgz#5360198209ad006973d46efb5f4b484b76abd515" + integrity sha512-/wqM2hnzvLy/iUlg/EU3JIF8MJcidy8I77Z7CCm5+CVEClDfcs6bH9PgghuisndwKTaud0Dh48RTD83gkfEjCw== dependencies: - "@babel/runtime" "^7.27.0" - extendable-media-recorder-wav-encoder-broker "^7.0.117" - extendable-media-recorder-wav-encoder-worker "^8.0.114" + "@babel/runtime" "^7.27.6" + extendable-media-recorder-wav-encoder-broker "^7.0.119" + extendable-media-recorder-wav-encoder-worker "^8.0.116" tslib "^2.8.1" extendable-media-recorder@^9.1.0: - version "9.2.24" - resolved "https://registry.yarnpkg.com/extendable-media-recorder/-/extendable-media-recorder-9.2.24.tgz#5ceb0dd6192fe7b9af71e358be2109bbcf45cb12" - integrity sha512-Qu8L3bttpl5NOy4l9C30HFYO1eShtdplwp2YF/sEK5yoc3saC3EOPo57FsIXdQze20IbzIKt5lTjwiD2rKRPmg== - dependencies: - "@babel/runtime" "^7.27.1" - media-encoder-host "^9.0.18" - multi-buffer-data-view "^6.0.20" - recorder-audio-worklet "^6.0.46" + version "9.2.27" + resolved "https://registry.yarnpkg.com/extendable-media-recorder/-/extendable-media-recorder-9.2.27.tgz#6428fa4209525d97797bd1d760ce85364ce7c9aa" + integrity sha512-2X+Ixi1cxLek0Cj9x9atmhQ+apG+LwJpP2p3ypP8Pxau0poDnicrg7FTfPVQV5PW/3DHFm/eQ16vbgo5Yk3HGQ== + dependencies: + "@babel/runtime" "^7.27.6" + media-encoder-host "^9.0.20" + multi-buffer-data-view "^6.0.22" + recorder-audio-worklet "^6.0.48" standardized-audio-context "^25.3.77" - subscribable-things "^2.1.51" + subscribable-things "^2.1.53" tslib "^2.8.1" fast-glob@^3.3.2: @@ -763,12 +763,12 @@ fast-glob@^3.3.2: merge2 "^1.3.0" micromatch "^4.0.8" -fast-unique-numbers@^9.0.20: - version "9.0.20" - resolved "https://registry.yarnpkg.com/fast-unique-numbers/-/fast-unique-numbers-9.0.20.tgz#56b94578ab0cc9c057b954f52939af5427c92dca" - integrity sha512-2ym6CEsBN3CDEYBn2QgIfj6zCtOghEHcNU7wjq69W/y3TGr0m5XDZbVJ72CluwqnGwQBI8RZxDuTAjg1Ttphvw== +fast-unique-numbers@^9.0.22: + version "9.0.22" + resolved "https://registry.yarnpkg.com/fast-unique-numbers/-/fast-unique-numbers-9.0.22.tgz#cb39d5e11fd1122b2590582d93db20cf9764cb3c" + integrity sha512-dBR+30yHAqBGvOuxxQdnn2lTLHCO6r/9B+M4yF8mNrzr3u1yiF+YVJ6u3GTyPN/VRWqaE1FcscZDdBgVKmrmQQ== dependencies: - "@babel/runtime" "^7.27.0" + "@babel/runtime" "^7.27.6" tslib "^2.8.1" fastq@^1.6.0: @@ -850,9 +850,9 @@ hasown@^2.0.2: function-bind "^1.1.2" immutable@^5.0.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.2.tgz#e8169476414505e5a4fa650107b65e1227d16d4b" - integrity sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ== + version "5.1.3" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.3.tgz#e6486694c8b76c37c063cca92399fa64098634d4" + integrity sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg== improved-yarn-audit@^3.0.0: version "3.0.3" @@ -949,35 +949,35 @@ luminous-lightbox@^2.3.2: resolved "https://registry.yarnpkg.com/luminous-lightbox/-/luminous-lightbox-2.4.0.tgz#5403eccdae8a276f0bfd002915674bffdd978930" integrity sha512-qu1IE2/+sTyAzUgEGffHPnBNenJ1CDDieSDy6yLgmJUsMVoTLCbTeDJuQvje4hpl+M3xZvq87TIP8hu3BsgaEQ== -media-encoder-host-broker@^8.0.17: - version "8.0.17" - resolved "https://registry.yarnpkg.com/media-encoder-host-broker/-/media-encoder-host-broker-8.0.17.tgz#a10363047e17afaa9b37e95db03e31a0c5f93b96" - integrity sha512-q/rx2w+Aa7HgB0pa6nTZPRo/APWNEZJRpVjccErAuUH3RS3rCOrcmfz4/SiV8OcN7kA3tEMCih9Ebpilm6wvmQ== +media-encoder-host-broker@^8.0.19: + version "8.0.19" + resolved "https://registry.yarnpkg.com/media-encoder-host-broker/-/media-encoder-host-broker-8.0.19.tgz#d71cf7240481f029ccd32c8dc605f9d294ed8c3a" + integrity sha512-lTpsNuaZdTCdtTHsOyww7Ae0Mwv+7mFS+O4YkFYWhXwVs0rm6XbRK5jRRn5JmcX3n1eTE1lQS5RgX8qbNaIjSg== dependencies: - "@babel/runtime" "^7.27.0" - broker-factory "^3.1.5" - fast-unique-numbers "^9.0.20" - media-encoder-host-worker "^10.0.17" + "@babel/runtime" "^7.27.6" + broker-factory "^3.1.7" + fast-unique-numbers "^9.0.22" + media-encoder-host-worker "^10.0.19" tslib "^2.8.1" -media-encoder-host-worker@^10.0.17: - version "10.0.17" - resolved "https://registry.yarnpkg.com/media-encoder-host-worker/-/media-encoder-host-worker-10.0.17.tgz#2de59f3b4417c7e8d0b0da7d2d903aa37d43ec3e" - integrity sha512-Q/ATlqwDa2QXPK5UhJVv+GNYRkI33BMuFSLcfVf5T0gghBwGgttIZO2EonuExKKlhlfuqdB9K62nbdjCC0+hnQ== +media-encoder-host-worker@^10.0.19: + version "10.0.19" + resolved "https://registry.yarnpkg.com/media-encoder-host-worker/-/media-encoder-host-worker-10.0.19.tgz#94b94c9485da6b4ff8f14aea7b9b641c6316315c" + integrity sha512-I8fwc6f41peER3RFSiwDxnIHbqU7p3pc2ghQozcw9CQfL0mWEo4IjQJtyswrrlL/HO2pgVSMQbaNzE4q/0mfDQ== dependencies: - "@babel/runtime" "^7.27.0" - extendable-media-recorder-wav-encoder-broker "^7.0.117" + "@babel/runtime" "^7.27.6" + extendable-media-recorder-wav-encoder-broker "^7.0.119" tslib "^2.8.1" - worker-factory "^7.0.41" + worker-factory "^7.0.43" -media-encoder-host@^9.0.18: - version "9.0.18" - resolved "https://registry.yarnpkg.com/media-encoder-host/-/media-encoder-host-9.0.18.tgz#71df28dfbee8ef463a344cfce386ed10e67b18d6" - integrity sha512-BeGFUvxM22a91UlW1f3T+70znp5zV+keyGsK9niVWamhCvu/Uf6RkVtzWmOIWq16HfoZd1oc4sRgMCI5wqeQdA== +media-encoder-host@^9.0.20: + version "9.0.20" + resolved "https://registry.yarnpkg.com/media-encoder-host/-/media-encoder-host-9.0.20.tgz#6b4167184a0377e663fbe79058c4b635271387f0" + integrity sha512-IyEYxw6az97RNuETOAZV4YZqNAPOiF9GKIp5mVZb4HOyWd6mhkWQ34ydOzhqAWogMyc4W05kjN/VCgTtgyFmsw== dependencies: - "@babel/runtime" "^7.27.0" - media-encoder-host-broker "^8.0.17" - media-encoder-host-worker "^10.0.17" + "@babel/runtime" "^7.27.6" + media-encoder-host-broker "^8.0.19" + media-encoder-host-worker "^10.0.19" tslib "^2.8.1" memoize-one@^6.0.0: @@ -1022,12 +1022,12 @@ minimatch@^9.0.4: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== -multi-buffer-data-view@^6.0.20: - version "6.0.20" - resolved "https://registry.yarnpkg.com/multi-buffer-data-view/-/multi-buffer-data-view-6.0.20.tgz#eeda5aab1e89af6ef2ce95ffb5bf33b4afe45ee3" - integrity sha512-I2TzuoyHtcgnfR4O3ukFJdw9XXkisLMM42Dx+m29aB9oeaJAqLkbNOs47xx7evvdPFqzVCYl8fCbyNTBJgwvow== +multi-buffer-data-view@^6.0.22: + version "6.0.22" + resolved "https://registry.yarnpkg.com/multi-buffer-data-view/-/multi-buffer-data-view-6.0.22.tgz#7c10bbfcf0d4bdb41b19ff1f852c799097ce031f" + integrity sha512-SsI/exkodHsh+ofCV7An2PZWRaJC7eFVl7gtHQlMWFEDmWtb7cELr/GK32Nhe/6dZQhbr81o+Moswx9aXN3RRg== dependencies: - "@babel/runtime" "^7.27.0" + "@babel/runtime" "^7.27.6" tslib "^2.8.1" mz@^2.7.0: @@ -1193,9 +1193,9 @@ postcss@^8.4.47: source-map-js "^1.2.1" preact@^10.5.13: - version "10.26.5" - resolved "https://registry.yarnpkg.com/preact/-/preact-10.26.5.tgz#7e1e998af178f139e4c7cb53f441bf2179f44ad2" - integrity sha512-fmpDkgfGU6JYux9teDWLhj9mKN55tyepwYbxHgQuIxbWQzgFg5vk7Mrrtfx7xRxq798ynkY4DDDxZr235Kk+4w== + version "10.26.9" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.26.9.tgz#b3898d1b65140640799062ad73b89846c293b6a7" + integrity sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA== queue-microtask@^1.2.2: version "1.2.3" @@ -1221,27 +1221,27 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -recorder-audio-worklet-processor@^5.0.34: - version "5.0.34" - resolved "https://registry.yarnpkg.com/recorder-audio-worklet-processor/-/recorder-audio-worklet-processor-5.0.34.tgz#f63144fd46ac809df3e33c60db98dd6cd4779fc0" - integrity sha512-AjR/+s9+jyV1g7PhUkX+1j7DKlivyqp3YLEhbvHoJ2QyxPpJ7S9nis5GUa0u4DXcv70SyjvbifJj+O61coCwfQ== +recorder-audio-worklet-processor@^5.0.35: + version "5.0.35" + resolved "https://registry.yarnpkg.com/recorder-audio-worklet-processor/-/recorder-audio-worklet-processor-5.0.35.tgz#3bad1acee60577d14839147bf891ea7ae3afabf0" + integrity sha512-5Nzbk/6QzC3QFQ1EG2SE34c1ygLE22lIOvLyjy7N6XxE/jpAZrL4e7xR+yihiTaG3ajiWy6UjqL4XEBMM9ahFQ== dependencies: - "@babel/runtime" "^7.27.1" + "@babel/runtime" "^7.27.6" tslib "^2.8.1" -recorder-audio-worklet@^6.0.46: - version "6.0.46" - resolved "https://registry.yarnpkg.com/recorder-audio-worklet/-/recorder-audio-worklet-6.0.46.tgz#b1c1861a2c6d7a4e99b6c42e64e0b7eb99b24e21" - integrity sha512-4WkYAZ+ZuL1cLRrFmjecb4xgwGMtqfJVEtJW085TpttQDbuCUlD9s8yZr7+4GvdyLMo3RP0q1viP8Qt5BJr6VA== +recorder-audio-worklet@^6.0.48: + version "6.0.48" + resolved "https://registry.yarnpkg.com/recorder-audio-worklet/-/recorder-audio-worklet-6.0.48.tgz#7c326d77b687182f4f83960575cab7fcdb5bcd7e" + integrity sha512-PVlq/1hjCrPcUGqARg8rR30A303xDCao0jmlBTaUaKkN3Xme58RI7EQxurv8rw2eDwVrN+nrni0UoJoa5/v+zg== dependencies: - "@babel/runtime" "^7.27.1" - broker-factory "^3.1.5" - fast-unique-numbers "^9.0.20" - recorder-audio-worklet-processor "^5.0.34" + "@babel/runtime" "^7.27.6" + broker-factory "^3.1.7" + fast-unique-numbers "^9.0.22" + recorder-audio-worklet-processor "^5.0.35" standardized-audio-context "^25.3.77" - subscribable-things "^2.1.51" + subscribable-things "^2.1.53" tslib "^2.8.1" - worker-factory "^7.0.41" + worker-factory "^7.0.43" resolve@^1.1.7, resolve@^1.22.8: version "1.22.10" @@ -1275,9 +1275,9 @@ rxjs-interop@^2.0.0: integrity sha512-ASEq9atUw7lualXB+knvgtvwkCEvGWV2gDD/8qnASzBkzEARZck9JAyxmY8OS6Nc1pCPEgDTKNcx+YqqYfzArw== sass@^1.60.0: - version "1.87.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.87.0.tgz#8cceb36fa63fb48a8d5d7f2f4c13b49c524b723e" - integrity sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw== + version "1.89.2" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.89.2.tgz#a771716aeae774e2b529f72c0ff2dfd46c9de10e" + integrity sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA== dependencies: chokidar "^4.0.0" immutable "^5.0.2" @@ -1379,12 +1379,12 @@ strip-ansi@^7.0.1: dependencies: ansi-regex "^6.0.1" -subscribable-things@^2.1.51: - version "2.1.51" - resolved "https://registry.yarnpkg.com/subscribable-things/-/subscribable-things-2.1.51.tgz#24cbbf0c0b748a1a17361896dd1b58f1e6c27a49" - integrity sha512-d1JDHHb8mFlS8Czfpi2oRqYwna0FT1DmZlDzRzfyQPCmmurRGF+k4OetEcYWtyU+hVqbG5Dn5w1C8uUp+QLdHQ== +subscribable-things@^2.1.53: + version "2.1.53" + resolved "https://registry.yarnpkg.com/subscribable-things/-/subscribable-things-2.1.53.tgz#55229f96e8eba49304e8c94495f88b6fc80c6aea" + integrity sha512-zWvN9F/eYQWDKszXl4NXkyqPXvMDZDmXfcHiM5C5WQZTTY2OK+2TZeDlA9oio69FEPqPu9T6yeEcAhQ2uRmnaw== dependencies: - "@babel/runtime" "^7.27.0" + "@babel/runtime" "^7.27.6" rxjs-interop "^2.0.0" tslib "^2.8.1" @@ -1487,13 +1487,13 @@ wildcard@^1.1.0: resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-1.1.2.tgz#a7020453084d8cd2efe70ba9d3696263de1710a5" integrity sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng== -worker-factory@^7.0.41: - version "7.0.41" - resolved "https://registry.yarnpkg.com/worker-factory/-/worker-factory-7.0.41.tgz#789402e9c65293b69de5afcda0a3c96641b6e482" - integrity sha512-YZ5T7pzoazUvE+CR5bF+njKQFCLPAeN+yt5xPqUaJhUwpo/xUUSNn8QgvErZNQ79r/TuWEvFMLyl/3i40GDBHQ== +worker-factory@^7.0.43: + version "7.0.43" + resolved "https://registry.yarnpkg.com/worker-factory/-/worker-factory-7.0.43.tgz#81b0c5976c8c0f54d85c13822d20d97cb94b70a7" + integrity sha512-SACVoj3gWKtMVyT9N+VD11Pd/Xe58+ZFfp8b7y/PagOvj3i8lU3Uyj+Lj7WYTmSBvNLC0JFaQkx44E6DhH5+WA== dependencies: - "@babel/runtime" "^7.27.0" - fast-unique-numbers "^9.0.20" + "@babel/runtime" "^7.27.6" + fast-unique-numbers "^9.0.22" tslib "^2.8.1" "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": From 928a9dfc5fd8f7673b840e8312e39bccc4ed225e Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 19 Jun 2025 14:57:11 +0100 Subject: [PATCH 15/44] Bump dependencies over here too --- Gemfile.lock | 122 ++++++++++++++++++++++++++------------------------- yarn.lock | 22 +++++----- 2 files changed, 74 insertions(+), 70 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 73d5fad..4580764 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -95,54 +95,54 @@ GEM activesupport (>= 6.1) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) - amazing_print (1.7.2) + amazing_print (1.8.1) arbre (2.2.0) activesupport (>= 7.0) aws-actionmailer-ses (1.0.0) actionmailer (>= 7.1.0) aws-sdk-ses (~> 1, >= 1.50.0) aws-sdk-sesv2 (~> 1, >= 1.34.0) - aws-eventstream (1.3.2) - aws-partitions (1.1099.0) - aws-sdk-core (3.223.0) + aws-eventstream (1.4.0) + aws-partitions (1.1118.0) + aws-sdk-core (3.226.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) base64 jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.100.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-kms (1.105.0) + aws-sdk-core (~> 3, >= 3.225.0) aws-sigv4 (~> 1.5) aws-sdk-rails (5.1.0) aws-sdk-core (~> 3) railties (>= 7.1.0) - aws-sdk-s3 (1.185.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-s3 (1.190.0) + aws-sdk-core (~> 3, >= 3.225.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sdk-ses (1.83.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-ses (1.85.0) + aws-sdk-core (~> 3, >= 3.225.0) aws-sigv4 (~> 1.5) - aws-sdk-sesv2 (1.75.0) - aws-sdk-core (~> 3, >= 3.216.0) + aws-sdk-sesv2 (1.77.0) + aws-sdk-core (~> 3, >= 3.225.0) aws-sigv4 (~> 1.5) - aws-sigv4 (1.11.0) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) - base64 (0.2.0) + base64 (0.3.0) bcrypt (3.1.20) - benchmark (0.4.0) + benchmark (0.4.1) better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bigdecimal (3.1.9) + bigdecimal (3.2.2) bindata (2.5.1) bindex (0.8.1) binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) blueprinter (1.1.2) - bootsnap (1.18.4) + bootsnap (1.18.6) msgpack (~> 1.2) builder (3.3.0) bundler-audit (0.9.2) @@ -176,19 +176,19 @@ GEM crass (1.0.6) cssbundling-rails (1.4.3) railties (>= 6.0.0) - csv (3.3.4) - cuprite (0.15.1) + csv (3.3.5) + cuprite (0.17) capybara (~> 3.0) - ferrum (~> 0.15.0) + ferrum (~> 0.17.0) database_cleaner (2.1.0) database_cleaner-active_record (>= 2, < 3) - database_cleaner-active_record (2.2.0) + database_cleaner-active_record (2.2.1) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - datadog (2.15.0) - datadog-ruby_core_source (~> 3.4) - libdatadog (~> 16.0.1.1.0) + datadog (2.17.0) + datadog-ruby_core_source (~> 3.4, >= 3.4.1) + libdatadog (~> 18.1.0.1.0) libddwaf (~> 1.22.0.0.2) logger msgpack @@ -204,8 +204,8 @@ GEM devise-jwt (0.12.1) devise (~> 4.0) warden-jwt_auth (~> 0.10) - diff-lcs (1.6.1) - drb (2.2.1) + diff-lcs (1.6.2) + drb (2.2.3) dry-auto_inject (1.1.0) dry-core (~> 1.1) zeitwerk (~> 2.6) @@ -220,20 +220,22 @@ GEM htmlentities (~> 4.3.3) launchy (>= 2.1, < 4.0) mail (~> 2.7) + erb (5.0.1) erubi (1.13.1) - factory_bot (6.5.1) + factory_bot (6.5.4) activesupport (>= 6.1.0) - factory_bot_rails (6.4.4) + factory_bot_rails (6.5.0) factory_bot (~> 6.5) - railties (>= 5.0.0) + railties (>= 6.1.0) faraday (2.13.1) faraday-net_http (>= 2.0, < 3.5) json logger - faraday-net_http (3.4.0) + faraday-net_http (3.4.1) net-http (>= 0.5.0) - ferrum (0.15) + ferrum (0.17.1) addressable (~> 2.5) + base64 (~> 0.2) concurrent-ruby (~> 1.1) webrick (~> 1.7) websocket-driver (~> 0.7) @@ -266,7 +268,7 @@ GEM has_scope (0.8.2) actionpack (>= 5.2) activesupport (>= 5.2) - hashdiff (1.1.2) + hashdiff (1.2.0) high_voltage (4.0.0) htmlentities (4.3.4) i18n (1.14.7) @@ -295,7 +297,7 @@ GEM jmespath (1.6.2) jsbundling-rails (1.3.1) railties (>= 6.0.0) - json (2.11.3) + json (2.12.2) json-schema (5.1.1) addressable (~> 2.8) bigdecimal (~> 3.1) @@ -317,16 +319,16 @@ GEM addressable (~> 2.8) childprocess (~> 5.0) logger (~> 1.6) - libdatadog (16.0.1.1.0) + libdatadog (18.1.0.1.0) libddwaf (1.22.0.0.2) ffi (~> 1.0) listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - literal (1.7.1) + literal (1.8.1) zeitwerk logger (1.7.0) - loofah (2.24.0) + loofah (2.24.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -335,7 +337,7 @@ GEM net-pop net-smtp marcel (1.0.4) - matrix (0.4.2) + matrix (0.4.3) meta-tags (2.22.1) actionpack (>= 6.0.0, < 8.1) method_source (1.1.0) @@ -343,7 +345,7 @@ GEM benchmark logger mini_mime (1.1.5) - mini_portile2 (2.8.8) + mini_portile2 (2.8.9) minitest (5.25.5) msgpack (1.8.0) multi_json (1.15.0) @@ -363,13 +365,14 @@ GEM mini_portile2 (~> 2.8.2) racc (~> 1.4) orm_adapter (0.5.0) - ostruct (0.6.1) + ostruct (0.6.2) pg (1.5.9) - phlex (2.2.1) + phlex (2.3.1) zeitwerk (~> 2.7) - phlex-rails (2.2.0) - phlex (~> 2.2.1) + phlex-rails (2.3.1) + phlex (~> 2.3.0) railties (>= 7.1, < 9) + zeitwerk (~> 2.7) pp (0.6.2) prettyprint prettyprint (0.2.0) @@ -381,7 +384,7 @@ GEM pry-rescue (1.6.0) interception (>= 0.5) pry (>= 0.12.0) - psych (5.2.4) + psych (5.2.6) date stringio public_suffix (6.0.2) @@ -395,7 +398,7 @@ GEM rspec-mocks (~> 3.12) rspec-support (~> 3.12) racc (1.8.1) - rack (3.1.14) + rack (3.1.16) rack-session (2.1.1) base64 (>= 0.1.0) rack (>= 3.0.0) @@ -417,7 +420,7 @@ GEM activesupport (= 8.0.2) bundler (>= 1.15.0) railties (= 8.0.2) - rails-dom-testing (2.2.0) + rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) @@ -441,7 +444,7 @@ GEM rake (>= 12.2) thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) - rake (13.2.1) + rake (13.3.0) rake-compiler-dock (1.9.1) ransack (4.3.0) activerecord (>= 6.1.5) @@ -452,7 +455,8 @@ GEM ffi (~> 1.0) rb_sys (0.9.116) rake-compiler-dock (= 1.9.1) - rdoc (6.13.1) + rdoc (6.14.1) + erb psych (>= 4.0.0) regexp_parser (2.10.0) reline (0.6.1) @@ -462,21 +466,21 @@ GEM railties (>= 5.2) rexml (3.4.1) rouge (4.5.2) - rspec (3.13.0) + rspec (3.13.1) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) rspec-collection_matchers (1.2.1) rspec-expectations (>= 2.99.0.beta1) - rspec-core (3.13.3) + rspec-core (3.13.4) rspec-support (~> 3.13.0) - rspec-expectations (3.13.4) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.4) + rspec-mocks (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (8.0.0) + rspec-rails (8.0.1) actionpack (>= 7.2) activesupport (>= 7.2) railties (>= 7.2) @@ -484,7 +488,7 @@ GEM rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) - rspec-support (3.13.3) + rspec-support (3.13.4) rswag-api (2.16.0) activesupport (>= 5.2, < 8.1) railties (>= 5.2, < 8.1) @@ -496,7 +500,7 @@ GEM rswag-ui (2.16.0) actionpack (>= 5.2, < 8.1) railties (>= 5.2, < 8.1) - ruby-vips (2.2.3) + ruby-vips (2.2.4) ffi (~> 1.12) logger ruby_ui (1.0.1) @@ -527,18 +531,18 @@ GEM stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.7) - tailwind_merge (1.2.0) + tailwind_merge (1.3.1) sin_lru_redux (~> 2.5) tailwindcss-rails (4.2.3) railties (>= 7.0.0) tailwindcss-ruby (~> 4.0) - tailwindcss-ruby (4.1.5) + tailwindcss-ruby (4.1.10) temple (0.10.3) thor (1.3.2) tilt (2.6.0) timecop (0.9.10) timeout (0.4.3) - turbo-rails (2.0.13) + turbo-rails (2.0.16) actionpack (>= 7.1.0) railties (>= 7.1.0) tzinfo (2.0.6) @@ -562,7 +566,7 @@ GEM crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) webrick (1.9.1) - websocket-driver (0.7.7) + websocket-driver (0.8.0) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -570,7 +574,7 @@ GEM railties (>= 3.0.7) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.7.2) + zeitwerk (2.7.3) PLATFORMS ruby diff --git a/yarn.lock b/yarn.lock index 5fbb4fe..1891897 100644 --- a/yarn.lock +++ b/yarn.lock @@ -145,19 +145,19 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz#7fc114af5f6563f19f73324b5d5ff36ece0803d1" integrity sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g== -"@floating-ui/core@^1.7.0": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.0.tgz#1aff27a993ea1b254a586318c29c3b16ea0f4d0a" - integrity sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA== +"@floating-ui/core@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.1.tgz#1abc6b157d4a936174f9dbd078278c3a81c8bc6b" + integrity sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw== dependencies: "@floating-ui/utils" "^0.2.9" "@floating-ui/dom@^1.7.0": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.0.tgz#f9f83ee4fee78ac23ad9e65b128fc11a27857532" - integrity sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg== + version "1.7.1" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.1.tgz#76a4e3cbf7a08edf40c34711cf64e0cc8053d912" + integrity sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ== dependencies: - "@floating-ui/core" "^1.7.0" + "@floating-ui/core" "^1.7.1" "@floating-ui/utils" "^0.2.9" "@floating-ui/utils@^0.2.9": @@ -1526,9 +1526,9 @@ tslib@^2.7.0, tslib@^2.8.1: integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== tw-animate-css@^1.2.9: - version "1.2.9" - resolved "https://registry.yarnpkg.com/tw-animate-css/-/tw-animate-css-1.2.9.tgz#b25d5fb31fd3c3ec6d91c1d1842c0d7c0fdbd999" - integrity sha512-9O4k1at9pMQff9EAcCEuy1UNO43JmaPQvq+0lwza9Y0BQ6LB38NiMj+qHqjoQf40355MX+gs6wtlR6H9WsSXFg== + version "1.3.4" + resolved "https://registry.yarnpkg.com/tw-animate-css/-/tw-animate-css-1.3.4.tgz#635c5e8f9378ff6a98283850400e70859e0c2672" + integrity sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg== typescript@^5.4.5: version "5.8.3" From babcd560e3dde4b7691f85c1657176b55be66d72 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 19 Jun 2025 15:32:42 +0100 Subject: [PATCH 16/44] Change the way PagesController works and add the phonetic guide --- app/controllers/pages_controller.rb | 18 +- app/views/pages/guides/phonetic.html.haml | 77 --------- app/views/pages/guides/phonetic.rb | 126 ++++++++++++++ app/views/pages/privacy.html.haml | 190 ---------------------- app/views/pages/terms.html.haml | 169 ------------------- config/routes.rb | 4 +- 6 files changed, 141 insertions(+), 443 deletions(-) delete mode 100644 app/views/pages/guides/phonetic.html.haml create mode 100644 app/views/pages/guides/phonetic.rb delete mode 100644 app/views/pages/privacy.html.haml delete mode 100644 app/views/pages/terms.html.haml diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 352bd8d..bdfb534 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -3,10 +3,18 @@ class PagesController < ApplicationController layout -> { Components::ShellLayout.new } - def show - render Views::Pages.const_get(params[:id].camelize) - # rescue NameError - # # If const_get fails, raise a 404 - # raise AbstractController::ActionNotFound + VALID_PAGES = %w[ + about + privacy + terms + guides/phonetic + ] + + def page + if params[:path].in? VALID_PAGES + render Views::Pages.const_get(params[:path].camelize) + else + raise AbstractController::ActionNotFound + end end end diff --git a/app/views/pages/guides/phonetic.html.haml b/app/views/pages/guides/phonetic.html.haml deleted file mode 100644 index 00400dc..0000000 --- a/app/views/pages/guides/phonetic.html.haml +++ /dev/null @@ -1,77 +0,0 @@ -= page_section(title: 'How to write a phonetic spelling', page_title: true) do - %p Spelling and pronunciation are not closely linked in English. Without someone teaching you, how would you know the word "cough" is pronounced "COFF" but the word "through" is pronounced "THROO"? - %p This is especially true of names! Someone without help is never going to be able to work out that the surname "Featherstonhaugh" is pronounced "FAN-shaw". - %p A phonetic spelling is a way of spelling your name using common sequences of letters in English, which makes it easier for English speakers to understand how to say it without hearing it. Of course, name.pn also allows you to record your name in your own voice, but it's great to have both on your page in case someone is not able to listen to the recording at the time they view your page. -= page_section(title: 'The elements of a phonetic spelling') do - %p There are three things to consider when writing a phonetic spelling of your name: - %ol - %li Syllables - %li Spelling - %li Stress - %p In this guide, we'll explore these three elements one by one. -= page_section(title: 'Syllables') do - %p Syllables are the rhythmic sounds of your name. Every word has at least one syllable, but it may have many more. - %p A syllable usually has a vowel sound (or a pair of vowel sounds) in the middle and maybe consonants either side. - %p One way to imagine your name is in a song. The syllables would form the "notes" of the song. (If you can't think of a tune, try putting the name in the "Happy Birthday" song!) For example, try singing these names: - %dl - %dt Christopher - %dd 3 syllables: chris-to-pher - %dt Anna - %dd 2 syllables: a-nna - %dt Paige - %dd 1 syllable: paige (in the "Happy Birthday" song you might sing the vowel twice - "pai-aige" - to make two notes but that's not how you say it when speaking.) - %p In a phonetic spelling we show multiple syllables by separating them with the hyphen (-). -= page_section(title: 'Spelling') do - %p A phonetic spelling of a syllable reduces it to more "obvious" spellings for speakers of English. - %p For example, if your name is John, you might write "jon" so it's clear the h is silent. - %p Other examples (note, many of these have more than one possible pronunciation - you might pronounce your name differently to these): - %ul - %li Phoebefee-bee - %li Rhysrees - %li Niamhneev - %li Geoffreyjef-ree - %li Hughhyoo - %h3#schwa The schwa (É™) - %p A very common vowel sound in names is called the #{link_to 'schwa', 'https://en.wikipedia.org/wiki/Schwa'}. It's a kind of neutral vowel that doesn't have a standard spelling. - %p In fact, in many cases it's written without any letters at all. Consider the word rhythm - there are two syllables here but only one of them has a vowel in the spelling - the y. The vowel in the second syllable appears between th and m but it has no spelling. This is a schwa - and is sometimes written using its #{link_to 'IPA', 'https://en.wikipedia.org/wiki/International_Phonetic_Alphabet'} symbol, like this: rih-thÉ™m. - %p I'd recommend when writing a phonetic spelling of your name, you spell the syllable without a vowel, or use "uh" as an approximate spelling of the schwa. If you want to be really explicit, name.pn also lets you add the IPA spelling of your name alongside the phonetic spelling. - %p Some examples of schwas in names: - %ul - %li Michaelmy-kl (or my-kuhl) - %li Stephenstee-vn (or stee-vuhn) - %li Theresatuh-ree-suh - %h3#rhoticity The letter r - %p Be careful when including the letter r in your phonetic spelling, especially if you speak a "non-rhotic" variety of English, such as that spoken in most parts of England. - %p Speakers of these varieties of English tend not to pronounce the letter r unless it appears at the start of a syllable, but this is not true of rhotic varieties such as those spoken in Scotland and most of the USA. - %p For example, the name "Tyler" to speakers of non-rhotic English sounds more like "ty-luh", whereas rhotic speakers will sound out the letter r at the end of the name. - %p If you are from England or another area with a non-rhotic variety of English, you might be tempted to write the letter r in your phonetic spellings even though you don't pronounce it. For example, the name "Palmer" you might write "par-mer" because when you read this spelling, you don't pronounce the r. But be aware that speakers of rhotic varieties will think to pronounce every r. Instead, it might be better to write something like "pah-muh" so it is clear you do not pronounce the letter r. - %p As a result of this distinction, some common names are pronounced quite differently between rhotic and non-rhotic areas. The name "Eleanor" is typically pronounced "el-uh-nuh" in non-rhotic areas (such as English) and "el-uh-nor" (pronouncing both the o and the r) in rhotic areas (such as the USA). -= page_section(title: 'Stress') do - %p The last part to understand is stress. In English, words of more than one syllable always have a primary stress - this is one syllable of the word that is pronounced with a little more force than the other syllables. (Long words sometimes have a secondary stress too.) This is part of how people listening can tell where one word stops and the next starts. - %p Try sounding out this famous sentence: - %blockquote - It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife. - %p There are five words in this sentence with more than one syllable. In each case, one of the syllables is said with a little more force than the others: - %blockquote - It is a truth un-i-VER-sa-lly ack-NOW-ledged, that a SIN-gle man in poss-ES-sion of a good FOR-tune, must be in want of a wife. - %p The same is true of names. As some examples: - %ul - %li Jonathan → JON-a-than - %li Samantha → sam-AN-tha - %li Alexander → al-ex-AN-der - %li Marie → ma-RIE (although in French is it often pronounced MA-rie, like the word "marry") - %p If your name has more than one syllable, try saying it out loud a few times and listen for that stressed syllable. Try stressing other syllables to see how it sounds - and how it sounds "wrong" to your ears - this might help you identify the stressed syllable. - %p In phonetic spellings, we write the stressed syllable using CAPITAL LETTERS. If the word has only one syllable, it's conventional to write it in CAPITAL LETTERS too but it's not really important. -= page_section(title: 'Putting it all together') do - %p Now you know all the parts, all that's left is to combine them to get a great phonetic spelling of your own name. Here are some examples to get you on your way: - %ul - %li (My name) Quinn Daley → KWIN DAY-lee - %li Barack Obama → buh-RAHK oh-BAH-muh - %li Marsha P Johnson → MAR-shuh PEE JON-sun - %li Rabindranath Tagore → ruh-BIN-druh-naht ta-GOR - %li Ada Lovelace → AY-duh LUV-layce - %li Eleanor Roosevelt → EL-in-or ROSE-uh-velt -= page_section(title: 'Learning more') do - %p #{link_to 'The Wikipedia article on what they call "pronunciation respelling"', 'https://en.wikipedia.org/wiki/Pronunciation_respelling_for_English'} will guide you to more information about this way of writing English pronunciations. - %p #{link_to 'Learn more about the IPA', 'https://en.wikipedia.org/wiki/International_Phonetic_Alphabet'} for the other way of spelling the pronunciation of your name. - %p If you find phonetics fun and want to learn more with an engaging online guidebook and community, check out #{link_to 'Funetics', 'https://www.funetics.com/'}, from professional accent coach and phonetics enthusiast Luke Nicholson. \ No newline at end of file diff --git a/app/views/pages/guides/phonetic.rb b/app/views/pages/guides/phonetic.rb new file mode 100644 index 0000000..90ac44c --- /dev/null +++ b/app/views/pages/guides/phonetic.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +class Views::Pages::Guides::Phonetic < Views::Base + def view_template + Prose <<~MD + # #{title 'How to write a phonetic spelling'} + + Spelling and pronunciation are not closely linked in English. Without someone teaching you, how would you know the word "**cough**" is pronounced "**COFF**" but the word "**through**" is pronounced "**THROO**"? + + This is especially true of names! Someone without help is never going to be able to work out that the surname "**Featherstonhaugh**" is pronounced "**FAN-shaw**". + + A phonetic spelling is a way of spelling your name using common sequences of letters in English, which makes it easier for English speakers to understand how to say it without hearing it. Of course, name.pn also allows you to record your name in your own voice, but it's great to have both on your page in case someone is not able to listen to the recording at the time they view your page. + + ## The elements of a phonetic spelling + + There are three things to consider when writing a phonetic spelling of your name: + + 1. Syllables + 2. Spelling + 3. Stress + + In this guide, we'll explore these three elements one by one. + + ## Syllables + + Syllables are the rhythmic sounds of your name. Every word has at least one syllable, but it may have many more. + + A syllable usually has a vowel sound (or a pair of vowel sounds) in the middle and maybe consonants either side. + + One way to imagine your name is in a song. The syllables would form the "notes" of the song. (If you can't think of a tune, try putting the name in the "Happy Birthday" song!) For example, try singing these names: + + Christopher + : 3 syllables: **chris-to-pher** + + Anna + : 2 syllables: **a-nna** + + Paige + : 1 syllable: **paige** (in the "Happy Birthday" song you might sing the vowel twice - "pai-aige" - to make two notes but that's not how you say it when speaking.) + + In a phonetic spelling we show multiple syllables by separating them with the hyphen (-). + + ## Spelling + + A phonetic spelling of a syllable reduces it to more "obvious" spellings for speakers of English. + + For example, if your name is **John**, you might write "**jon**" so it's clear the _h_ is silent. + + Other examples (note, many of these have more than one possible pronunciation - you might pronounce your name differently to these): + + * **Phoebe** → **fee-bee** + * **Rhys** → **rees** + * **Niamh** → **neev** + * **Geoffrey** → **jef-ree** + * **Hugh** → **hyoo** + + ### The schwa (É™) + + A very common vowel sound in names is called the [schwa](https://en.wikipedia.org/wiki/Schwa). It's a kind of neutral vowel that doesn't have a standard spelling. + + In fact, in many cases it's written without any letters at all. Consider the word **rhythm** - there are two syllables here but only one of them has a vowel in the spelling - the _y_. The vowel in the second syllable appears between _th_ and _m_ but it has no spelling. This is a schwa - and is sometimes written using its [IPA](https://en.wikipedia.org/wiki/International_Phonetic_Alphabet) symbol, like this: **rih-thÉ™m**. + + I'd recommend when writing a phonetic spelling of your name, you spell the syllable without a vowel, or use "uh" as an approximate spelling of the schwa. If you want to be really explicit, name.pn also lets you add the IPA spelling of your name alongside the phonetic spelling. + + Some examples of schwas in names: + + * **Michael** → **my-kl** (or **my-kuhl**) + * **Stephen** → **stee-vn** (or **stee-vuhn**) + * **Theresa** → **tuh-ree-suh** + + ### The letter r + + Be careful when including the letter _r_ in your phonetic spelling, especially if you speak a "non-rhotic" variety of English, such as that spoken in most parts of England. + + Speakers of these varieties of English tend not to pronounce the letter _r_ unless it appears at the start of a syllable, but this is not true of rhotic varieties such as those spoken in Scotland and most of the USA. + + For example, the name "**Tyler**" to speakers of non-rhotic English sounds more like "**ty-luh**", whereas rhotic speakers will sound out the letter _r_ at the end of the name. + + If you are from England or another area with a non-rhotic variety of English, you might be tempted to write the letter _r_ in your phonetic spellings even though you don't pronounce it. For example, the name "Palmer" you might write "_par-mer_" because when you read this spelling, you don't pronounce the _r_. But be aware that speakers of rhotic varieties will think to pronounce every _r_. Instead, it might be better to write something like "**pah-muh**" so it is clear you do not pronounce the letter _r_. + + As a result of this distinction, some common names are pronounced quite differently between rhotic and non-rhotic areas. The name "**Eleanor**" is typically pronounced "**el-uh-nuh**" in non-rhotic areas (such as England) and "**el-uh-nor**" (pronouncing both the _o_ and the _r_) in rhotic areas (such as the USA). + + ## Stress + + The last part to understand is _stress_. In English, words of more than one syllable always have a **primary stress** - this is one syllable of the word that is pronounced with a little more force than the other syllables. (Long words sometimes have a secondary stress too.) This is part of how people listening can tell where one word stops and the next starts. + + Try sounding out this famous sentence: + + > It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife. + + There are five words in this sentence with more than one syllable. In each case, one of the syllables is said with a little more force than the others: + + > It is a truth un-i-**VER**-sa-lly ack-**NOW**-ledged, that a **SIN**-gle man in poss-**ES**-sion of a good **FOR**-tune, must be in want of a wife. + + The same is true of names. As some examples: + + * Jonathan → **JON**-a-than + * Samantha → sam-**AN**-tha + * Alexander → al-ex-**AN**-der + * Marie → ma-**RIE** (although in French is it often pronounced **MA**-rie, like the word "marry") + + If your name has more than one syllable, try saying it out loud a few times and listen for that stressed syllable. Try stressing other syllables to see how it sounds - and how it sounds "wrong" to your ears - this might help you identify the stressed syllable. + + In phonetic spellings, we write the stressed syllable using CAPITAL LETTERS. If the word has only one syllable, it's conventional to write it in CAPITAL LETTERS too but it's not really important. + + ## Putting it all together + + Now you know all the parts, all that's left is to combine them to get a great phonetic spelling of your own name. Here are some examples to get you on your way: + + * (My name) Quinn Daley → **KWIN DAY-lee** + * Barack Obama → **buh-RAHK oh-BAH-muh** + * Marsha P Johnson → **MAR-shuh PEE JON-sun** + * Rabindranath Tagore → **ruh-BIN-druh-naht ta-GOR** + * Ada Lovelace → **AY-duh LUV-layce** + * Eleanor Roosevelt → **EL-in-or ROSE-uh-velt** + + ## Learning more + + [The Wikipedia article on what they call "pronunciation respelling"](https://en.wikipedia.org/wiki/Pronunciation_respelling_for_English) will guide you to more information about this way of writing English pronunciations. + + [Learn more about the IPA](https://en.wikipedia.org/wiki/International_Phonetic_Alphabet) for the other way of spelling the pronunciation of your name. + + If you find phonetics fun and want to learn more with an engaging online guidebook and community, check out [Funetics](https://www.funetics.com/), from professional accent coach and phonetics enthusiast Luke Nicholson. + MD + end +end diff --git a/app/views/pages/privacy.html.haml b/app/views/pages/privacy.html.haml deleted file mode 100644 index 3587386..0000000 --- a/app/views/pages/privacy.html.haml +++ /dev/null @@ -1,190 +0,0 @@ -= page_section(title: 'Privacy policy', page_title: true, id: 'contents') do - %p This is the name.pn privacy policy. We want to be clear about how we will use the information you give us and keep it safe. - %p It tells you how we look after your personal data, like your name or photo, when you visit our website wherever you visit it from. It tells you about your privacy rights and how the law protects you. - %p This privacy policy is provided in a layered format so you can click through to the specific areas set out below. - %p name.pn puts you in control - your name, your rules. The short-URL we create and identity data is set and managed by you, and you choose what is made public by using our service. - %p name.pn will not ask you for personal information about yourself except where it will be published on your own name.pn page. Apart from a few fields needed to ensure a uniform experience, most data requested is optional and you only need to provide it if you want it to be on your page. - %ul - %li= link_to '1. Important information and who we are', '#glossary' - %li= link_to '2. The data we collect about you', '#what' - %li= link_to '3. How is your personal data collected?', '#how' - %li= link_to '4. How we use your personal data', '#usage' - %li= link_to '5. Disclosures of your personal data', '#disclosures' - %li= link_to '6. International transfers', '#international' - %li= link_to '7. Data security', '#security' - %li= link_to '8. Data retention', '#retention' - %li= link_to '9. Your legal rights', '#rights' -= page_section(id: 'glossary', title: '1. Important information and who we are') do - %h3 Purpose of this privacy policy - %p This privacy policy gives you information on how name.pn collects and uses your personal data when you visit this website, including any data you may provide through this website when you use our service. - %p We only use the data you provide us, and don’t collect data about you from anywhere else. - %p This website is not intended for children and we do not knowingly collect data relating to children. - %p It is important that you read this privacy policy together with any other information we may provide on specific occasions when we are collecting or processing personal data about you. For example, if we were to offer new services or email contact options. - %p This privacy policy supplements other notices and privacy policies and is not intended to override them. - %p Third parties that may be involved in processing your data are: - %ul - %li #{link_to 'Heroku Postgres', 'https://www.heroku.com/postgres'}: Your profile data is stored and backed up here. - %li #{link_to 'Amazon Web Services', 'https://aws.amazon.com/'}: If you upload images or audio, they are stored here. - %li #{link_to 'Buttondown', 'https://buttondown.email/'}: We use your email address and name preferences to send you occasional emails, if you have opted in. - %li #{link_to 'Google Analytics', 'https://analytics.google.com/'}: We use this to understand more about who is visiting our site. - %li #{link_to 'Datadog', 'https://www.datadoghq.com/'}: We send technical data about the functioning of the application to Datadog in order to predict and diagnose issues with the site. - %h3 Controller - %p name.pn is the controller and responsible for your personal data. We are referred to as name.pn, "we", "us" or "our" in this privacy policy. - %p If you have any questions about this privacy policy, including any requests to exercise #{link_to 'your legal rights', '#rights'}, please contact us using the details set out below. - %h3 Contact details - %p If you have any questions about this privacy policy, you can contact us at: - %ul - %li Full name of legal entity: Fish Percolator Limited - %li Email address: privacy@fishpercolator.co.uk - %li Postal address: International House, 14 King Street, Leeds LS1 2HL, United Kingdom - %p You have the right to make a complaint at any time to the #{link_to 'Information Commissioner\'s Office (ICO)', 'https://www.ico.org.uk/'}, the UK regulator for data protection issues. Our ICO registration is ZB138365. - %p We would appreciate the chance to deal with your concerns before you approach the ICO. - %h3 Changes to the privacy policy and your duty to inform us of changes - %p We keep our privacy policy under regular review. This version is dated 22 June 2022. - %p It is important that the personal data we hold about you is accurate and current. Our service lets you ensure the personal data we hold can be updated by you when needed. - %h3 Third-party links - %p This website may include links to third-party websites, plug-ins and applications. Clicking on those links or enabling those connections may allow third parties to collect or share data about you. We do not control these third-party websites and are not responsible for their privacy statements. When you leave our website, we encourage you to read the privacy policy of every website you visit. -= page_section(id: 'what', title: '2. The data we collect about you') do - %p Personal data, or personal information, means any information about an individual from which that person can be identified - like your name or photo. It does not include data where the identity has been removed (anonymous data). - %p We may collect, use, store and transfer different kinds of personal data about you which we have grouped together as follows: - %ul - %li Identity Data includes various forms of your personal name, your photo/likeness and your social media profiles. - %li Contact Data includes email address. - %li Technical Data includes internet protocol (IP) address, your login data, browser type and version, and the times of your visits. - %li Usage Data includes information about how you use our website. - %li Marketing and Communications Data includes your preferences in receiving marketing from us and your communication preferences. - %p We also collect, use and share Aggregated Data such as statistical or demographic data for any purpose. Aggregated Data could be derived from your personal data but is not considered personal data in law as this data will not directly or indirectly reveal your identity. For example, we may aggregate your Usage Data to calculate the percentage of users accessing a specific website feature. - %p However, if we combine or connect Aggregated Data with your personal data so that it can directly or indirectly identify you, we treat the combined data as personal data which will be used in accordance with this privacy policy. - %p We do not collect any Special Categories of Personal Data about you (this includes details about your race or ethnicity, religious or philosophical beliefs, sex life, sexual orientation, political opinions, trade union membership, information about your health, and genetic and biometric data). Nor do we collect any information about criminal convictions and offences. -= page_section(id: 'how', title: '3. How is your personal data collected?') do - %p We use different methods to collect data from and about you including: - %ul - %li - Personal interactions. You may give us your Identity, Contact and Financial Data by filling in forms or by sending us an email. This includes personal data you provide when you: - %ul - %li create an account on our website; - %li fill out your public profile information using our website; - %li subscribe to our service or publications; - %li request marketing to be sent to you; - %li give us feedback or contact us. - %li - Automated technologies or interactions. As you interact with our website, we will automatically collect Technical Data about your equipment, browsing actions and patterns. We collect this personal data by using cookies, server logs and other similar technologies. Technical Data from the following parties: - %ul - %li analytics providers such as Google based outside the UK; - %li advertising networks such as Instagram based outside the UK; and - %li search information providers such as Google based outside the UK. -= page_section(id: 'usage', title: '4. How we use your personal data') do - %p We will only use your personal data as the law allows us to. We make sure we have a clear legal basis. We will use your personal data in the following circumstances: - %ul - %li Where it is necessary for our legitimate interests (or those of a third party) and your interests and fundamental rights do not override those interests. We have a legitimate interest in offering this service to allow people to better communicate their name and personal pronouns on the internet. - %li Where we need to comply with a legal obligation. - %p We do not use consent as a legal basis for processing your personal data, but we will get your consent before sending third party direct marketing communications to you via email or text message. - %p You have the right to withdraw consent to marketing at any time by contacting us or by using #{link_to 'the opt-out form available on your account page', edit_user_registration_path}. - %h3 Purposes for which we will use your personal data - %p We have set out a table of all the ways we plan to use your personal data, and which of the legal bases we rely on to do so. We have also identified what our legitimate interests are where appropriate. - %p We may process your personal data for more than one lawful ground depending on the specific purpose for which we are using your data. - %p Please contact us if you need details about the specific legal ground we are relying on to process your personal data where more than one ground has been set out in the table below. - %table - %thead - %tr - %th Purpose - %th Type of data - %th Lawful basis for processing - %tbody - %tr - %td To register you as a new user - %td - %ol.mt-0.ml-5 - %li Identity - %li Contact - %td Legitimate interest - %tr - %td - To manage our relationship with you which will include: - %ul.mt-0 - %li Notifying you about changes to our terms or privacy policy - %li Receiving email updates - %td - %ol.mt-0.ml-5 - %li Identity - %li Contact - %li Profile - %li Marketing and Communications - %td Legitimate interest - %tr - %td To administer and protect our business and this website (including troubleshooting, data analysis, testing, system maintenance, support, reporting and hosting of data) - %td - %ol.mt-0.ml-5 - %li Identity - %li Contact - %li Technical - %td Legitimate interest; Necessary to comply with a legal obligation - %tr - %td To use data analytics to improve our website, products/services, marketing, customer relationships and experiences - %td - %ol.mt-0.ml-5 - %li Technical - %li Usage - %td Necessary for our legitimate interests (to define types of customers for our products and services, to keep our website updated and relevant, to develop our business and to inform our marketing strategy) - %h3 Opting out - %p You can ask us or third parties to stop sending you marketing messages at any time by logging into the website and unsubscribing from #{link_to 'your account page', edit_user_registration_path} OR by following the opt-out links on any marketing message sent to you OR by contacting us at any time. - %p Where you opt out of receiving these marketing messages, this will not apply to personal data provided to us as a result of a product/service purchase, warranty registration, product/service experience or other transactions. - %h3 Cookies - %p You can set your browser to refuse all or some browser cookies, or to alert you when websites set or access cookies. If you disable or refuse cookies, please note that some parts of this website may become inaccessible or not function properly. - %p The cookies we use are: - %dl - %dt - %tt _name_pn_session - %dd Used to identify your current browser session and store encrypted information related to whether you are logged in and what user you are logged in as. - %dt - %tt remember_user_token - %dd If you select 'remember me' on the login page, this cookie will be used to log you back in when you return to name.pn in a future session. - %dt - %tt _ga - %dd Used by Google Analytics to collect the analytics data described above. - %h3 Change of purpose - %p We will only use your personal data for the purposes for which we collected it, unless we reasonably consider that we need to use it for another reason and that reason is compatible with the original purpose. - %p If you wish to get an explanation as to how the processing for the new purpose is compatible with the original purpose, please contact us. - %p If we need to use your personal data for an unrelated purpose, we will notify you and we will explain the legal basis which allows us to do so. - %p Please note that we may process your personal data without your knowledge or consent, in compliance with the above rules, where this is required or permitted by law. -= page_section(id: 'disclosures', title: '5. Disclosures of your personal data') do - %p We may share your personal data with the parties set out below for the purposes set out in the table above. - %ul - %li Internal Third Parties as set out in the #{link_to 'Glossary', '#glossary'}. - %li External Third Parties as set out in the #{link_to 'Glossary', '#glossary'}. - %li Third parties to whom we may choose to sell, transfer or merge parts of our business or our assets. Alternatively, we may seek to acquire other businesses or merge with them. If a change happens to our business, then the new owners may use your personal data in the same way as set out in this privacy policy. - %p We require all third parties to respect the security of your personal data and to treat it in accordance with the law. We do not allow our third-party service providers to use your personal data for their own purposes and only permit them to process your personal data for specified purposes and in accordance with our instructions. -= page_section(id: 'international', title: '6. International transfers') do - %p Many of our external third parties are based outside the UK so their processing of your personal data will involve a transfer of data outside the UK - %p Whenever we transfer your personal data out of the UK, we ensure a similar degree of protection is afforded to it by ensuring at least one of the following safeguards is implemented: - %ul - %li We will only transfer your personal data to countries that have been deemed to provide an adequate level of protection for personal data. - %li Where we use certain service providers, we may use specific contracts approved for use in the UK which give personal data the same protection it has in the UK. - %p Please contact us if you want further information on the specific mechanism used by us when transferring your personal data out of the UK. -= page_section(id: 'security', title: '7. Data security') do - %p We have put in place appropriate security measures to prevent your personal data from being accidentally lost, used or accessed in an unauthorised way, altered by people other than you or disclosed. - %p In addition, we limit access to your personal data to those employees, agents, contractors and other third parties who have a business need to know. They will only process your personal data on our instructions and they are subject to a duty of confidentiality. The duty of confidentiality does not extend to information you have made public, but does apply to private information you provide to us such as your email address and IP address. - %p We have put in place procedures to deal with any suspected personal data breach and will notify you and any applicable regulator of a breach where we are legally required to do so. -= page_section(id: 'retention', title: '8. Data retention') do - %h3 How long will you use my personal data for? - %p We will only keep your personal data for as long as reasonably necessary to fulfil the purposes we collected it for. You can choose to keep using name.pn as long as you like, and while using it your personal data will be retained. If you want to stop using name.pn, your data will be deleted from the main application immediately and from backups in 4 weeks. - %p We may keep your personal data for a longer period in the event of a complaint or if we reasonably believe there is a prospect of litigation in respect to our relationship with you. - %p To determine the appropriate retention period for personal data, we consider the amount, nature and sensitivity of the personal data, the potential risk of harm from unauthorised use or disclosure of your personal data, the purposes for which we process your personal data and whether we can achieve those purposes through other means. - %p In some circumstances you can ask us to delete your data: see #{link_to 'your legal rights', '#rights'} below for further information. - %p In some circumstances we will anonymise your personal data (so that it can no longer be associated with you) for research or statistical purposes, in which case we may use this information indefinitely without further notice to you. -= page_section(id: 'rights', title: '9. Your legal rights') do - %p You have rights under data protection laws in relation to your personal data. Please click on the links below to find out more about these rights: - %ul - %li #{link_to 'Request access to your personal data', 'https://ico.org.uk/your-data-matters/your-right-to-get-copies-of-your-data/'} - you can view most of your personal data by using our service. You can request a copy of any other information we hold about you. - %li #{link_to 'Request correction of your personal data', 'https://ico.org.uk/your-data-matters/your-right-to-get-your-data-corrected/'} - you can correct most of your personal data by using our service. You can request a correction of any other information we hold about you. - %li #{link_to 'Request erasure of your personal data', 'https://ico.org.uk/your-data-matters/your-right-to-get-your-data-deleted/'} - you can erase or delete most of your personal data by using our service. You can request erasure of any other information we hold about you. - %li #{link_to 'Object to processing of your personal data', 'https://ico.org.uk/your-data-matters/the-right-to-object-to-the-use-of-your-data/'} - you can object to our use of your personal data where this is outside the use of our service, which you are in control of. - %li #{link_to 'Request restriction of processing your personal data', 'https://ico.org.uk/your-data-matters/your-right-to-limit-how-organisations-use-your-data/'} - you can request that we restrict the further use of your personal data. - %li #{link_to 'Request transfer of your personal data', 'https://ico.org.uk/your-data-matters/your-right-to-data-portability/'} - you can use our service to receive a transfer of your personal data. - %p If you wish to request any of the rights set out above, please contact privacy@fishpercolator.co.uk. - %h3 No fee usually required - %p You will not have to pay a fee to access your personal data (or to exercise any of the other rights). However, we may charge a reasonable fee if your request is clearly unfounded, repetitive or excessive. Alternatively, we could refuse to comply with your request in these circumstances. - %h3 What we may need from you - %p We may need to request specific information from you to help us confirm your identity and ensure your right to access your personal data (or to exercise any of your other rights). This is a security measure to ensure that personal data is not disclosed to any person who has no right to receive it. We may also contact you to ask you for further information in relation to your request to speed up our response. - %h3 Time limit to respond - %p We try to respond to all legitimate requests within one month. Occasionally it could take us longer than a month if your request is particularly complex or you have made a number of requests. In this case, we will notify you and keep you updated. diff --git a/app/views/pages/terms.html.haml b/app/views/pages/terms.html.haml deleted file mode 100644 index 48e359e..0000000 --- a/app/views/pages/terms.html.haml +++ /dev/null @@ -1,169 +0,0 @@ -= page_section(title: 'Terms and conditions of use', page_title: true) -= page_section(title: '1. Introduction') do - %p 1.1 These terms and conditions shall govern your use of our website. - %p 1.2 By using our website, you accept these terms and conditions in full; accordingly, if you disagree with these terms and conditions or any part of these terms and conditions, you must not use our website. - %p 1.3 If you register with our website, submit any material to our website or use any of our website services, we will ask you to expressly agree to these terms and conditions. - %p 1.4 You must be at least 15 years of age to use our website; by using our website or agreeing to these terms and conditions, you warrant and represent to us that you are at least 15 years of age. -= page_section(title: '2. Copyright notice') do - %p 2.1 Copyright (c) 2021 Fish Percolator Limited. - %p 2.2 Subject to the express provisions of these terms and conditions: - %ul - %li (a) we, together with our licensors and other contributors to our codebase, own and control all the copyright and other intellectual property rights in our website and the material on our website; - %li (b) this program is free software: you can redistribute it and/or modify it under the terms of the #{link_to 'GNU Affero General Public License', 'https://www.gnu.org/licenses/agpl-3.0.en.html'} as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version; and - %li (c) all other copyright and other intellectual property rights in our website and the material on our website are reserved. -= page_section(title: '3. Permission to use website') do - %p 3.1 You may: - %ul - %li (a) view pages from our website in a web browser; - %li (b) download pages from our website for caching in a web browser; - %li (c) print pages from our website for your own personal and non-commercial use, providing that such printing is not systematic or excessive; - %li (d) stream audio files from our website using the media player on our website; - %li (e) use our website services by means of a web browser; and - %li (f) share your own personal content on our website, - %p subject to the other provisions of these terms and conditions. - %p 3.2 Except as expressly permitted by Section 3.1 or the other provisions of these terms and conditions, you must not download any material from our website or save any such material to your computer. - %p 3.3 You may only use our website for your own personal and business purposes; you must not use our website for any other purposes. - %p 3.4 Except as expressly permitted by these terms and conditions, you must not edit or otherwise modify any material on our website. - %p 3.5 Unless you own or control the relevant rights in the material, you must not: - %ul - %li (a) republish material from our website (including republication on another website); - %li (b) sell, rent or sub-license material from our website; - %li (c) show any material from our website in public; - %li (d) exploit material from our website for a commercial purpose; or - %li (e) redistribute material from our website. - %p 3.6 Notwithstanding Section 3.5, you may redistribute our newsletter in print and electronic form to any person. - %p 3.7 We reserve the right to suspend or restrict access to our website, to areas of our website and/or to functionality upon our website. We may, for example, suspend access to the website during server maintenance or when we update the website. You must not circumvent or bypass, or attempt to circumvent or bypass, any access restriction measures on the website. -= page_section(title: '4. Misuse of website') do - %p 4.1 You must not: - %ul - %li (a) use our website in any way or take any action that causes, or may cause, damage to the website or impairment of the performance, availability, accessibility, integrity or security of the website; - %li (b) use our website in any way that is unlawful, illegal, fraudulent or harmful, or in connection with any unlawful, illegal, fraudulent or harmful purpose or activity; - %li (c) hack or otherwise tamper with our website; - %li (d) probe, scan or test the vulnerability of our website without our permission; - %li (e) circumvent any authentication or security systems or processes on or relating to our website; - %li (f) use our website to copy, store, host, transmit, send, use, publish or distribute any material which consists of (or is linked to) any spyware, computer virus, Trojan horse, worm, keystroke logger, rootkit or other malicious computer software; - %li (g) impose an unreasonably large load on our website resources (including bandwidth, storage capacity, processing capacity and automation using the provided API); - %li (h) decrypt or decipher any communications sent by or to our website without our permission; - %li (i) conduct any systematic or automated data collection activities (including without limitation scraping, data mining, data extraction and data harvesting) on or in relation to our website without our express written consent; - %li (j) access or otherwise interact with our website using any robot, spider or other automated means, except by using the provided API, or for the purpose of search engine indexing; - %li (l) violate the directives set out in the robots meta tag on each page of our website; - %li (m) use data collected from our website for any direct marketing activity (including without limitation email marketing, SMS marketing, telemarketing and direct mailing); or - %li (n) do anything that interferes with the normal use of our website. - %p 4.2 You must not use data collected from our website to contact individuals, companies or other persons or entities, except where users have chosen to provide links to their contact details. - %p 4.3 You must ensure that all the information you supply to us through our website, or in relation to our website, is true, accurate, current, complete and non-misleading. Your name is true if it is the one you use in your day-to-day life; you are not in violation of this clause by using a true name other than the one legally recognised in your jurisdiction. -= page_section(title: '5. Registration and accounts') do - %p 5.1 You may register for an account with our website by completing and submitting the account registration form on our website. - %p 5.2 You must not allow any other person to use your account to access the website. - %p 5.3 You must notify us in writing immediately if you become aware of any unauthorised use of your account. - %p 5.4 You must not use any other person's account to access the website, unless you have that person's express permission to do so. -= page_section(title: '6. User login details') do - %p 6.1 If you register for an account with our website, you will be asked to choose a login email address and password. - %p 6.2 You must not use your account or user ID for or in connection with the impersonation of any person. - %p 6.3 You must keep your password confidential. - %p 6.4 You must notify us in writing immediately if you become aware of any disclosure of your password. - %p 6.5 You are responsible for any activity on our website arising out of any failure to keep your password confidential, and may be held liable for any losses arising out of such a failure. -= page_section(title: '7. Cancellation and suspension of account') do - %p 7.1 We may: - %ul - %li (a) suspend your account; - %li (b) cancel your account; and/or - %li (c) edit your account details, - %p at any time in our sole discretion with or without notice to you. - %p 7.2 You may cancel your account on our website using your account control panel on the website. -= page_section(title: '8. Our rights to use your content') do - %p 8.1 In these terms and conditions, "your content" means all works and materials (including without limitation text, graphics, images, audio material, video material, audio-visual material, scripts, software and files) that you submit to us or our website for storage or publication on, processing by, or transmission via, our website. - %p 8.2 You grant to us a worldwide, non-exclusive, royalty-free licence to reproduce, store and publish your content on and in relation to this website and any successor website. - %p 8.3 You grant to us the right to sub-license the rights licensed under Section 8.2. - %p 8.4 You grant to us the right to bring an action for infringement of the rights licensed under Section 8.2. - %p 8.5 You may edit your content to the extent permitted using the editing functionality made available on our website. - %p 8.6 Without prejudice to our other rights under these terms and conditions, if you breach any provision of these terms and conditions in any way, or if we reasonably suspect that you have breached these terms and conditions in any way, we may delete, unpublish or edit any or all of your content. -= page_section(title: '9. Rules about your content') do - %p 9.1 You warrant and represent that your content will comply with these terms and conditions. - %p 9.2 Your content must not be illegal or unlawful, must not infringe any person's legal rights, and must not be capable of giving rise to legal action against any person (in each case in any jurisdiction and under any applicable law). - %p 9.3 Your content, and the use of your content by us in accordance with these terms and conditions, must not: - %ul - %li (a) be libellous or maliciously false; - %li (b) be obscene or indecent; - %li (c) infringe any copyright, moral right, database right, trade mark right, design right, right in passing off or other intellectual property right; - %li (d) infringe any right of confidence, right of privacy or right under data protection legislation; - %li (e) constitute negligent advice or contain any negligent statement; - %li (f) constitute an incitement to commit a crime, instructions for the commission of a crime or the promotion of criminal activity; - %li (g) be in contempt of any court or in breach of any court order; - %li (h) be in breach of racial or religious hatred or discrimination legislation; - %li (i) be in breach of official secrets legislation; - %li (j) be in breach of any contractual obligation owed to any person; - %li (k) depict violence; - %li (l) be pornographic or sexually explicit; - %li (m) be untrue, false, inaccurate or misleading; - %li (n) consist of or contain any instructions, advice or other information which may be acted upon and could, if acted upon, cause illness, injury or death, or any other loss or damage; - %li (o) constitute spam; - %li (p) be offensive, deceptive, fraudulent, threatening, abusive, harassing, anti-social, menacing, hateful, discriminatory or inflammatory; or - %li (r) cause annoyance, inconvenience or needless anxiety to any person. -= page_section(title: '10. Limited warranties') do - %p 10.1 We do not warrant or represent: - %ul - %li (a) the completeness or accuracy of the information published on our website; - %li (b) that the material on the website is up to date; - %li (c) that the website will operate without fault; or - %li (d) that the website or any service on the website will remain available. - %p 10.2 We reserve the right to discontinue or alter any or all of our website services, and to stop publishing our website, at any time in our sole discretion without notice or explanation; and save to the extent expressly provided otherwise in these terms and conditions, you will not be entitled to any compensation or other payment upon the discontinuance or alteration of any website services, or if we stop publishing the website. - %p 10.3 To the maximum extent permitted by applicable law and subject to Section 11.1, we exclude all representations and warranties relating to the subject matter of these terms and conditions, our website and the use of our website. -= page_section(title: '11. Limitations and exclusions of liability') do - %p 11.1 Nothing in these terms and conditions will: - %ul - %li (a) limit or exclude any liability for death or personal injury resulting from negligence; - %li (b) limit or exclude any liability for fraud or fraudulent misrepresentation; - %li (c) limit any liabilities in any way that is not permitted under applicable law; or - %li (d) exclude any liabilities that may not be excluded under applicable law. - %p 11.2 The limitations and exclusions of liability set out in this Section 11 and elsewhere in these terms and conditions: - %ul - %li (a) are subject to Section 11.1; and - %li (b) govern all liabilities arising under these terms and conditions or relating to the subject matter of these terms and conditions, including liabilities arising in contract, in tort (including negligence) and for breach of statutory duty, except to the extent expressly provided otherwise in these terms and conditions. - %p 11.3 To the extent that our website and the information and services on our website are provided free of charge, we will not be liable for any loss or damage of any nature. - %p 11.4 We will not be liable to you in respect of any losses arising out of any event or events beyond our reasonable control. - %p 11.5 We will not be liable to you in respect of any business losses, including (without limitation) loss of or damage to profits, income, revenue, use, production, anticipated savings, business, contracts, commercial opportunities or goodwill. - %p 11.6 We will not be liable to you in respect of any loss or corruption of any data, database or software. - %p 11.7 We will not be liable to you in respect of any special, indirect or consequential loss or damage. - %p 11.8 You accept that we have an interest in limiting the personal liability of our officers and employees and, having regard to that interest, you acknowledge that we are a limited liability entity; you agree that you will not bring any claim personally against our officers or employees in respect of any losses you suffer in connection with the website or these terms and conditions (this will not, of course, limit or exclude the liability of the limited liability entity itself for the acts and omissions of our officers and employees). -= page_section(title: '12. Breaches of these terms and conditions') do - %p 12.1 Without prejudice to our other rights under these terms and conditions, if you breach these terms and conditions in any way, or if we reasonably suspect that you have breached these terms and conditions in any way, we may: - %ul - %li (a) send you one or more formal warnings; - %li (b) temporarily suspend your access to our website; - %li (c) permanently prohibit you from accessing our website; - %li (d) block computers using your IP address from accessing our website; - %li (e) contact any or all of your internet service providers and request that they block your access to our website; - %li (f) commence legal action against you, whether for breach of contract or otherwise; and/or - %li (g) suspend or delete your account on our website. - %p 12.2 Where we suspend or prohibit or block your access to our website or a part of our website, you must not take any action to circumvent such suspension or prohibition or blocking (including without limitation creating and/or using a different account). -= page_section(title: '13. Variation') do - %p 13.1 We may revise these terms and conditions from time to time. - %p 13.2 The revised terms and conditions shall apply to the use of our website from the date of publication of the revised terms and conditions on the website, and you hereby waive any right you may otherwise have to be notified of, or to consent to, revisions of these terms and conditions. - %p 13.3 If you have given your express agreement to these terms and conditions, we will ask for your express agreement to any revision of these terms and conditions; and if you do not give your express agreement to the revised terms and conditions within such period as we may specify, we will disable or delete your account on the website, and you must stop using the website. -= page_section(title: '14. Assignment') do - %p 14.1 You hereby agree that we may assign, transfer, sub-contract or otherwise deal with our rights and/or obligations under these terms and conditions. - %p 14.2 You may not without our prior written consent assign, transfer, sub-contract or otherwise deal with any of your rights and/or obligations under these terms and conditions. -= page_section(title: '15. Severability') do - %p 15.1 If a provision of these terms and conditions is determined by any court or other competent authority to be unlawful and/or unenforceable, the other provisions will continue in effect. - %p 15.2 If any unlawful and/or unenforceable provision of these terms and conditions would be lawful or enforceable if part of it were deleted, that part will be deemed to be deleted, and the rest of the provision will continue in effect. -= page_section(title: '16. Third party rights') do - %p 16.1 A contract under these terms and conditions is for our benefit and your benefit, and is not intended to benefit or be enforceable by any third party. - %p 16.2 The exercise of the parties' rights under a contract under these terms and conditions is not subject to the consent of any third party. -= page_section(title: '17. Entire agreement') do - %p 17.1 Subject to Section 11.1, these terms and conditions, together with #{link_to 'our privacy and cookies policy', page_path('privacy')}, shall constitute the entire agreement between you and us in relation to your use of our website and shall supersede all previous agreements between you and us in relation to your use of our website. -= page_section(title: '18. Law and jurisdiction') do - %p 18.1 These terms and conditions shall be governed by and construed in accordance with English law. - %p 18.2 Any disputes relating to these terms and conditions shall be subject to the exclusive jurisdiction of the courts of England. -= page_section(title: '19. Statutory and regulatory disclosures') do - %p 19.1 We are registered with the #{link_to 'Information Commissioner\'s Office', 'https://ico.org.uk/'} and our registration number is ZB138365. - %p 19.2 We subscribe to the Contributor Covenant Code of Conduct, which can be consulted electronically at #{link_to 'contributor-covenant.org', 'https://www.contributor-covenant.org/'}. - %p 19.3 Our VAT number is GB 218804800. -= page_section(title: '20. Our details') do - %p 20.1 This website is owned and operated by #{link_to 'Fish Percolator Limited', 'https://www.fishpercolator.co.uk/'}. - %p 20.2 We are registered in England and Wales under registration number 09617660, and our registered office is our principal place of business. - %p 20.3 Our principal place of business is at International House, 14 King Street, Leeds LS1 2HL, United Kingdom. - %p 20.4 You can contact us: - %ul - %li (a) by post, to the postal address given above; - %li (b) using #{link_to 'our website contact form', 'https://www.fishpercolator.co.uk/contact'}; or - %li (d) by email, using support@name.pn. diff --git a/config/routes.rb b/config/routes.rb index 301cf58..0d3e094 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,14 +18,14 @@ resources :profile resources :clients, only: %i[create destroy] - resources :pages, only: %i[show] namespace :api, defaults: {format: :json} do namespace :v1 do resources :users, only: [:show] end end - + + get "/pages/*path", to: 'pages#page', as: :page get "/:id(/:nominative/:oblique)", to: 'users#show', constraints: { id: %r{[-a-z\d]+}i, nominative: %r{[a-z]+}i, oblique: %r{[a-z]+}i, format: %r{html|png} }, as: :user root to: 'home#index' end From 9449f97db87ee18ef420396bb57cf040ef712a46 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 19 Jun 2025 15:39:39 +0100 Subject: [PATCH 17/44] Make this a little safer in the hope CodeQL thinks it's OK --- app/controllers/pages_controller.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index bdfb534..c445533 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -3,16 +3,17 @@ class PagesController < ApplicationController layout -> { Components::ShellLayout.new } - VALID_PAGES = %w[ + # A frozen hash of valid pages converted to their Component names + PAGES = %w[ about privacy terms guides/phonetic - ] + ].to_h { [it, Views::Pages.const_get(it.camelize)] }.freeze def page - if params[:path].in? VALID_PAGES - render Views::Pages.const_get(params[:path].camelize) + if PAGES.key? params[:path] + render PAGES[params[:path]] else raise AbstractController::ActionNotFound end From 926b879438acc34a9b08ed7133bed1c84d4bc2ef Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Wed, 23 Jul 2025 10:11:23 +0100 Subject: [PATCH 18/44] At least compile --- app/views/home/anon.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/home/anon.rb b/app/views/home/anon.rb index c5b027f..07b40e9 100644 --- a/app/views/home/anon.rb +++ b/app/views/home/anon.rb @@ -2,7 +2,7 @@ class Views::Home::Anon < Views::Base def view_template - ShellLayout(title: "Home") do + ShellLayout do h1 { "This is the homepage" } Button { "Hello" } end From 2c8f937a800ca7b67c7651ed1257dc47d36c64dc Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 24 Jul 2025 11:31:14 +0100 Subject: [PATCH 19/44] Rebuild homepage logo as a component --- Tiltfile | 2 +- app/components/base.rb | 1 + app/components/home/logo.rb | 18 ++++++++++++++++++ app/controllers/home_controller.rb | 4 ++-- app/views/home/anon.rb | 5 +---- config/locales/views.en.yml | 7 +++++++ 6 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 app/components/home/logo.rb diff --git a/Tiltfile b/Tiltfile index 15d27db..1a84ef3 100644 --- a/Tiltfile +++ b/Tiltfile @@ -29,7 +29,7 @@ helm_resource( podman_build('name-pn', '.', ignore=['log', 'tmp'], live_update=[ - fall_back_on(['./config', './Containerfile', './k8s.yaml']), + fall_back_on(['./config/environments', './config/initializers', './config/environment.rb', './config/application.rb', './Containerfile', './k8s.yaml']), sync('.', '/rails'), run('bundle && touch tmp/restart.txt', trigger=['./Gemfile', './Gemfile.lock']), run('yarn', trigger=['./package.json', './yarn.lock']), diff --git a/app/components/base.rb b/app/components/base.rb index 0eaa9b3..c3143db 100644 --- a/app/components/base.rb +++ b/app/components/base.rb @@ -6,6 +6,7 @@ class Components::Base < Phlex::HTML include RubyUI # Include any helpers you want to be available across all components + include Phlex::Rails::Helpers::ImageTag include Phlex::Rails::Helpers::Routes include Phlex::Rails::Helpers::T diff --git a/app/components/home/logo.rb b/app/components/home/logo.rb new file mode 100644 index 0000000..1b4521f --- /dev/null +++ b/app/components/home/logo.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Components::Home::Logo < Components::Base + def view_template + div(class: 'flex flex-col items-center gap-4') do + h1(class: 'text-5xl font-bold leading-none grid grid-cols-[1.15em_1fr_1fr] grid-rows-2') do + div(class: 'pr-[0.15em]') { image_tag 'logo.svg', alt: t('icon_alt') } + div(class: 'text-right') { t('.namedot') } + div { t('.pn') } + div(class: 'text-xl leading-none text-primary mt-2 pl-[2px] col-start-3 flex flex-col') do + span { t('.pronouns') } + span { t('.pronounce') } + end + end + h2(class: 'text-2xl md:text-4xl text-primary') { t('.subtitle') } + end + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 7623607..288e93f 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -3,13 +3,13 @@ class HomeController < ApplicationController skip_after_action :verify_policy_scoped skip_after_action :verify_authorized - layout false + layout -> { user_signed_in? ? 'application' : Components::ShellLayout.new } def index if user_signed_in? # Refuse to show the dashboard to people who haven't completed step 1 return redirect_to(profile_index_path) if !current_user.basic_names_complete? - render 'user_home', layout: 'application' + render 'user_home' else render Views::Home::Anon end diff --git a/app/views/home/anon.rb b/app/views/home/anon.rb index 07b40e9..c1daaf8 100644 --- a/app/views/home/anon.rb +++ b/app/views/home/anon.rb @@ -2,9 +2,6 @@ class Views::Home::Anon < Views::Base def view_template - ShellLayout do - h1 { "This is the homepage" } - Button { "Hello" } - end + div(class: 'flex justify-center') { Home::Logo() } end end diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index 238d2d1..2a2082f 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -3,6 +3,13 @@ en: shell_layout: about: About admin: Admin + home: + logo: + namedot: name. + pn: pn + pronouns: pronouns + pronounce: pronunciation + subtitle: Your name – your rules layouts: navbar: label: main navigation From 01cc4211a9dca42e7857d6079477c6df44edfa62 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 24 Jul 2025 15:11:28 +0100 Subject: [PATCH 20/44] Set the font to Raleway --- app/assets/tailwind/application.css | 2 ++ app/components/layout.rb | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index 5f197d6..aad997f 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -8,6 +8,8 @@ @custom-variant dark (&:is(.dark *)); :root { + --font-sans: "Raleway", "sans-serif"; + --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); --card: oklch(1 0 0); diff --git a/app/components/layout.rb b/app/components/layout.rb index d430177..f38bd3c 100644 --- a/app/components/layout.rb +++ b/app/components/layout.rb @@ -30,11 +30,18 @@ def render_head favicon_link_tag 'apple-touch-icon.png', rel: 'apple-touch-icon', type: 'image/png' link(rel: 'manifest', href: manifest_path(format: :json)) javascript_include_tag 'application', data_turbo_track: 'reload', type: 'module' + google_fonts stylesheet_link_tag 'tailwind', media: 'all', data_turbo_track: 'reload' render partial('layouts/ga') end end + def google_fonts + link(rel: 'preconnect', href: 'https://fonts.googleapis.com') + link(rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: true) + link(href: 'https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100..900;1,100..900&display=swap', rel: 'stylesheet') + end + def default_meta_tags { site: t('product_name'), From 504ec49c8439d09e2035735e3402d8ed843a7b3a Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 24 Jul 2025 16:22:09 +0100 Subject: [PATCH 21/44] Add the promo boxes --- app/views/home/anon.rb | 16 ++++++++++++++++ config/locales/views.en.yml | 15 +++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/app/views/home/anon.rb b/app/views/home/anon.rb index c1daaf8..e06c0b9 100644 --- a/app/views/home/anon.rb +++ b/app/views/home/anon.rb @@ -3,5 +3,21 @@ class Views::Home::Anon < Views::Base def view_template div(class: 'flex justify-center') { Home::Logo() } + div(class: 'grid md:grid-cols-3 gap-6 my-20') do + %w[promo1 promo2 promo3].each do |p| + args = %i[title icon_name description].to_h {[_1, t(_1, scope: ['views.home.anon', p])]} + promo(**args) + end + end + end + + private + + def promo(title:, icon_name:, description:) + div(class: 'rounded-xl border bg-background shadow p-4 md:p-6 grid grid-cols-[48px_auto] gap-3 items-center') do + icon(icon_name, class: 'w-12 h-12') + h3(class: 'col-start-2 text-lg font-bold') { title } + p(class: 'col-start-2') { description } + end end end diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index 2a2082f..5c1a573 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -221,3 +221,18 @@ en: key_modal: title: Your new API key blurb: Be sure to copy this key before closing this box. After closing the box there will be no way to retrieve this key. + views: + home: + anon: + promo1: + title: Share how you'd like people to use your name + description: Not everyone wants to be "title surname". Your name.pn page tells people how you like to be addressed in different situations and which nicknames you like and dislike. + icon_name: sticker-check-outline + promo2: + title: Your name spoken in your voice + description: You can use name.pn to share how your name is pronounced. Let all your contacts hear you saying your name in your own voice. + icon_name: bullhorn + promo3: + title: Display your pronouns to reduce misgendering + description: You can't guess someone's pronouns (they, she, he, etc.) from their name. Sharing your pronouns helps to create a world where fewer people are misgendered. + icon_name: gender-male-female \ No newline at end of file From de046e12d1edbd7d08a12cf345515a8aa8352899 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 24 Jul 2025 16:46:11 +0100 Subject: [PATCH 22/44] Finish the anon_home page and fix some colours --- app/assets/tailwind/application.css | 10 +++++----- app/components/home/logo.rb | 4 ++-- app/views/home/anon.rb | 8 +++++++- config/locales/views.en.yml | 4 +++- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index aad997f..a7fb5f2 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -16,14 +16,14 @@ --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.57 0.2077 22.63); + --primary: oklch(0.4334 0.0811 235.69); --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.32 0.0192 264.26); --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.97 0 0); --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); + --accent: oklch(0.649 0.2154 11.32); + --accent-foreground: oklch(0.985 0 0); --destructive: oklch(0.577 0.245 27.325); --destructive-foreground: oklch(0.577 0.245 27.325); --border: oklch(0.922 0 0); @@ -58,13 +58,13 @@ --card-foreground: oklch(0.985 0 0); --popover: oklch(0.145 0 0); --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.985 0 0); + --primary: oklch(0.7504 0.0969 232.09); --primary-foreground: oklch(0.205 0 0); --secondary: oklch(0.269 0 0); --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.269 0 0); --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); + --accent: oklch(0.649 0.2154 11.32); --accent-foreground: oklch(0.985 0 0); --destructive: oklch(0.396 0.141 25.723); --destructive-foreground: oklch(0.637 0.237 25.331); diff --git a/app/components/home/logo.rb b/app/components/home/logo.rb index 1b4521f..c717b08 100644 --- a/app/components/home/logo.rb +++ b/app/components/home/logo.rb @@ -7,12 +7,12 @@ def view_template div(class: 'pr-[0.15em]') { image_tag 'logo.svg', alt: t('icon_alt') } div(class: 'text-right') { t('.namedot') } div { t('.pn') } - div(class: 'text-xl leading-none text-primary mt-2 pl-[2px] col-start-3 flex flex-col') do + div(class: 'text-xl leading-none text-accent mt-2 pl-[2px] col-start-3 flex flex-col') do span { t('.pronouns') } span { t('.pronounce') } end end - h2(class: 'text-2xl md:text-4xl text-primary') { t('.subtitle') } + h2(class: 'text-2xl md:text-4xl text-accent') { t('.subtitle') } end end end diff --git a/app/views/home/anon.rb b/app/views/home/anon.rb index e06c0b9..54b9b33 100644 --- a/app/views/home/anon.rb +++ b/app/views/home/anon.rb @@ -9,13 +9,19 @@ def view_template promo(**args) end end + div(class: 'flex justify-center') do + Link(href: new_user_registration_path, variant: :primary, class: 'px-6 py-3 h-14 text-2xl') { t('.sign_up') } + end + div(class: 'text-center mt-10') do + Link(href: page_path('about'), size: :lg) { t('.learn_more') } + end end private def promo(title:, icon_name:, description:) div(class: 'rounded-xl border bg-background shadow p-4 md:p-6 grid grid-cols-[48px_auto] gap-3 items-center') do - icon(icon_name, class: 'w-12 h-12') + icon(icon_name, class: 'w-12 h-12 fill-current') h3(class: 'col-start-2 text-lg font-bold') { title } p(class: 'col-start-2') { description } end diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index 5c1a573..d234165 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -235,4 +235,6 @@ en: promo3: title: Display your pronouns to reduce misgendering description: You can't guess someone's pronouns (they, she, he, etc.) from their name. Sharing your pronouns helps to create a world where fewer people are misgendered. - icon_name: gender-male-female \ No newline at end of file + icon_name: gender-male-female + sign_up: Sign up – it's free + learn_more: Learn more about name.pn \ No newline at end of file From 60d0921b531970b8cb97728d95e4ea8a2c3ad1c5 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 24 Jul 2025 16:54:16 +0100 Subject: [PATCH 23/44] Add a subtle gradient to the back of all wpages --- app/components/layout.rb | 8 ++++++-- app/components/shell_layout.rb | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/components/layout.rb b/app/components/layout.rb index f38bd3c..85ce45f 100644 --- a/app/components/layout.rb +++ b/app/components/layout.rb @@ -9,7 +9,7 @@ def view_template doctype html(lang: 'en', class: html_class) do render_head - body(class: 'bg-background text-foreground') do + body(class: body_class) do yield end end @@ -18,7 +18,11 @@ def view_template protected def html_class - nil + 'h-full' + end + + def body_class + 'bg-background text-foreground' end def render_head diff --git a/app/components/shell_layout.rb b/app/components/shell_layout.rb index d853c19..47e0317 100644 --- a/app/components/shell_layout.rb +++ b/app/components/shell_layout.rb @@ -25,7 +25,11 @@ def view_template end end + def body_class + 'bg-linear-to-b bg-fixed from-zinc-100 to-zinc-200 dark:from-black dark:to-zinc-800 text-foreground' + end + def html_class - 'scroll-pt-15' + 'h-full scroll-pt-15' end end From 460c533d0b2c7cc06185ef577b764b348b294905 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 24 Jul 2025 16:57:15 +0100 Subject: [PATCH 24/44] Remove the old anon_home view --- app/views/home/anon_home.html.haml | 44 ------------------------------ config/locales/views.en.yml | 16 ----------- 2 files changed, 60 deletions(-) delete mode 100644 app/views/home/anon_home.html.haml diff --git a/app/views/home/anon_home.html.haml b/app/views/home/anon_home.html.haml deleted file mode 100644 index 62a1eeb..0000000 --- a/app/views/home/anon_home.html.haml +++ /dev/null @@ -1,44 +0,0 @@ -%section.hero - .hero-body - .container.has-text-centered - %h1.home-title - .home-title__image= image_tag 'logo.svg', alt: t('icon_alt') - .home-title__name= t('.title.namedot') - .home-title__pn= t('.title.pn') - .home-title__examples.has-text-primary.is-size-5 - = t('.title.pronouns') - %br - = t('.title.pronounce') - %h2.subtitle.is-3.has-text-primary= t('.subtitle') -%section.section - .container - .columns.is-multiline.is-centered - .column - .box.is-fullheight - %article.media - .media-left - %span.icon.is-large= icon 'sticker-check-outline', class: 'mdi mdi-48px' - .media-content - %h3.title.is-5= t('.get_your_name_right') - %p.mt-5= t('.name_right_blurb') - .column - .box.is-fullheight - %article.media - .media-left - %span.icon.is-large= icon 'bullhorn', class: 'mdi mdi-48px' - .media-content - %h3.title.is-5= t('.pronunciation') - %p.mt-5= t('.pronunciation_blurb') - .column - .box.is-fullheight - %article.media - .media-left - %span.icon.is-large= icon 'gender-male-female', class: 'mdi mdi-48px' - .media-content - %h3.title.is-5= t('.pronouns') - %p.mt-5= t('.pronouns_blurb') -%section.section - .container.has-text-centered - %p= link_to t('.sign_up_free'), new_user_registration_path, class: 'button is-link is-large' - %p.has-text-grey.is-6.mt-5= t('.warning') - %p.is-6.mt-5= link_to t('.learn_more'), page_path('about') diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index d234165..4d0d213 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -33,22 +33,6 @@ en: support: report_a_bug: Report a bug on our issues board home: - anon_home: - title: - namedot: name. - pn: pn - pronouns: pronouns - pronounce: pronunciation - subtitle: Your name – your rules - sign_up_free: Sign up – it's free - get_your_name_right: Share how you'd like people to use your name - name_right_blurb: Not everyone wants to be "title surname". Your name.pn page tells people how you like to be addressed in different situations and which nicknames you like and dislike. - pronunciation: Your name spoken in your voice - pronunciation_blurb: You can use name.pn to share how your name is pronounced. Let all your contacts hear you saying your name in your own voice. - pronouns: Display your pronouns to reduce misgendering - pronouns_blurb: You can't guess someone's pronouns (they, she, he, etc.) from their name. Sharing your pronouns helps to create a world where fewer people are misgendered. - warning: 'Note: name.pn is pre-alpha software and is changing all the time. Feel free to use it but expect changes and bugs!' - learn_more: Learn more about name.pn user_home: title: Your name dashboard page_description1: Here you can create a page that describes the different ways you would like to be addressed in English. From ff5b573e512239217aedfae0ae5236bc58f8705d Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 24 Jul 2025 16:58:40 +0100 Subject: [PATCH 25/44] Add some more space at the top of the homepage --- app/views/home/anon.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/home/anon.rb b/app/views/home/anon.rb index 54b9b33..4208293 100644 --- a/app/views/home/anon.rb +++ b/app/views/home/anon.rb @@ -2,7 +2,7 @@ class Views::Home::Anon < Views::Base def view_template - div(class: 'flex justify-center') { Home::Logo() } + div(class: 'flex justify-center mt-6') { Home::Logo() } div(class: 'grid md:grid-cols-3 gap-6 my-20') do %w[promo1 promo2 promo3].each do |p| args = %i[title icon_name description].to_h {[_1, t(_1, scope: ['views.home.anon', p])]} From d5b143a00d77db471f501d298c237bca37697469 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Fri, 25 Jul 2025 16:15:39 +0100 Subject: [PATCH 26/44] Add the skeletons of some of the Devise forms - still a long way to go on these --- Gemfile | 3 + Gemfile.lock | 10 +++ app/components/base.rb | 2 + app/components/box.rb | 11 +++ app/components/centered_form.rb | 17 +++++ app/components/ruby_ui/checkbox/checkbox.rb | 23 ++++++ .../ruby_ui/checkbox/checkbox_group.rb | 20 ++++++ app/components/ruby_ui/form/form.rb | 15 ++++ app/components/ruby_ui/form/form_field.rb | 20 ++++++ .../ruby_ui/form/form_field_error.rb | 20 ++++++ .../ruby_ui/form/form_field_hint.rb | 15 ++++ .../ruby_ui/form/form_field_label.rb | 15 ++++ app/components/ruby_ui/input/input.rb | 26 +++++++ app/components/shell_layout.rb | 12 ++-- app/controllers/application_controller.rb | 4 ++ app/controllers/home_controller.rb | 2 - .../ruby_ui/checkbox_group_controller.js | 21 ++++++ .../ruby_ui/form_field_controller.js | 61 ++++++++++++++++ app/views/devise/registrations/new.html.erb | 1 + app/views/devise/registrations/new.html.haml | 20 ------ app/views/devise/registrations/new.rb | 14 ++++ app/views/devise/sessions/new.html.erb | 1 + app/views/devise/sessions/new.html.haml | 13 ---- app/views/devise/sessions/new.rb | 14 ++++ app/views/forms/application_form.rb | 70 +++++++++++++++++++ app/views/forms/registration_form.rb | 10 +++ app/views/forms/session_form.rb | 10 +++ app/views/home/anon.rb | 2 +- config/application.rb | 1 + 29 files changed, 409 insertions(+), 44 deletions(-) create mode 100644 app/components/box.rb create mode 100644 app/components/centered_form.rb create mode 100644 app/components/ruby_ui/checkbox/checkbox.rb create mode 100644 app/components/ruby_ui/checkbox/checkbox_group.rb create mode 100644 app/components/ruby_ui/form/form.rb create mode 100644 app/components/ruby_ui/form/form_field.rb create mode 100644 app/components/ruby_ui/form/form_field_error.rb create mode 100644 app/components/ruby_ui/form/form_field_hint.rb create mode 100644 app/components/ruby_ui/form/form_field_label.rb create mode 100644 app/components/ruby_ui/input/input.rb create mode 100644 app/javascript/controllers/ruby_ui/checkbox_group_controller.js create mode 100644 app/javascript/controllers/ruby_ui/form_field_controller.js create mode 100644 app/views/devise/registrations/new.html.erb delete mode 100644 app/views/devise/registrations/new.html.haml create mode 100644 app/views/devise/registrations/new.rb create mode 100644 app/views/devise/sessions/new.html.erb delete mode 100644 app/views/devise/sessions/new.html.haml create mode 100644 app/views/devise/sessions/new.rb create mode 100644 app/views/forms/application_form.rb create mode 100644 app/views/forms/registration_form.rb create mode 100644 app/views/forms/session_form.rb diff --git a/Gemfile b/Gemfile index 7d45899..c28aea6 100644 --- a/Gemfile +++ b/Gemfile @@ -55,6 +55,7 @@ gem 'meta-tags', '~> 2.13' gem 'mini_magick', '~> 5.2' gem 'ostruct', '~> 0.6.1' # dependency of gaffe gem 'phlex-rails', '~> 2.2' +gem 'phlexible', '~> 3.0' gem 'pundit', '~> 2.1' gem 'rails_semantic_logger', '~> 4.6' gem 'rswag-api', '~> 2.4' @@ -113,3 +114,5 @@ group :production do gem 'aws-sdk-s3', '~> 1.183' gem 'rails_12factor', '~> 0.0.3' end + +gem "superform", "~> 0.5.1" diff --git a/Gemfile.lock b/Gemfile.lock index c175be1..1eae1b6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -414,6 +414,11 @@ GEM phlex (~> 2.3.0) railties (>= 7.1, < 9) zeitwerk (~> 2.7) + phlexible (3.0.0) + phlex (>= 1.10.0, < 3.0.0) + phlex-rails (>= 1.2.0, < 3.0.0) + rails (>= 7.2.0, < 9.0.0) + zeitwerk (~> 2.7.2) pp (0.6.2) prettyprint prettyprint (0.2.0) @@ -572,6 +577,9 @@ GEM stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.7) + superform (0.5.1) + phlex-rails (>= 1.0, < 3.0) + zeitwerk (~> 2.6) tailwind_merge (1.3.1) sin_lru_redux (~> 2.5) tailwindcss-rails (4.2.3) @@ -685,6 +693,7 @@ DEPENDENCIES ostruct (~> 0.6.1) pg (>= 0.18, < 2.0) phlex-rails (~> 2.2) + phlexible (~> 3.0) pry-rails pry-rescue puma (~> 6.0) @@ -706,6 +715,7 @@ DEPENDENCIES spinach-rails sprockets-rails (~> 3.5) stimulus-rails (~> 1.3) + superform (~> 0.5.1) tailwind_merge (~> 1.2) tailwindcss-rails (~> 4.2) tailwindcss-ruby (~> 4.1) diff --git a/app/components/base.rb b/app/components/base.rb index c3143db..037909e 100644 --- a/app/components/base.rb +++ b/app/components/base.rb @@ -25,3 +25,5 @@ def container_classes end end + +ApplicationComponent = Components::Base diff --git a/app/components/box.rb b/app/components/box.rb new file mode 100644 index 0000000..a4792b7 --- /dev/null +++ b/app/components/box.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Components::Box < Components::Base + def initialize(**attributes) + @attributes = attributes + end + + def view_template(&) + div(**mix({class: 'rounded-xl border bg-background shadow p-4 md:p-6'}, @attributes), &) + end +end diff --git a/app/components/centered_form.rb b/app/components/centered_form.rb new file mode 100644 index 0000000..6853b7c --- /dev/null +++ b/app/components/centered_form.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Components::CenteredForm < Components::Base + def view_template(&) + div(class: 'grow flex items-center justify-center') do + div(&) + end + end + + def title(&) + h1(class: 'text-3xl font-bold mb-6', &) + end + + def main(&) + Box(&) + end +end diff --git a/app/components/ruby_ui/checkbox/checkbox.rb b/app/components/ruby_ui/checkbox/checkbox.rb new file mode 100644 index 0000000..391289d --- /dev/null +++ b/app/components/ruby_ui/checkbox/checkbox.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module RubyUI + class Checkbox < Base + def view_template + input(**attrs) + end + + private + + def default_attrs + { + type: "checkbox", + data: { + ruby_ui__form_field_target: "input", + ruby_ui__checkbox_group_target: "checkbox", + action: "change->ruby-ui--checkbox-group#onChange change->ruby-ui--form-field#onInput invalid->ruby-ui--form-field#onInvalid" + }, + class: "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 accent-primary" + } + end + end +end diff --git a/app/components/ruby_ui/checkbox/checkbox_group.rb b/app/components/ruby_ui/checkbox/checkbox_group.rb new file mode 100644 index 0000000..4f93613 --- /dev/null +++ b/app/components/ruby_ui/checkbox/checkbox_group.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module RubyUI + class CheckboxGroup < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + role: "group", + data: { + controller: "ruby-ui--checkbox-group" + } + } + end + end +end diff --git a/app/components/ruby_ui/form/form.rb b/app/components/ruby_ui/form/form.rb new file mode 100644 index 0000000..c34e774 --- /dev/null +++ b/app/components/ruby_ui/form/form.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module RubyUI + class Form < Base + def view_template(&) + form(**attrs, &) + end + + private + + def default_attrs + {} + end + end +end diff --git a/app/components/ruby_ui/form/form_field.rb b/app/components/ruby_ui/form/form_field.rb new file mode 100644 index 0000000..dc4b511 --- /dev/null +++ b/app/components/ruby_ui/form/form_field.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module RubyUI + class FormField < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + data: { + controller: "ruby-ui--form-field" + }, + class: "space-y-2" + } + end + end +end diff --git a/app/components/ruby_ui/form/form_field_error.rb b/app/components/ruby_ui/form/form_field_error.rb new file mode 100644 index 0000000..0fceacd --- /dev/null +++ b/app/components/ruby_ui/form/form_field_error.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module RubyUI + class FormFieldError < Base + def view_template(&) + p(**attrs, &) + end + + private + + def default_attrs + { + data: { + ruby_ui__form_field_target: "error" + }, + class: "text-sm font-medium text-destructive" + } + end + end +end diff --git a/app/components/ruby_ui/form/form_field_hint.rb b/app/components/ruby_ui/form/form_field_hint.rb new file mode 100644 index 0000000..5304a25 --- /dev/null +++ b/app/components/ruby_ui/form/form_field_hint.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module RubyUI + class FormFieldHint < Base + def view_template(&) + p(**attrs, &) + end + + private + + def default_attrs + {class: "text-sm text-muted-foreground"} + end + end +end diff --git a/app/components/ruby_ui/form/form_field_label.rb b/app/components/ruby_ui/form/form_field_label.rb new file mode 100644 index 0000000..032ada1 --- /dev/null +++ b/app/components/ruby_ui/form/form_field_label.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module RubyUI + class FormFieldLabel < Base + def view_template(&) + label(**attrs, &) + end + + private + + def default_attrs + {class: "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"} + end + end +end diff --git a/app/components/ruby_ui/input/input.rb b/app/components/ruby_ui/input/input.rb new file mode 100644 index 0000000..05087c2 --- /dev/null +++ b/app/components/ruby_ui/input/input.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module RubyUI + class Input < Base + def initialize(type: :string, **attrs) + @type = type.to_sym + super(**attrs) + end + + def view_template + input(type: @type, **attrs) + end + + private + + def default_attrs + { + data: { + ruby_ui__form_field_target: "input", + action: "input->ruby-ui--form-field#onInput invalid->ruby-ui--form-field#onInvalid" + }, + class: "flex h-9 w-full rounded-md border bg-background px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50 border-border focus-visible:ring-ring placeholder:text-muted-foreground" + } + end + end +end diff --git a/app/components/shell_layout.rb b/app/components/shell_layout.rb index 47e0317..d81ba06 100644 --- a/app/components/shell_layout.rb +++ b/app/components/shell_layout.rb @@ -4,7 +4,7 @@ class Components::ShellLayout < Components::Layout register_value_helper :user_signed_in? register_value_helper :current_user - def view_template + def view_template(&) super do PageHeader do |header| header.main_link(page_path('about')) { t('.about') } @@ -17,19 +17,15 @@ def view_template header.action_link(new_user_registration_path) { t('devise.registrations.new.title') } end end - main do - div(class: 'px-2 sm:px-4 sm:container py-4 sm:py-8') do - yield - end - end + main(class: 'flex flex-col grow px-2 sm:px-4 sm:container py-4 sm:py-8', &) end end def body_class - 'bg-linear-to-b bg-fixed from-zinc-100 to-zinc-200 dark:from-black dark:to-zinc-800 text-foreground' + 'min-h-screen flex flex-col bg-linear-to-b bg-fixed from-zinc-100 to-zinc-200 dark:from-black dark:to-zinc-800 text-foreground' end def html_class - 'h-full scroll-pt-15' + 'min-h-screen scroll-pt-15' end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0902096..7ebab98 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,10 +1,14 @@ class ApplicationController < ActionController::Base include Pundit::Authorization + include Phlexible::Rails::ActionController::ImplicitRender + after_action :verify_authorized, except: :index, unless: :framework_controller? after_action :verify_policy_scoped, only: :index, unless: :framework_controller? rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized + layout -> { Components::ShellLayout } + def authenticate_admin! if !current_user&.role_admin? sign_out diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 288e93f..f32da86 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -2,8 +2,6 @@ class HomeController < ApplicationController protect_from_forgery except: :service_worker skip_after_action :verify_policy_scoped skip_after_action :verify_authorized - - layout -> { user_signed_in? ? 'application' : Components::ShellLayout.new } def index if user_signed_in? diff --git a/app/javascript/controllers/ruby_ui/checkbox_group_controller.js b/app/javascript/controllers/ruby_ui/checkbox_group_controller.js new file mode 100644 index 0000000..546167a --- /dev/null +++ b/app/javascript/controllers/ruby_ui/checkbox_group_controller.js @@ -0,0 +1,21 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = ["checkbox"]; + + connect() { + this.#handleRequired(); + } + + onChange() { + this.#handleRequired(); + } + + #handleRequired() { + if (!this.element.hasAttribute("data-required")) return; + + const checked = this.checkboxTargets.some(({ checked }) => checked); + + this.checkboxTargets.forEach((checkbox) => (checkbox.required = !checked)); + } +} diff --git a/app/javascript/controllers/ruby_ui/form_field_controller.js b/app/javascript/controllers/ruby_ui/form_field_controller.js new file mode 100644 index 0000000..b7af939 --- /dev/null +++ b/app/javascript/controllers/ruby_ui/form_field_controller.js @@ -0,0 +1,61 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = ["input", "error"]; + static values = { shouldValidate: false }; + + connect() { + if (this.hasErrorTarget) { + if (this.errorTarget.textContent) { + this.shouldValidateValue = true; + } else { + this.errorTarget.classList.add("hidden"); + } + } + } + + onInvalid(error) { + error.preventDefault(); + + this.shouldValidateValue = true; + this.#setErrorMessage(); + } + + onInput() { + this.#setErrorMessage(); + } + + onChange() { + this.#setErrorMessage(); + } + + #setErrorMessage() { + if (!this.shouldValidateValue) return; + + if (this.inputTarget.validity.valid) { + this.errorTarget.textContent = ""; + this.errorTarget.classList.add("hidden"); + } else { + this.errorTarget.textContent = this.#getValidationMessage(); + this.errorTarget.classList.remove("hidden"); + } + } + + #getValidationMessage() { + let errorMessage; + + const { validity, dataset, validationMessage } = this.inputTarget; + + if (validity.tooLong) errorMessage = dataset.tooLong; + if (validity.tooShort) errorMessage = dataset.tooShort; + if (validity.badInput) errorMessage = dataset.badInput; + if (validity.typeMismatch) errorMessage = dataset.typeMismatch; + if (validity.stepMismatch) errorMessage = dataset.stepMismatch; + if (validity.valueMissing) errorMessage = dataset.valueMissing; + if (validity.rangeOverflow) errorMessage = dataset.rangeOverflow; + if (validity.rangeUnderflow) errorMessage = dataset.rangeUnderflow; + if (validity.patternMismatch) errorMessage = dataset.patternMismatch; + + return errorMessage || validationMessage; + } +} diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb new file mode 100644 index 0000000..c78f957 --- /dev/null +++ b/app/views/devise/registrations/new.html.erb @@ -0,0 +1 @@ +<%= render Views::Devise::Registrations::New.new(resource:) %> \ No newline at end of file diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml deleted file mode 100644 index 9329aff..0000000 --- a/app/views/devise/registrations/new.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -- content_for(:main_class) { 'is-justify-content-center' } -%section.section - .container - .columns - .column.is-three-fifths.is-offset-one-fifth - %h1.title= title t('.title') - = simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| - .box - = f.input :email, autofocus: true, label: t('.email'), hint: t('.email_hint') - = f.input :password, label: t('.password') - = f.input :password_confirmation - .box.mt-5 - = f.input :subscribe_to_mailing_list, as: :boolean, input_html: {checked: true} - .box.mt-5 - %p= t('.privacy_notice_html', url: page_path('privacy')) - .mt-5 - = f.input :terms, as: :boolean, label: t('.terms_label_html', url: page_path('terms')) - = f.button :submit, t('.submit'), class: 'is-link' - .mt-5 - = render "devise/shared/links" diff --git a/app/views/devise/registrations/new.rb b/app/views/devise/registrations/new.rb new file mode 100644 index 0000000..ae7d4c7 --- /dev/null +++ b/app/views/devise/registrations/new.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class Views::Devise::Registrations::New < Views::Base + def initialize(resource:) + @resource = resource + end + + def view_template + CenteredForm do |cf| + cf.title { title t('devise.registrations.new.title') } + cf.main { render RegistrationForm.new(@resource) } + end + end +end \ No newline at end of file diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb new file mode 100644 index 0000000..7efd87e --- /dev/null +++ b/app/views/devise/sessions/new.html.erb @@ -0,0 +1 @@ +<%= render Views::Devise::Sessions::New.new(resource:) %> \ No newline at end of file diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml deleted file mode 100644 index f969144..0000000 --- a/app/views/devise/sessions/new.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- content_for(:main_class) { 'is-justify-content-center' } -%section.section - .container - .columns - .column.is-three-fifths.is-offset-one-fifth - %h1.title= title t('.title') - .box - = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| - = f.input :email - = f.input :password - = f.input :remember_me, as: :boolean - = f.button :submit, t('.submit'), class: 'is-link' - = render "devise/shared/links" diff --git a/app/views/devise/sessions/new.rb b/app/views/devise/sessions/new.rb new file mode 100644 index 0000000..07baead --- /dev/null +++ b/app/views/devise/sessions/new.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class Views::Devise::Sessions::New < Views::Base + def initialize(resource:) + @resource = resource + end + + def view_template + CenteredForm do |cf| + cf.title { title t('devise.sessions.new.title') } + cf.main { render SessionForm.new(@resource) } + end + end +end diff --git a/app/views/forms/application_form.rb b/app/views/forms/application_form.rb new file mode 100644 index 0000000..804088e --- /dev/null +++ b/app/views/forms/application_form.rb @@ -0,0 +1,70 @@ +class ApplicationForm < Superform::Rails::Form + class CheckboxComponent < Superform::Rails::Components::CheckboxComponent + def view_template(&) + input(name: dom.name, type: :hidden, value: "0") + div(class: 'flex items-center space-x-3') do + Checkbox(value: "1", **attributes) + label(for: dom.id, class: 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70') { field.label_string } + end + end + end + + class InputComponent < Superform::Rails::Components::InputComponent + def view_template(&) + Input(**attributes, &) + FormFieldError { field.error_message } + end + end + + class LabelComponent < Superform::Rails::Components::LabelComponent + def view_template(&content) + content ||= Proc.new { field.label_string } + FormFieldLabel(**attributes, &content) + end + end + + class Field < Field + def errors + @object.errors[@key] + end + + def error_message + errors.to_sentence if errors.present? + end + + def label_string + @object.class.human_attribute_name(@key) + end + + def checkbox(**attributes) + CheckboxComponent.new(self, attributes:) + end + + def input(**attributes) + InputComponent.new(self, attributes:) + end + + def label(**attributes, &) + LabelComponent.new(self, attributes:, &) + end + end + + def form_class + 'space-y-4' + end + + def form_tag(&) + Form(action: form_action, method: form_method, class: form_class, **@attributes, &) + end + + def row(component) + FormField do + render component.field.label + render component + end + end + + def submit(value = submit_value, **attributes) + Button(**attributes.merge(type: :submit)) { value } + end +end diff --git a/app/views/forms/registration_form.rb b/app/views/forms/registration_form.rb new file mode 100644 index 0000000..64c8f15 --- /dev/null +++ b/app/views/forms/registration_form.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class RegistrationForm < ApplicationForm + def view_template(&) + row field(:email).input + row field(:password).input(type: :password) + row field(:password_confirmation).input(type: :password) + submit t('devise.registrations.new.submit') + end +end diff --git a/app/views/forms/session_form.rb b/app/views/forms/session_form.rb new file mode 100644 index 0000000..5ca92b9 --- /dev/null +++ b/app/views/forms/session_form.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class SessionForm < ApplicationForm + def view_template(&) + row field(:email).input + row field(:password).input(type: :password) + render field(:remember_me).checkbox + submit t('devise.sessions.new.submit') + end +end diff --git a/app/views/home/anon.rb b/app/views/home/anon.rb index 4208293..295ef79 100644 --- a/app/views/home/anon.rb +++ b/app/views/home/anon.rb @@ -20,7 +20,7 @@ def view_template private def promo(title:, icon_name:, description:) - div(class: 'rounded-xl border bg-background shadow p-4 md:p-6 grid grid-cols-[48px_auto] gap-3 items-center') do + Box(class: 'grid grid-cols-[48px_auto] gap-3 items-center') do icon(icon_name, class: 'w-12 h-12 fill-current') h3(class: 'col-start-2 text-lg font-bold') { title } p(class: 'col-start-2') { description } diff --git a/config/application.rb b/config/application.rb index d6c0ae5..9ad8bdd 100644 --- a/config/application.rb +++ b/config/application.rb @@ -44,6 +44,7 @@ class Application < Rails::Application # not contain `.rb` files, or that should not be reloaded or eager loaded. # Common ones are `templates`, `generators`, or `middleware`, for example. config.autoload_lib(ignore: %w[assets tasks]) + config.autoload_paths << "#{root}/app/views/forms" # Configuration for the application, engines, and railties goes here. # From cba4a13bf9284673b229804cb6363dc9c0160122 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Mon, 28 Jul 2025 11:17:51 +0100 Subject: [PATCH 27/44] Change the way layouts work --- app/components/centered_form.rb | 14 ++++++++----- app/components/shell_layout.rb | 6 +++++- app/controllers/application_controller.rb | 2 +- app/controllers/pages_controller.rb | 2 +- app/views/devise/registrations/new.rb | 4 ++-- app/views/devise/sessions/new.rb | 2 +- app/views/home/anon.rb | 24 ++++++++++++----------- 7 files changed, 32 insertions(+), 22 deletions(-) diff --git a/app/components/centered_form.rb b/app/components/centered_form.rb index 6853b7c..12fd8a7 100644 --- a/app/components/centered_form.rb +++ b/app/components/centered_form.rb @@ -2,16 +2,20 @@ class Components::CenteredForm < Components::Base def view_template(&) - div(class: 'grow flex items-center justify-center') do - div(&) + ShellLayout(main_class: 'grow flex items-center justify-center') do + div(class: 'w-screen max-w-lg', &) end end def title(&) - h1(class: 'text-3xl font-bold mb-6', &) + h1(class: 'text-3xl font-bold mx-3 sm:mx-0', &) end - def main(&) - Box(&) + def section(&) + Box(class: 'mx-3 sm:mx-0 my-6', &) + end + + def footer(&) + div(class: 'mx-3 sm:mx-0 my-6', &) end end diff --git a/app/components/shell_layout.rb b/app/components/shell_layout.rb index d81ba06..a4566cd 100644 --- a/app/components/shell_layout.rb +++ b/app/components/shell_layout.rb @@ -4,6 +4,10 @@ class Components::ShellLayout < Components::Layout register_value_helper :user_signed_in? register_value_helper :current_user + def initialize(main_class: 'flex flex-col grow px-2 sm:px-4 sm:container py-4 sm:py-8') + @main_class = main_class + end + def view_template(&) super do PageHeader do |header| @@ -17,7 +21,7 @@ def view_template(&) header.action_link(new_user_registration_path) { t('devise.registrations.new.title') } end end - main(class: 'flex flex-col grow px-2 sm:px-4 sm:container py-4 sm:py-8', &) + main(class: @main_class, &) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7ebab98..ad550d6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -7,7 +7,7 @@ class ApplicationController < ActionController::Base rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized - layout -> { Components::ShellLayout } + layout false def authenticate_admin! if !current_user&.role_admin? diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index c445533..2e5867c 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -1,7 +1,7 @@ class PagesController < ApplicationController skip_after_action :verify_authorized - layout -> { Components::ShellLayout.new } + layout -> { Components::ShellLayout } # A frozen hash of valid pages converted to their Component names PAGES = %w[ diff --git a/app/views/devise/registrations/new.rb b/app/views/devise/registrations/new.rb index ae7d4c7..98ff4c9 100644 --- a/app/views/devise/registrations/new.rb +++ b/app/views/devise/registrations/new.rb @@ -8,7 +8,7 @@ def initialize(resource:) def view_template CenteredForm do |cf| cf.title { title t('devise.registrations.new.title') } - cf.main { render RegistrationForm.new(@resource) } + cf.section { render RegistrationForm.new(@resource) } end end -end \ No newline at end of file +end diff --git a/app/views/devise/sessions/new.rb b/app/views/devise/sessions/new.rb index 07baead..bf3d80a 100644 --- a/app/views/devise/sessions/new.rb +++ b/app/views/devise/sessions/new.rb @@ -8,7 +8,7 @@ def initialize(resource:) def view_template CenteredForm do |cf| cf.title { title t('devise.sessions.new.title') } - cf.main { render SessionForm.new(@resource) } + cf.section { render SessionForm.new(@resource) } end end end diff --git a/app/views/home/anon.rb b/app/views/home/anon.rb index 295ef79..4b8de72 100644 --- a/app/views/home/anon.rb +++ b/app/views/home/anon.rb @@ -2,18 +2,20 @@ class Views::Home::Anon < Views::Base def view_template - div(class: 'flex justify-center mt-6') { Home::Logo() } - div(class: 'grid md:grid-cols-3 gap-6 my-20') do - %w[promo1 promo2 promo3].each do |p| - args = %i[title icon_name description].to_h {[_1, t(_1, scope: ['views.home.anon', p])]} - promo(**args) + ShellLayout do + div(class: 'flex justify-center mt-6') { Home::Logo() } + div(class: 'grid md:grid-cols-3 gap-6 my-20') do + %w[promo1 promo2 promo3].each do |p| + args = %i[title icon_name description].to_h {[_1, t(_1, scope: ['views.home.anon', p])]} + promo(**args) + end + end + div(class: 'flex justify-center') do + Link(href: new_user_registration_path, variant: :primary, class: 'px-6 py-3 h-14 text-2xl') { t('.sign_up') } + end + div(class: 'text-center mt-10') do + Link(href: page_path('about'), size: :lg) { t('.learn_more') } end - end - div(class: 'flex justify-center') do - Link(href: new_user_registration_path, variant: :primary, class: 'px-6 py-3 h-14 text-2xl') { t('.sign_up') } - end - div(class: 'text-center mt-10') do - Link(href: page_path('about'), size: :lg) { t('.learn_more') } end end From 32b8b9613c4775a1d10f06d9ba2aa5848efd283c Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Mon, 28 Jul 2025 11:25:58 +0100 Subject: [PATCH 28/44] Pass the CF itself to the form component --- app/components/centered_form.rb | 4 ++-- app/views/devise/registrations/new.rb | 2 +- app/views/devise/sessions/new.rb | 2 +- app/views/forms/registration_form.rb | 19 +++++++++++++++---- app/views/forms/session_form.rb | 19 +++++++++++++++---- 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/app/components/centered_form.rb b/app/components/centered_form.rb index 12fd8a7..821d485 100644 --- a/app/components/centered_form.rb +++ b/app/components/centered_form.rb @@ -12,10 +12,10 @@ def title(&) end def section(&) - Box(class: 'mx-3 sm:mx-0 my-6', &) + Box(class: 'mx-3 sm:mx-0 my-6 space-y-4', &) end def footer(&) - div(class: 'mx-3 sm:mx-0 my-6', &) + div(class: 'mx-3 sm:mx-0 my-6 space-y-4', &) end end diff --git a/app/views/devise/registrations/new.rb b/app/views/devise/registrations/new.rb index 98ff4c9..2581bf9 100644 --- a/app/views/devise/registrations/new.rb +++ b/app/views/devise/registrations/new.rb @@ -8,7 +8,7 @@ def initialize(resource:) def view_template CenteredForm do |cf| cf.title { title t('devise.registrations.new.title') } - cf.section { render RegistrationForm.new(@resource) } + render RegistrationForm.new(@resource, cf:) end end end diff --git a/app/views/devise/sessions/new.rb b/app/views/devise/sessions/new.rb index bf3d80a..e683f58 100644 --- a/app/views/devise/sessions/new.rb +++ b/app/views/devise/sessions/new.rb @@ -8,7 +8,7 @@ def initialize(resource:) def view_template CenteredForm do |cf| cf.title { title t('devise.sessions.new.title') } - cf.section { render SessionForm.new(@resource) } + render SessionForm.new(@resource, cf:) end end end diff --git a/app/views/forms/registration_form.rb b/app/views/forms/registration_form.rb index 64c8f15..18a41d7 100644 --- a/app/views/forms/registration_form.rb +++ b/app/views/forms/registration_form.rb @@ -1,10 +1,21 @@ # frozen_string_literal: true class RegistrationForm < ApplicationForm + attr_reader :cf + + def initialize(*, **) + super + @cf = @attributes.delete(:cf) # CenteredForm + end + def view_template(&) - row field(:email).input - row field(:password).input(type: :password) - row field(:password_confirmation).input(type: :password) - submit t('devise.registrations.new.submit') + cf.section do + row field(:email).input + row field(:password).input(type: :password) + row field(:password_confirmation).input(type: :password) + end + cf.footer do + submit t('devise.registrations.new.submit') + end end end diff --git a/app/views/forms/session_form.rb b/app/views/forms/session_form.rb index 5ca92b9..67b5a8a 100644 --- a/app/views/forms/session_form.rb +++ b/app/views/forms/session_form.rb @@ -1,10 +1,21 @@ # frozen_string_literal: true class SessionForm < ApplicationForm + attr_reader :cf + + def initialize(*, **) + super + @cf = @attributes.delete(:cf) # CenteredForm + end + def view_template(&) - row field(:email).input - row field(:password).input(type: :password) - render field(:remember_me).checkbox - submit t('devise.sessions.new.submit') + cf.section do + row field(:email).input + row field(:password).input(type: :password) + render field(:remember_me).checkbox + end + cf.footer do + submit t('devise.sessions.new.submit') + end end end From 4d998474c48ce61e776635d65e0e6b0605d02fae Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Mon, 28 Jul 2025 16:06:21 +0100 Subject: [PATCH 29/44] Remove Superform altogether. Will need a refactor --- Gemfile | 2 - Gemfile.lock | 4 -- app/builders/rui_form_builder.rb | 68 ++++++++++++++++++++ app/controllers/home_controller.rb | 2 +- app/controllers/profile_controller.rb | 2 + app/views/devise/registrations/new.html.erb | 2 +- app/views/devise/registrations/new.rb | 31 ++++++++- app/views/devise/sessions/new.html.erb | 2 +- app/views/devise/sessions/new.rb | 24 ++++++- app/views/forms/application_form.rb | 70 --------------------- app/views/forms/registration_form.rb | 21 ------- app/views/forms/session_form.rb | 21 ------- config/application.rb | 2 +- 13 files changed, 125 insertions(+), 126 deletions(-) create mode 100644 app/builders/rui_form_builder.rb delete mode 100644 app/views/forms/application_form.rb delete mode 100644 app/views/forms/registration_form.rb delete mode 100644 app/views/forms/session_form.rb diff --git a/Gemfile b/Gemfile index c28aea6..4afc0be 100644 --- a/Gemfile +++ b/Gemfile @@ -114,5 +114,3 @@ group :production do gem 'aws-sdk-s3', '~> 1.183' gem 'rails_12factor', '~> 0.0.3' end - -gem "superform", "~> 0.5.1" diff --git a/Gemfile.lock b/Gemfile.lock index 1eae1b6..b3547ae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -577,9 +577,6 @@ GEM stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.7) - superform (0.5.1) - phlex-rails (>= 1.0, < 3.0) - zeitwerk (~> 2.6) tailwind_merge (1.3.1) sin_lru_redux (~> 2.5) tailwindcss-rails (4.2.3) @@ -715,7 +712,6 @@ DEPENDENCIES spinach-rails sprockets-rails (~> 3.5) stimulus-rails (~> 1.3) - superform (~> 0.5.1) tailwind_merge (~> 1.2) tailwindcss-rails (~> 4.2) tailwindcss-ruby (~> 4.1) diff --git a/app/builders/rui_form_builder.rb b/app/builders/rui_form_builder.rb new file mode 100644 index 0000000..fb5fc83 --- /dev/null +++ b/app/builders/rui_form_builder.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +class RuiFormBuilder < ActionView::Helpers::FormBuilder + def label(method, text = nil, options = {}) + content = text || object.class.human_attribute_name(method) + @template.render RubyUI::FormFieldLabel.new( + for: field_id(method), + **options + ) { block_given? ? yield : content } + end + + def email_field(method, options = {}) + input_field(method, type: :email, **options) + end + + def text_field(method, options = {}) + input_field(method, type: :text, **options) + end + + def password_field(method, options = {}) + input_field(method, type: :password, **options) + end + + def submit(value = nil, options = {}) + value, options = nil, value if value.is_a?(Hash) + value ||= submit_default_value + @template.render RubyUI::Button.new( + type: :submit, + **options, + ) { value } + end + + def labelled_checkbox(method, text = nil, options = {}) + content = text || object.class.human_attribute_name(method) + options[:outer_class] ||= 'flex items-center space-x-3' + options[:label_class] ||= 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' + + @template.hidden_field_tag(field_name(method), '0', id: nil) + + @template.content_tag(:div, class: options.delete(:outer_class)) do + @template.render(RubyUI::Checkbox.new( + id: field_id(method), + name: field_name(method), + value: '1', + checked: ActiveModel::Type::Boolean.new.cast(object.public_send(method)), + **options + )) + + @template.content_tag(:label, class: options.delete(:label_class), for: field_id(method)) { content } + end + end + + def errors(method) + if object.errors.key?(method) + @template.render(RubyUI::FormFieldError.new { object.errors[method].to_sentence }) + end + end + + private + + def input_field(method, type:, **options) + @template.render(RubyUI::Input.new( + type:, + id: field_id(method), + name: field_name(method), + value: object.public_send(method), + **options + )) + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index f32da86..79696eb 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -7,7 +7,7 @@ def index if user_signed_in? # Refuse to show the dashboard to people who haven't completed step 1 return redirect_to(profile_index_path) if !current_user.basic_names_complete? - render 'user_home' + render 'user_home', layout: 'application' else render Views::Home::Anon end diff --git a/app/controllers/profile_controller.rb b/app/controllers/profile_controller.rb index a18062f..8e2542c 100644 --- a/app/controllers/profile_controller.rb +++ b/app/controllers/profile_controller.rb @@ -1,5 +1,7 @@ class ProfileController < ApplicationController include Wicked::Wizard + + layout 'application' before_action :authenticate_user! before_action :set_user diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb index c78f957..c745bc3 100644 --- a/app/views/devise/registrations/new.html.erb +++ b/app/views/devise/registrations/new.html.erb @@ -1 +1 @@ -<%= render Views::Devise::Registrations::New.new(resource:) %> \ No newline at end of file +<%= render Views::Devise::Registrations::New.new(resource:, resource_name:) %> \ No newline at end of file diff --git a/app/views/devise/registrations/new.rb b/app/views/devise/registrations/new.rb index 2581bf9..fc68179 100644 --- a/app/views/devise/registrations/new.rb +++ b/app/views/devise/registrations/new.rb @@ -1,14 +1,41 @@ # frozen_string_literal: true class Views::Devise::Registrations::New < Views::Base - def initialize(resource:) + include Phlex::Rails::Helpers::FormFor + + attr_reader :resource, :resource_name + + def initialize(resource:, resource_name:) @resource = resource + @resource_name = resource_name end def view_template CenteredForm do |cf| cf.title { title t('devise.registrations.new.title') } - render RegistrationForm.new(@resource, cf:) + form_for(resource, as: resource_name, url: user_registration_path, builder: RuiFormBuilder) do |f| + cf.section do + FormField do + f.label :email, t('devise.registrations.new.email') + f.email_field :email, autofocus: true, autocomplete: 'email' + f.errors :email + FormFieldHint { t('devise.registrations.new.email_hint') } + end + FormField do + f.label :password, t('devise.registrations.new.password') + f.password_field :password, autocomplete: 'new-password' + f.errors :password + end + FormField do + f.label :password_confirmation + f.password_field :password_confirmation, autocomplete: 'new-password' + f.errors :password_confirmation + end + end + cf.footer do + f.submit t('devise.registrations.new.submit') + end + end end end end diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 7efd87e..2a0092d 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -1 +1 @@ -<%= render Views::Devise::Sessions::New.new(resource:) %> \ No newline at end of file +<%= render Views::Devise::Sessions::New.new(resource:, resource_name:) %> \ No newline at end of file diff --git a/app/views/devise/sessions/new.rb b/app/views/devise/sessions/new.rb index e683f58..4b1d18c 100644 --- a/app/views/devise/sessions/new.rb +++ b/app/views/devise/sessions/new.rb @@ -1,14 +1,34 @@ # frozen_string_literal: true class Views::Devise::Sessions::New < Views::Base - def initialize(resource:) + include Phlex::Rails::Helpers::FormFor + + attr_reader :resource, :resource_name + + def initialize(resource:, resource_name:) @resource = resource + @resource_name = resource_name end def view_template CenteredForm do |cf| cf.title { title t('devise.sessions.new.title') } - render SessionForm.new(@resource, cf:) + form_for(resource, as: resource_name, url: user_session_path, builder: RuiFormBuilder) do |f| + cf.section do + FormField do + f.label :email + f.email_field :email, autofocus: true, autocomplete: 'email' + end + FormField do + f.label :password + f.password_field :password, autocomplete: 'current-password' + end + f.labelled_checkbox :remember_me + end + cf.footer do + f.submit t('devise.sessions.new.submit') + end + end end end end diff --git a/app/views/forms/application_form.rb b/app/views/forms/application_form.rb deleted file mode 100644 index 804088e..0000000 --- a/app/views/forms/application_form.rb +++ /dev/null @@ -1,70 +0,0 @@ -class ApplicationForm < Superform::Rails::Form - class CheckboxComponent < Superform::Rails::Components::CheckboxComponent - def view_template(&) - input(name: dom.name, type: :hidden, value: "0") - div(class: 'flex items-center space-x-3') do - Checkbox(value: "1", **attributes) - label(for: dom.id, class: 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70') { field.label_string } - end - end - end - - class InputComponent < Superform::Rails::Components::InputComponent - def view_template(&) - Input(**attributes, &) - FormFieldError { field.error_message } - end - end - - class LabelComponent < Superform::Rails::Components::LabelComponent - def view_template(&content) - content ||= Proc.new { field.label_string } - FormFieldLabel(**attributes, &content) - end - end - - class Field < Field - def errors - @object.errors[@key] - end - - def error_message - errors.to_sentence if errors.present? - end - - def label_string - @object.class.human_attribute_name(@key) - end - - def checkbox(**attributes) - CheckboxComponent.new(self, attributes:) - end - - def input(**attributes) - InputComponent.new(self, attributes:) - end - - def label(**attributes, &) - LabelComponent.new(self, attributes:, &) - end - end - - def form_class - 'space-y-4' - end - - def form_tag(&) - Form(action: form_action, method: form_method, class: form_class, **@attributes, &) - end - - def row(component) - FormField do - render component.field.label - render component - end - end - - def submit(value = submit_value, **attributes) - Button(**attributes.merge(type: :submit)) { value } - end -end diff --git a/app/views/forms/registration_form.rb b/app/views/forms/registration_form.rb deleted file mode 100644 index 18a41d7..0000000 --- a/app/views/forms/registration_form.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -class RegistrationForm < ApplicationForm - attr_reader :cf - - def initialize(*, **) - super - @cf = @attributes.delete(:cf) # CenteredForm - end - - def view_template(&) - cf.section do - row field(:email).input - row field(:password).input(type: :password) - row field(:password_confirmation).input(type: :password) - end - cf.footer do - submit t('devise.registrations.new.submit') - end - end -end diff --git a/app/views/forms/session_form.rb b/app/views/forms/session_form.rb deleted file mode 100644 index 67b5a8a..0000000 --- a/app/views/forms/session_form.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -class SessionForm < ApplicationForm - attr_reader :cf - - def initialize(*, **) - super - @cf = @attributes.delete(:cf) # CenteredForm - end - - def view_template(&) - cf.section do - row field(:email).input - row field(:password).input(type: :password) - render field(:remember_me).checkbox - end - cf.footer do - submit t('devise.sessions.new.submit') - end - end -end diff --git a/config/application.rb b/config/application.rb index 9ad8bdd..ff64030 100644 --- a/config/application.rb +++ b/config/application.rb @@ -44,7 +44,7 @@ class Application < Rails::Application # not contain `.rb` files, or that should not be reloaded or eager loaded. # Common ones are `templates`, `generators`, or `middleware`, for example. config.autoload_lib(ignore: %w[assets tasks]) - config.autoload_paths << "#{root}/app/views/forms" + config.autoload_paths << root.join('app/builders') # Configuration for the application, engines, and railties goes here. # From 3d7dfb37313bd312a8d2fac22de06bb08fc43362 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Mon, 28 Jul 2025 17:52:02 +0100 Subject: [PATCH 30/44] Add flashes (not very happy with how they look but they'll do for now --- app/components/centered_form.rb | 2 +- app/components/flashes.rb | 26 ++++++++++++++ app/components/ruby_ui/alert/alert.rb | 36 +++++++++++++++++++ .../ruby_ui/alert/alert_description.rb | 17 +++++++++ app/components/ruby_ui/alert/alert_title.rb | 17 +++++++++ app/components/shell_layout.rb | 8 +++-- app/views/devise/base.rb | 12 +++++++ app/views/devise/registrations/new.rb | 11 +----- app/views/devise/sessions/new.rb | 12 ++----- config/locales/devise.en.yml | 4 +-- 10 files changed, 120 insertions(+), 25 deletions(-) create mode 100644 app/components/flashes.rb create mode 100644 app/components/ruby_ui/alert/alert.rb create mode 100644 app/components/ruby_ui/alert/alert_description.rb create mode 100644 app/components/ruby_ui/alert/alert_title.rb create mode 100644 app/views/devise/base.rb diff --git a/app/components/centered_form.rb b/app/components/centered_form.rb index 821d485..a4ae57d 100644 --- a/app/components/centered_form.rb +++ b/app/components/centered_form.rb @@ -2,7 +2,7 @@ class Components::CenteredForm < Components::Base def view_template(&) - ShellLayout(main_class: 'grow flex items-center justify-center') do + ShellLayout(main_class: 'grow flex flex-col items-center justify-center', flashes_class: 'w-screen max-w-lg mx-3 sm:mx-0 pb-6') do div(class: 'w-screen max-w-lg', &) end end diff --git a/app/components/flashes.rb b/app/components/flashes.rb new file mode 100644 index 0000000..eadb2d1 --- /dev/null +++ b/app/components/flashes.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class Components::Flashes < Components::Base + include Phlex::Rails::Helpers::Flash + + def initialize(class: nil) + @class = grab(class:) + end + + def view_template(&) + if flash[:notice] || flash[:alert] + div(class: @class) do + if flash[:notice] + Alert do + AlertTitle { flash[:notice] } + end + end + if flash[:alert] + Alert(variant: :destructive) do + AlertTitle { flash[:alert] } + end + end + end + end + end +end diff --git a/app/components/ruby_ui/alert/alert.rb b/app/components/ruby_ui/alert/alert.rb new file mode 100644 index 0000000..c83352d --- /dev/null +++ b/app/components/ruby_ui/alert/alert.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module RubyUI + class Alert < Base + def initialize(variant: nil, **attrs) + @variant = variant + super(**attrs) # must be called after variant is set + end + + def view_template(&) + div(**attrs, &) + end + + private + + def colors + case @variant + when nil + "ring-border bg-muted/20 text-foreground [&>svg]:opacity-80" + when :warning + "ring-warning/20 bg-warning/5 text-warning [&>svg]:text-warning/80" + when :success + "ring-success/20 bg-success/5 text-success [&>svg]:text-success/80" + when :destructive + "ring-destructive/20 bg-destructive/5 text-destructive [&>svg]:text-destructive/80" + end + end + + def default_attrs + base_classes = "backdrop-blur relative w-full ring-1 ring-inset rounded-lg px-4 py-4 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:start-4 [&>svg]:top-4 [&>svg~*]:ps-8" + { + class: [base_classes, colors] + } + end + end +end diff --git a/app/components/ruby_ui/alert/alert_description.rb b/app/components/ruby_ui/alert/alert_description.rb new file mode 100644 index 0000000..b36a15d --- /dev/null +++ b/app/components/ruby_ui/alert/alert_description.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class AlertDescription < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: "text-sm [&_p]:leading-relaxed" + } + end + end +end diff --git a/app/components/ruby_ui/alert/alert_title.rb b/app/components/ruby_ui/alert/alert_title.rb new file mode 100644 index 0000000..37e2fff --- /dev/null +++ b/app/components/ruby_ui/alert/alert_title.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class AlertTitle < Base + def view_template(&) + h5(**attrs, &) + end + + private + + def default_attrs + { + class: "mb-1 font-medium leading-none tracking-tight" + } + end + end +end diff --git a/app/components/shell_layout.rb b/app/components/shell_layout.rb index a4566cd..4123bb1 100644 --- a/app/components/shell_layout.rb +++ b/app/components/shell_layout.rb @@ -4,8 +4,9 @@ class Components::ShellLayout < Components::Layout register_value_helper :user_signed_in? register_value_helper :current_user - def initialize(main_class: 'flex flex-col grow px-2 sm:px-4 sm:container py-4 sm:py-8') + def initialize(main_class: 'flex flex-col grow px-2 sm:px-4 sm:container py-4 sm:py-8', flashes_class: 'sm:px-4 sm:container py-4 sm:py-8') @main_class = main_class + @flashes_class = flashes_class end def view_template(&) @@ -21,7 +22,10 @@ def view_template(&) header.action_link(new_user_registration_path) { t('devise.registrations.new.title') } end end - main(class: @main_class, &) + main(class: @main_class) do + Flashes(class: @flashes_class) + yield + end end end diff --git a/app/views/devise/base.rb b/app/views/devise/base.rb new file mode 100644 index 0000000..77552fa --- /dev/null +++ b/app/views/devise/base.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class Views::Devise::Base < Views::Base + include Phlex::Rails::Helpers::FormFor + + attr_reader :resource, :resource_name + + def initialize(resource:, resource_name:) + @resource = resource + @resource_name = resource_name + end +end \ No newline at end of file diff --git a/app/views/devise/registrations/new.rb b/app/views/devise/registrations/new.rb index fc68179..e97584a 100644 --- a/app/views/devise/registrations/new.rb +++ b/app/views/devise/registrations/new.rb @@ -1,15 +1,6 @@ # frozen_string_literal: true -class Views::Devise::Registrations::New < Views::Base - include Phlex::Rails::Helpers::FormFor - - attr_reader :resource, :resource_name - - def initialize(resource:, resource_name:) - @resource = resource - @resource_name = resource_name - end - +class Views::Devise::Registrations::New < Views::Devise::Base def view_template CenteredForm do |cf| cf.title { title t('devise.registrations.new.title') } diff --git a/app/views/devise/sessions/new.rb b/app/views/devise/sessions/new.rb index 4b1d18c..e5f356c 100644 --- a/app/views/devise/sessions/new.rb +++ b/app/views/devise/sessions/new.rb @@ -1,15 +1,7 @@ # frozen_string_literal: true -class Views::Devise::Sessions::New < Views::Base - include Phlex::Rails::Helpers::FormFor - - attr_reader :resource, :resource_name - - def initialize(resource:, resource_name:) - @resource = resource - @resource_name = resource_name - end - +class Views::Devise::Sessions::New < Views::Devise::Base + include Phlex::Rails::Helpers::Debug def view_template CenteredForm do |cf| cf.title { title t('devise.sessions.new.title') } diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 58885ca..b6e6166 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -12,10 +12,10 @@ en: failure: already_authenticated: "You are already signed in." inactive: "Your account is not activated yet." - invalid: "Invalid %{authentication_keys} or password." + invalid: "Invalid email address or password." locked: "Your account is locked." last_attempt: "You have one more attempt before your account is locked." - not_found_in_database: "Invalid %{authentication_keys} or password." + not_found_in_database: "Invalid email address or password." timeout: "Your session expired. Please sign in again to continue." unauthenticated: "You need to sign in or sign up before continuing." unconfirmed: "You have to confirm your email address before continuing." From e082a51cc80ed5a3d8878daa13f2445ddd63cdb5 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 7 Aug 2025 15:17:17 +0100 Subject: [PATCH 31/44] Much of the scaffolding for the home page --- app/components/base.rb | 2 + app/components/home/card.rb | 54 ++++++++++++ app/components/page_intro.rb | 18 ++++ app/components/prose.rb | 2 +- app/components/ruby_ui/card/card.rb | 17 ++++ app/components/ruby_ui/card/card_content.rb | 17 ++++ .../ruby_ui/card/card_description.rb | 17 ++++ app/components/ruby_ui/card/card_footer.rb | 17 ++++ app/components/ruby_ui/card/card_header.rb | 17 ++++ app/components/ruby_ui/card/card_title.rb | 17 ++++ app/components/ruby_ui/typography/heading.rb | 60 ++++++++++++++ .../ruby_ui/typography/inline_code.rb | 17 ++++ .../ruby_ui/typography/inline_link.rb | 22 +++++ app/components/ruby_ui/typography/text.rb | 53 ++++++++++++ .../typography/typography_blockquote.rb | 17 ++++ app/controllers/home_controller.rb | 2 +- app/views/home/index.rb | 83 +++++++++++++++++++ config/locales/views.en.yml | 32 ++++++- 18 files changed, 461 insertions(+), 3 deletions(-) create mode 100644 app/components/home/card.rb create mode 100644 app/components/page_intro.rb create mode 100644 app/components/ruby_ui/card/card.rb create mode 100644 app/components/ruby_ui/card/card_content.rb create mode 100644 app/components/ruby_ui/card/card_description.rb create mode 100644 app/components/ruby_ui/card/card_footer.rb create mode 100644 app/components/ruby_ui/card/card_header.rb create mode 100644 app/components/ruby_ui/card/card_title.rb create mode 100644 app/components/ruby_ui/typography/heading.rb create mode 100644 app/components/ruby_ui/typography/inline_code.rb create mode 100644 app/components/ruby_ui/typography/inline_link.rb create mode 100644 app/components/ruby_ui/typography/text.rb create mode 100644 app/components/ruby_ui/typography/typography_blockquote.rb create mode 100644 app/views/home/index.rb diff --git a/app/components/base.rb b/app/components/base.rb index 037909e..f0287f6 100644 --- a/app/components/base.rb +++ b/app/components/base.rb @@ -13,6 +13,8 @@ class Components::Base < Phlex::HTML register_output_helper :icon register_output_helper :inline_svg_tag + delegate :current_user, :user_signed_in?, to: :helpers + if Rails.env.development? def before_template comment { "Before #{self.class.name}" } diff --git a/app/components/home/card.rb b/app/components/home/card.rb new file mode 100644 index 0000000..1254bf4 --- /dev/null +++ b/app/components/home/card.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class Components::Home::Card < Components::Base + def initialize(title:, edit: nil, **attributes) + @title = title + @edit = edit + @attributes = attributes + end + + def view_template(&content) + render RubyUI::Card.new(**@attributes) do + CardHeader(class: 'flex-row justify-between items-center') do + CardTitle { @title } + if @edit.present? + Link(variant: :outline, icon: true, href: @edit, title: t('.edit')) { icon 'pencil', class: 'w-6 h-6' } + end + end + content&.call + end + end + + def content(&) + CardContent(&) + end + + def footer(&) + CardFooter(&) + end + + def blurb(**attributes, &) + Text(**mix({class: 'mb-4 text-muted-foreground'}, attributes), &) + end + + # Provide a list of key/value pairs to render as a dl. If value is a list, it + # will be rendered as a ul within the dl + def list(pairs, **attributes) + dl(**attributes) do + pairs.each do |k,v| + dt(class: "font-semibold after:content-[':']") { k } + dd(class: 'ms-6') do + case v + when nil, '' + em(class: 'text-muted-foreground font-italic') { t('.not_set') } + when Array + ul { v.each { li { _1 }}} + else + v + end + end + end + end + end + +end diff --git a/app/components/page_intro.rb b/app/components/page_intro.rb new file mode 100644 index 0000000..345649f --- /dev/null +++ b/app/components/page_intro.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Components::PageIntro < Components::Base + def initialize(title:, paragraphs: [], class: nil, **attributes) + @title = title + @paragraphs = paragraphs + @attributes = attributes + end + + def view_template + section(**mix({ class: "mt-10 mb-15" }, @attributes)) do + Heading(level: 1, class: 'mb-5') { @title } + @paragraphs.each do |para| + Text(as: 'p', size: '5', weight: 'muted', class: 'my-2') { para } + end + end + end +end diff --git a/app/components/prose.rb b/app/components/prose.rb index 702044f..eb86c07 100644 --- a/app/components/prose.rb +++ b/app/components/prose.rb @@ -18,7 +18,7 @@ def initialize(md) def view_template rendered = Commonmarker.to_html(@md, options: CM_OPTIONS) - div(class: 'prose dark:prose-invert') do + div(class: 'prose dark:prose-invert prose-headings:font-bold') do raw safe(rendered) end end diff --git a/app/components/ruby_ui/card/card.rb b/app/components/ruby_ui/card/card.rb new file mode 100644 index 0000000..520c04b --- /dev/null +++ b/app/components/ruby_ui/card/card.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class Card < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: "rounded-xl border bg-background shadow" + } + end + end +end diff --git a/app/components/ruby_ui/card/card_content.rb b/app/components/ruby_ui/card/card_content.rb new file mode 100644 index 0000000..675780c --- /dev/null +++ b/app/components/ruby_ui/card/card_content.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class CardContent < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: "p-6 pt-0" + } + end + end +end diff --git a/app/components/ruby_ui/card/card_description.rb b/app/components/ruby_ui/card/card_description.rb new file mode 100644 index 0000000..c00e743 --- /dev/null +++ b/app/components/ruby_ui/card/card_description.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class CardDescription < Base + def view_template(&) + p(**attrs, &) + end + + private + + def default_attrs + { + class: "text-sm text-muted-foreground" + } + end + end +end diff --git a/app/components/ruby_ui/card/card_footer.rb b/app/components/ruby_ui/card/card_footer.rb new file mode 100644 index 0000000..63e59cc --- /dev/null +++ b/app/components/ruby_ui/card/card_footer.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class CardFooter < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: "items-center p-6 pt-0" + } + end + end +end diff --git a/app/components/ruby_ui/card/card_header.rb b/app/components/ruby_ui/card/card_header.rb new file mode 100644 index 0000000..302da73 --- /dev/null +++ b/app/components/ruby_ui/card/card_header.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class CardHeader < Base + def view_template(&) + div(**attrs, &) + end + + private + + def default_attrs + { + class: "flex flex-col space-y-1.5 p-6" + } + end + end +end diff --git a/app/components/ruby_ui/card/card_title.rb b/app/components/ruby_ui/card/card_title.rb new file mode 100644 index 0000000..87dd16a --- /dev/null +++ b/app/components/ruby_ui/card/card_title.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class CardTitle < Base + def view_template(&) + h3(**attrs, &) + end + + private + + def default_attrs + { + class: "font-semibold leading-none tracking-tight" + } + end + end +end diff --git a/app/components/ruby_ui/typography/heading.rb b/app/components/ruby_ui/typography/heading.rb new file mode 100644 index 0000000..bd883c0 --- /dev/null +++ b/app/components/ruby_ui/typography/heading.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module RubyUI + class Heading < Base + def initialize(level: nil, as: nil, size: nil, **attrs) + @level = level + @as = as + @size = size + super(**attrs) + end + + def view_template(&) + tag = determine_tag + public_send(tag, **attrs, &) + end + + private + + def determine_tag + return @as if @as + return "h#{@level}" if @level + "h1" + end + + def default_attrs + { + class: class_names + } + end + + def class_names + base_classes = "scroll-m-20 font-bold tracking-tight" + size_class = size_to_class[(@size || level_to_size[@level&.to_s] || "6").to_s] + "#{base_classes} #{size_class}" + end + + def size_to_class + { + "1" => "text-xs", + "2" => "text-sm", + "3" => "text-base", + "4" => "text-lg", + "5" => "text-xl", + "6" => "text-2xl", + "7" => "text-3xl lg:text-4xl", + "8" => "text-4xl", + "9" => "text-5xl" + } + end + + def level_to_size + { + "1" => "7", + "2" => "6", + "3" => "5", + "4" => "4" + } + end + end +end diff --git a/app/components/ruby_ui/typography/inline_code.rb b/app/components/ruby_ui/typography/inline_code.rb new file mode 100644 index 0000000..539c961 --- /dev/null +++ b/app/components/ruby_ui/typography/inline_code.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class InlineCode < Base + def view_template(&) + code(**attrs, &) + end + + private + + def default_attrs + { + class: "relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold" + } + end + end +end diff --git a/app/components/ruby_ui/typography/inline_link.rb b/app/components/ruby_ui/typography/inline_link.rb new file mode 100644 index 0000000..07929dd --- /dev/null +++ b/app/components/ruby_ui/typography/inline_link.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module RubyUI + class InlineLink < Base + def initialize(href:, **attrs) + super(**attrs) + @href = href + end + + def view_template(&) + a(href: @href, **attrs, &) + end + + private + + def default_attrs + { + class: "text-primary font-medium hover:underline underline-offset-4 cursor-pointer" + } + end + end +end diff --git a/app/components/ruby_ui/typography/text.rb b/app/components/ruby_ui/typography/text.rb new file mode 100644 index 0000000..b394593 --- /dev/null +++ b/app/components/ruby_ui/typography/text.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module RubyUI + class Text < Base + def initialize(as: "p", size: "3", weight: "regular", **attrs) + @as = as + @size = size + @weight = weight + super(**attrs) + end + + def view_template(&) + public_send(@as, **attrs, &) + end + + private + + def default_attrs + { + class: class_names + } + end + + def class_names + "#{size_to_class[@size]} #{weight_to_class[@weight]}" + end + + def size_to_class + { + "1" => "text-xs", "xs" => "text-xs", + "2" => "text-sm", "sm" => "text-sm", + "3" => "text-base", "base" => "text-base", + "4" => "text-lg", "lg" => "text-lg", + "5" => "text-xl", "xl" => "text-xl", + "6" => "text-2xl", "2xl" => "text-2xl", + "7" => "text-3xl", "3xl" => "text-3xl", + "8" => "text-4xl", "4xl" => "text-4xl", + "9" => "text-5xl", "5xl" => "text-5xl" + } + end + + def weight_to_class + { + "muted" => "text-muted-foreground", + "light" => "font-light", + "regular" => "font-normal", + "medium" => "font-medium", + "semibold" => "font-semibold", + "bold" => "font-bold" + } + end + end +end diff --git a/app/components/ruby_ui/typography/typography_blockquote.rb b/app/components/ruby_ui/typography/typography_blockquote.rb new file mode 100644 index 0000000..b4d4e84 --- /dev/null +++ b/app/components/ruby_ui/typography/typography_blockquote.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class TypographyBlockquote < Base + def view_template(&) + blockquote(**attrs, &) + end + + private + + def default_attrs + { + class: "mt-6 border-l-2 pl-6 italic" + } + end + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 79696eb..6567e70 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -7,7 +7,7 @@ def index if user_signed_in? # Refuse to show the dashboard to people who haven't completed step 1 return redirect_to(profile_index_path) if !current_user.basic_names_complete? - render 'user_home', layout: 'application' + render Views::Home::Index else render Views::Home::Anon end diff --git a/app/views/home/index.rb b/app/views/home/index.rb new file mode 100644 index 0000000..02f1a07 --- /dev/null +++ b/app/views/home/index.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +class Views::Home::Index < Views::Base + include Phlex::Rails::Helpers::ContentTag + + def view_template + page_title = title(t '.title') + + ShellLayout do + PageIntro( + title: page_title, + paragraphs: [t('.page_description1'), t('.page_description2')] + ) + profile_grid + end + end + + protected + + def profile_grid + section(class: 'grid grid-cols-1 md:grid-cols-2 gap-3') do + url_card + div(class: 'flex flex-col gap-3 *:flex-auto') do + name_card + pronouns_card + pronunciation_card + end + div(class: 'flex flex-col gap-3 *:flex-auto') do + variants_card + likeness_card + links_card + end + end + end + + def url_card + render Home::Card.new(title: t('.url_card.title'), edit: profile_path(:slug), class: 'md:col-span-2 bg-indigo-50 dark:bg-indigo-950') do + end + end + + def name_card + render Home::Card.new(title: t('.name_card.title'), edit: profile_path(:personal_name), class: 'bg-rose-50 dark:bg-rose-950') do |hc| + hc.content do + hc.blurb { t('profile.personal_name.blurb') } + hc.list(user_attributes_hash %i[personal_name full_name]) + end + end + end + + def pronouns_card + render Home::Card.new(title: t('.pronouns_card.title'), edit: profile_path(:pronouns), class: 'bg-green-50 dark:bg-green-950') + end + + def pronunciation_card + render Home::Card.new(title: t('.pronunciation_card.title'), edit: profile_path(:pronunciation), class: 'bg-rose-50 dark:bg-rose-950') + end + + def variants_card + render Home::Card.new(title: t('.variants_card.title'), edit: profile_path(:variants), class: 'bg-green-50 dark:bg-green-950') do |hc| + hc.content do + hc.blurb { t('profile.variants.blurb') } + hc.list(user_attributes_hash %i[envelope_name formal_name email_name]) + end + end + end + + def likeness_card + render Home::Card.new(title: t('.likeness_card.title'), edit: profile_path(:likeness), class: 'bg-indigo-50 dark:bg-indigo-950') + end + + def links_card + render Home::Card.new(title: t('.links_card.title'), edit: profile_path(:links), class: 'bg-rose-50 dark:bg-rose-950') + end + + # Convert a list of attribute names to a hash of human name to value + def user_attributes_hash(attributes) + attributes.to_h { [ + User.human_attribute_name(_1), + current_user.public_send(_1), + ] } + end + +end diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index 4d0d213..0c32e3d 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -10,6 +10,9 @@ en: pronouns: pronouns pronounce: pronunciation subtitle: Your name – your rules + card: + not_set: Not set + edit: Edit layouts: navbar: label: main navigation @@ -221,4 +224,31 @@ en: description: You can't guess someone's pronouns (they, she, he, etc.) from their name. Sharing your pronouns helps to create a world where fewer people are misgendered. icon_name: gender-male-female sign_up: Sign up – it's free - learn_more: Learn more about name.pn \ No newline at end of file + learn_more: Learn more about name.pn + index: + title: Your name dashboard + page_description1: Here you can create a page that describes the different ways you would like to be addressed in English. + page_description2: Once you're happy with the page, you can share it with people and put it in your email signature so that everyone knows. + you_must: You must set this before you can share your page + name_card: + title: Your name + variants_card: + title: Variants of your name + pronunciation_card: + title: How to pronounce your name + name_is_pronounced: '%{name} is pronounced' + ipa: 'IPA pronunciation of %{name}' + audio_clip: Audio clip of '%{name}' + pronouns_card: + title: Your pronouns + likeness_card: + title: What you look like + links_card: + title: Your personal links + url_card: + title: Your unique URL + view_your_page: View + copy_to_clipboard: Copy + copied: Copied! + you_must_complete: You must complete the highlighted parts before you can preview your page + or_with_pronouns: 'or, if you prefer a URL containing your pronouns:' From 8301e6a527107fa9a895d122f275a8989f38f266 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 7 Aug 2025 16:06:36 +0100 Subject: [PATCH 32/44] Add name variants - needs a big refactor --- app/components/home/card.rb | 6 ++++-- app/views/home/index.rb | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/components/home/card.rb b/app/components/home/card.rb index 1254bf4..9a43be4 100644 --- a/app/components/home/card.rb +++ b/app/components/home/card.rb @@ -12,7 +12,7 @@ def view_template(&content) CardHeader(class: 'flex-row justify-between items-center') do CardTitle { @title } if @edit.present? - Link(variant: :outline, icon: true, href: @edit, title: t('.edit')) { icon 'pencil', class: 'w-6 h-6' } + Link(variant: :outline, icon: true, href: @edit, title: t('.edit')) { icon 'pencil', class: 'p-2 fill-current' } end end content&.call @@ -42,7 +42,9 @@ def list(pairs, **attributes) when nil, '' em(class: 'text-muted-foreground font-italic') { t('.not_set') } when Array - ul { v.each { li { _1 }}} + ul(class: 'list-disc') do + v.each {|name| li { name }} + end else v end diff --git a/app/views/home/index.rb b/app/views/home/index.rb index 02f1a07..5025554 100644 --- a/app/views/home/index.rb +++ b/app/views/home/index.rb @@ -59,7 +59,11 @@ def variants_card render Home::Card.new(title: t('.variants_card.title'), edit: profile_path(:variants), class: 'bg-green-50 dark:bg-green-950') do |hc| hc.content do hc.blurb { t('profile.variants.blurb') } - hc.list(user_attributes_hash %i[envelope_name formal_name email_name]) + attributes = user_attributes_hash %i[envelope_name formal_name email_name] + current_user.alternate_names.grouped_by_category.each do |category, ans| + attributes[t(category, scope: 'alternate_name_categories')] = ans.map(&:name) + end + hc.list(attributes) end end end From ff05e9c2b5056c438f518dd0d2890445e25e9bf7 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Fri, 8 Aug 2025 10:51:46 +0100 Subject: [PATCH 33/44] Refactor --- app/components/home/card.rb | 34 +++++++++++++++++----------------- app/views/home/index.rb | 15 +++++++++++---- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/app/components/home/card.rb b/app/components/home/card.rb index 9a43be4..ad33aa1 100644 --- a/app/components/home/card.rb +++ b/app/components/home/card.rb @@ -11,9 +11,7 @@ def view_template(&content) render RubyUI::Card.new(**@attributes) do CardHeader(class: 'flex-row justify-between items-center') do CardTitle { @title } - if @edit.present? - Link(variant: :outline, icon: true, href: @edit, title: t('.edit')) { icon 'pencil', class: 'p-2 fill-current' } - end + Link(variant: :outline, icon: true, href: @edit, title: t('.edit')) { icon 'pencil', class: 'p-2 fill-current' } if @edit.present? end content&.call end @@ -31,26 +29,28 @@ def blurb(**attributes, &) Text(**mix({class: 'mb-4 text-muted-foreground'}, attributes), &) end - # Provide a list of key/value pairs to render as a dl. If value is a list, it - # will be rendered as a ul within the dl + # Provide a list of key/value pairs to render as a dl. def list(pairs, **attributes) dl(**attributes) do pairs.each do |k,v| dt(class: "font-semibold after:content-[':']") { k } - dd(class: 'ms-6') do - case v - when nil, '' - em(class: 'text-muted-foreground font-italic') { t('.not_set') } - when Array - ul(class: 'list-disc') do - v.each {|name| li { name }} - end - else - v - end - end + dd(class: 'ms-6') { list_value v } end end end + # If value is a list, it will be rendered as a ul + def list_value(v) + case v + when nil, '' + em(class: 'text-muted-foreground font-italic') { t('.not_set') } + when Array + ul(class: 'list-disc') do + v.each {|name| li { name }} + end + else + v + end + end + end diff --git a/app/views/home/index.rb b/app/views/home/index.rb index 5025554..ba58984 100644 --- a/app/views/home/index.rb +++ b/app/views/home/index.rb @@ -60,10 +60,7 @@ def variants_card hc.content do hc.blurb { t('profile.variants.blurb') } attributes = user_attributes_hash %i[envelope_name formal_name email_name] - current_user.alternate_names.grouped_by_category.each do |category, ans| - attributes[t(category, scope: 'alternate_name_categories')] = ans.map(&:name) - end - hc.list(attributes) + hc.list(attributes.merge(human_alternate_names)) end end end @@ -84,4 +81,14 @@ def user_attributes_hash(attributes) ] } end + # Convert current_user.alternate_names.grouped_by_category to human-readable form + def human_alternate_names + current_user.alternate_names.grouped_by_category.to_h do |cat,v| + [ + t(cat, scope: 'alternate_name_categories'), + v.map(&:name) + ] + end + end + end From 76b81f606e9c08a69b748a9c795d2052f18f0ad3 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Fri, 8 Aug 2025 11:26:51 +0100 Subject: [PATCH 34/44] Pronoun card --- app/assets/tailwind/application.css | 3 ++ app/components/home/card.rb | 2 +- app/components/pronoun_set/table.rb | 43 +++++++++++++++++++ app/components/ruby_ui/table/table.rb | 19 ++++++++ app/components/ruby_ui/table/table_body.rb | 17 ++++++++ app/components/ruby_ui/table/table_caption.rb | 17 ++++++++ app/components/ruby_ui/table/table_cell.rb | 17 ++++++++ app/components/ruby_ui/table/table_footer.rb | 17 ++++++++ app/components/ruby_ui/table/table_head.rb | 17 ++++++++ app/components/ruby_ui/table/table_header.rb | 17 ++++++++ app/components/ruby_ui/table/table_row.rb | 17 ++++++++ app/views/home/index.rb | 7 ++- config/locales/views.en.yml | 3 ++ 13 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 app/components/pronoun_set/table.rb create mode 100644 app/components/ruby_ui/table/table.rb create mode 100644 app/components/ruby_ui/table/table_body.rb create mode 100644 app/components/ruby_ui/table/table_caption.rb create mode 100644 app/components/ruby_ui/table/table_cell.rb create mode 100644 app/components/ruby_ui/table/table_footer.rb create mode 100644 app/components/ruby_ui/table/table_head.rb create mode 100644 app/components/ruby_ui/table/table_header.rb create mode 100644 app/components/ruby_ui/table/table_row.rb diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index a7fb5f2..530e055 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -151,4 +151,7 @@ body { @apply bg-background text-foreground; } + b { + @apply font-semibold; + } } diff --git a/app/components/home/card.rb b/app/components/home/card.rb index ad33aa1..533fe1a 100644 --- a/app/components/home/card.rb +++ b/app/components/home/card.rb @@ -10,7 +10,7 @@ def initialize(title:, edit: nil, **attributes) def view_template(&content) render RubyUI::Card.new(**@attributes) do CardHeader(class: 'flex-row justify-between items-center') do - CardTitle { @title } + CardTitle(class: 'm-0') { @title } Link(variant: :outline, icon: true, href: @edit, title: t('.edit')) { icon 'pencil', class: 'p-2 fill-current' } if @edit.present? end content&.call diff --git a/app/components/pronoun_set/table.rb b/app/components/pronoun_set/table.rb new file mode 100644 index 0000000..aaeeb49 --- /dev/null +++ b/app/components/pronoun_set/table.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class Components::PronounSet::Table < Components::Base + def initialize(pronoun_sets:, user:) + @pronoun_sets = pronoun_sets + @user = user + end + + def view_template + render RubyUI::Table.new do + header + body + end + end + + def header + TableHeader do + TableRow do + @pronoun_sets.each do |pronoun_set| + TableHead { t('.header', pronoun: pronoun_set.to_s(@user)) } + end + end + end + end + + def body + TableBody do + inflexes.each do |inflex| + TableRow do + @pronoun_sets.each do |pronoun_set| + pronoun = (inflex == :name ? @user.personal_name : pronoun_set.public_send(inflex)) + TableCell { raw safe(t(inflex, pronoun: pronoun, pronoun_initial: pronoun.capitalize, nominative: pronoun_set.nominative, nominative_initial: pronoun_set.nominative.capitalize, copula: pronoun_set.copula, personal_name: @user.personal_name, scope: "pronoun_examples.#{@user.pronoun_example}")) } + end + end + end + end + end + + def inflexes + %i[name nominative oblique possessive_determiner possessive reflexive] + end + +end diff --git a/app/components/ruby_ui/table/table.rb b/app/components/ruby_ui/table/table.rb new file mode 100644 index 0000000..d3f2c37 --- /dev/null +++ b/app/components/ruby_ui/table/table.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module RubyUI + class Table < Base + def view_template(&block) + div(class: "relative w-full overflow-auto") do + table(**attrs, &block) + end + end + + private + + def default_attrs + { + class: "w-full caption-bottom text-sm" + } + end + end +end diff --git a/app/components/ruby_ui/table/table_body.rb b/app/components/ruby_ui/table/table_body.rb new file mode 100644 index 0000000..84891e2 --- /dev/null +++ b/app/components/ruby_ui/table/table_body.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class TableBody < Base + def view_template(&) + tbody(**attrs, &) + end + + private + + def default_attrs + { + class: "[&_tr:last-child]:border-0" + } + end + end +end diff --git a/app/components/ruby_ui/table/table_caption.rb b/app/components/ruby_ui/table/table_caption.rb new file mode 100644 index 0000000..21335a8 --- /dev/null +++ b/app/components/ruby_ui/table/table_caption.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class TableCaption < Base + def view_template(&) + caption(**attrs, &) + end + + private + + def default_attrs + { + class: "mt-4 text-sm text-muted-foreground" + } + end + end +end diff --git a/app/components/ruby_ui/table/table_cell.rb b/app/components/ruby_ui/table/table_cell.rb new file mode 100644 index 0000000..6d7e353 --- /dev/null +++ b/app/components/ruby_ui/table/table_cell.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class TableCell < Base + def view_template(&) + td(**attrs, &) + end + + private + + def default_attrs + { + class: "p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]" + } + end + end +end diff --git a/app/components/ruby_ui/table/table_footer.rb b/app/components/ruby_ui/table/table_footer.rb new file mode 100644 index 0000000..156d768 --- /dev/null +++ b/app/components/ruby_ui/table/table_footer.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class TableFooter < Base + def view_template(&) + tfoot(**attrs, &) + end + + private + + def default_attrs + { + class: "border-t bg-muted bg-opacity-50 font-medium[& amp;>tr]:last:border-b-0" + } + end + end +end diff --git a/app/components/ruby_ui/table/table_head.rb b/app/components/ruby_ui/table/table_head.rb new file mode 100644 index 0000000..c0c8d0e --- /dev/null +++ b/app/components/ruby_ui/table/table_head.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class TableHead < Base + def view_template(&) + th(**attrs, &) + end + + private + + def default_attrs + { + class: "h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]" + } + end + end +end diff --git a/app/components/ruby_ui/table/table_header.rb b/app/components/ruby_ui/table/table_header.rb new file mode 100644 index 0000000..5d632e8 --- /dev/null +++ b/app/components/ruby_ui/table/table_header.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class TableHeader < Base + def view_template(&) + thead(**attrs, &) + end + + private + + def default_attrs + { + class: "[&_tr]:border-b" + } + end + end +end diff --git a/app/components/ruby_ui/table/table_row.rb b/app/components/ruby_ui/table/table_row.rb new file mode 100644 index 0000000..4b451df --- /dev/null +++ b/app/components/ruby_ui/table/table_row.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module RubyUI + class TableRow < Base + def view_template(&) + tr(**attrs, &) + end + + private + + def default_attrs + { + class: "border-b transition-colors hover:bg-muted hover:bg-opacity-50 data-[state=selected]:bg-muted" + } + end + end +end diff --git a/app/views/home/index.rb b/app/views/home/index.rb index ba58984..cba8986 100644 --- a/app/views/home/index.rb +++ b/app/views/home/index.rb @@ -48,7 +48,12 @@ def name_card end def pronouns_card - render Home::Card.new(title: t('.pronouns_card.title'), edit: profile_path(:pronouns), class: 'bg-green-50 dark:bg-green-950') + render Home::Card.new(title: t('.pronouns_card.title'), edit: profile_path(:pronouns), class: 'bg-green-50 dark:bg-green-950') do |hc| + hc.content do + hc.blurb { t('profile.pronouns.blurb')&.first } + render PronounSet::Table.new(pronoun_sets: current_user.pronoun_sets, user: current_user) + end + end end def pronunciation_card diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index 0c32e3d..8bf0594 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -13,6 +13,9 @@ en: card: not_set: Not set edit: Edit + pronoun_set: + table: + header: 'Pronouns: %{pronoun}' layouts: navbar: label: main navigation From 971147fb6209762465a2e405788ae67900b8be59 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Fri, 8 Aug 2025 12:37:04 +0100 Subject: [PATCH 35/44] Add the likeness card --- app/assets/tailwind/application.css | 10 ++++++++++ app/components/home/card.rb | 7 ++++++- app/components/user/likeness.rb | 24 ++++++++++++++++++++++++ app/views/home/index.rb | 13 +++++++++++-- config/locales/views.en.yml | 4 ++++ 5 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 app/components/user/likeness.rb diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index 530e055..c4901f4 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -4,6 +4,7 @@ @plugin "@tailwindcss/typography"; @import "tw-animate-css"; +@import "luminous-lightbox/dist/luminous-basic"; @custom-variant dark (&:is(.dark *)); @@ -154,4 +155,13 @@ b { @apply font-semibold; } + .lum-lightbox { + @apply z-100; + } + .lum-lightbox-image-wrapper { + @apply !flex items-center justify-center; + } + .lum-img { + @apply rounded-lg; + } } diff --git a/app/components/home/card.rb b/app/components/home/card.rb index 533fe1a..eac604e 100644 --- a/app/components/home/card.rb +++ b/app/components/home/card.rb @@ -43,7 +43,7 @@ def list(pairs, **attributes) def list_value(v) case v when nil, '' - em(class: 'text-muted-foreground font-italic') { t('.not_set') } + not_set when Array ul(class: 'list-disc') do v.each {|name| li { name }} @@ -53,4 +53,9 @@ def list_value(v) end end + def not_set + em(class: 'text-muted-foreground font-italic') { t('.not_set') } + end + + end diff --git a/app/components/user/likeness.rb b/app/components/user/likeness.rb new file mode 100644 index 0000000..3a16079 --- /dev/null +++ b/app/components/user/likeness.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class Components::User::Likeness < Components::Base + attr_reader :user, :attributes + + def initialize(user:, **attributes) + @user = user + @attributes = attributes + end + + def view_template + a(**mix({ + href: url_for(user.likeness), + data: {controller: 'lightbox'}, + title: t('.expand'), + class: 'cursor-zoom-in' + }, attributes) + ) do + image_tag user.likeness.variant(resize_to_limit: [300, 300]), + alt: t('.alt', name: user.full_name), + class: 'rounded-full' + end + end +end diff --git a/app/views/home/index.rb b/app/views/home/index.rb index cba8986..570d58d 100644 --- a/app/views/home/index.rb +++ b/app/views/home/index.rb @@ -71,7 +71,16 @@ def variants_card end def likeness_card - render Home::Card.new(title: t('.likeness_card.title'), edit: profile_path(:likeness), class: 'bg-indigo-50 dark:bg-indigo-950') + render Home::Card.new(title: t('.likeness_card.title'), edit: profile_path(:likeness), class: 'bg-indigo-50 dark:bg-indigo-950') do |hc| + hc.content do + hc.blurb { t('profile.likeness.blurb') } + if current_user.likeness.attached? + render Components::User::Likeness.new(user: current_user) + else + hc.not_set + end + end + end end def links_card @@ -81,7 +90,7 @@ def links_card # Convert a list of attribute names to a hash of human name to value def user_attributes_hash(attributes) attributes.to_h { [ - User.human_attribute_name(_1), + ::User.human_attribute_name(_1), current_user.public_send(_1), ] } end diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index 8bf0594..8020392 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -16,6 +16,10 @@ en: pronoun_set: table: header: 'Pronouns: %{pronoun}' + user: + likeness: + expand: Expand + alt: A picture of %{name} layouts: navbar: label: main navigation From 0d411422f1e573324a4704cbf1e8adbbbdd90c21 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Mon, 11 Aug 2025 10:20:02 +0100 Subject: [PATCH 36/44] Some k8s tsetup to ensure any ActiveStorage in dev persists across builds --- Tiltfile | 7 ++++++- k8s/storage-pv.yaml | 14 ++++++++++++++ k8s/storage-pvc.yaml | 11 +++++++++++ k8s.yaml => k8s/web.yaml | 7 +++++++ 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 k8s/storage-pv.yaml create mode 100644 k8s/storage-pvc.yaml rename k8s.yaml => k8s/web.yaml (79%) diff --git a/Tiltfile b/Tiltfile index 1a84ef3..474054d 100644 --- a/Tiltfile +++ b/Tiltfile @@ -25,6 +25,11 @@ helm_resource( labels=['database'], ) +# Set up a persistent volume for ActiveStorage so files are not deleted on +# restart +k8s_yaml('k8s/storage-pv.yaml') +k8s_yaml('k8s/storage-pvc.yaml') + # The Rails app itself is built and served by app.yaml podman_build('name-pn', '.', ignore=['log', 'tmp'], @@ -37,7 +42,7 @@ podman_build('name-pn', '.', run('yarn build:css', trigger=['./app/assets/stylesheets', './app/admin']), run('yarn check-types', trigger=["./app/javascript"]), ]) -k8s_yaml('k8s.yaml') +k8s_yaml('k8s/web.yaml') k8s_resource('name-pn', labels=['app'], resource_deps=['name-pn-postgresql'], diff --git a/k8s/storage-pv.yaml b/k8s/storage-pv.yaml new file mode 100644 index 0000000..a0938cb --- /dev/null +++ b/k8s/storage-pv.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: name-pn-storage-volume + labels: + type: local +spec: + storageClassName: manual + capacity: + storage: 10Gi + accessModes: + - ReadWriteMany + hostPath: + path: "/mnt/storage" diff --git a/k8s/storage-pvc.yaml b/k8s/storage-pvc.yaml new file mode 100644 index 0000000..b01464f --- /dev/null +++ b/k8s/storage-pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: name-pn-storage-claim +spec: + storageClassName: manual + accessModes: + - ReadWriteMany + resources: + requests: + storage: 10Gi diff --git a/k8s.yaml b/k8s/web.yaml similarity index 79% rename from k8s.yaml rename to k8s/web.yaml index f4da64d..d8cb8b0 100644 --- a/k8s.yaml +++ b/k8s/web.yaml @@ -13,11 +13,18 @@ spec: labels: app: name-pn spec: + volumes: + - name: name-pn-storage + persistentVolumeClaim: + claimName: name-pn-storage-claim containers: - name: name-pn image: name-pn ports: - containerPort: 3000 + volumeMounts: + - mountPath: /rails/storage + name: name-pn-storage env: - name: JWT_SECRET valueFrom: From dbdb4f25fbf301ca125de95263abc94ebdc0024b Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Tue, 12 Aug 2025 17:58:44 +0100 Subject: [PATCH 37/44] Build the URL card and a big refactor in the process --- app/assets/tailwind/application.css | 40 ++++++++++ app/components/home/card.rb | 26 +++++-- app/components/home/card/likeness.rb | 16 ++++ app/components/home/card/links.rb | 7 ++ app/components/home/card/name.rb | 12 +++ app/components/home/card/pronouns.rb | 16 ++++ app/components/home/card/pronunciation.rb | 7 ++ app/components/home/card/url.rb | 67 ++++++++++++++++ app/components/home/card/variants.rb | 25 ++++++ app/javascript/controllers/url_controller.ts | 2 +- app/views/home/index.rb | 81 ++------------------ config/locales/views.en.yml | 46 +++++------ 12 files changed, 243 insertions(+), 102 deletions(-) create mode 100644 app/components/home/card/likeness.rb create mode 100644 app/components/home/card/links.rb create mode 100644 app/components/home/card/name.rb create mode 100644 app/components/home/card/pronouns.rb create mode 100644 app/components/home/card/pronunciation.rb create mode 100644 app/components/home/card/url.rb create mode 100644 app/components/home/card/variants.rb diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index c4901f4..e1ec62d 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -136,6 +136,46 @@ --color-warning-foreground: var(--warning-foreground); --color-success: var(--success); --color-success-foreground: var(--success-foreground); + + /* Taken from animate.css https://github.com/animate-css/animate.css/blob/main/LICENSE */ + --animate-bounce-in: bounce-in 0.75s 1; + @keyframes bounce-in { + from, + 20%, + 40%, + 60%, + 80%, + to { + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + + 0% { + opacity: 0; + transform: scale3d(0.3, 0.3, 0.3); + } + + 20% { + transform: scale3d(1.1, 1.1, 1.1); + } + + 40% { + transform: scale3d(0.9, 0.9, 0.9); + } + + 60% { + opacity: 1; + transform: scale3d(1.03, 1.03, 1.03); + } + + 80% { + transform: scale3d(0.97, 0.97, 0.97); + } + + to { + opacity: 1; + transform: scale3d(1, 1, 1); + } + } } /* Container settings */ diff --git a/app/components/home/card.rb b/app/components/home/card.rb index eac604e..53de32a 100644 --- a/app/components/home/card.rb +++ b/app/components/home/card.rb @@ -22,7 +22,11 @@ def content(&) end def footer(&) - CardFooter(&) + CardFooter(class: 'flex gap-2 pt-6', &) + end + + def footer_button(component=RubyUI::Link, **attributes, &) + render component.new(**mix({class: 'grow basis-0 bg-background cursor-pointer h-auto p-3', variant: :ghost}, attributes), &) end def blurb(**attributes, &) @@ -30,20 +34,20 @@ def blurb(**attributes, &) end # Provide a list of key/value pairs to render as a dl. - def list(pairs, **attributes) + def list(pairs, required: false, **attributes) dl(**attributes) do pairs.each do |k,v| dt(class: "font-semibold after:content-[':']") { k } - dd(class: 'ms-6') { list_value v } + dd(class: 'ms-6') { list_value v, required: } end end end # If value is a list, it will be rendered as a ul - def list_value(v) + def list_value(v, required: false) case v when nil, '' - not_set + required ? you_must : not_set when Array ul(class: 'list-disc') do v.each {|name| li { name }} @@ -57,5 +61,17 @@ def not_set em(class: 'text-muted-foreground font-italic') { t('.not_set') } end + def you_must + strong(class: 'font-semibold text-destructive-foreground') { t('.you_must') } + end + + # Convert a list of attribute names to a hash of human name to value + def user_attributes_hash(attributes) + attributes.to_h { [ + ::User.human_attribute_name(_1), + current_user.public_send(_1), + ] } + end + end diff --git a/app/components/home/card/likeness.rb b/app/components/home/card/likeness.rb new file mode 100644 index 0000000..09e8e73 --- /dev/null +++ b/app/components/home/card/likeness.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class Components::Home::Card::Likeness < Components::Base + def view_template + render Home::Card.new(title: t('.title'), edit: profile_path(:likeness), class: 'bg-indigo-50 dark:bg-indigo-950') do |hc| + hc.content do + hc.blurb { t('profile.likeness.blurb') } + if current_user.likeness.attached? + render Components::User::Likeness.new(user: current_user) + else + hc.not_set + end + end + end + end +end diff --git a/app/components/home/card/links.rb b/app/components/home/card/links.rb new file mode 100644 index 0000000..5dfe1fc --- /dev/null +++ b/app/components/home/card/links.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class Components::Home::Card::Links < Components::Base + def view_template + render Home::Card.new(title: t('.title'), edit: profile_path(:links), class: 'bg-rose-50 dark:bg-rose-950') + end +end diff --git a/app/components/home/card/name.rb b/app/components/home/card/name.rb new file mode 100644 index 0000000..175c849 --- /dev/null +++ b/app/components/home/card/name.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class Components::Home::Card::Name < Components::Base + def view_template + render Home::Card.new(title: t('.title'), edit: profile_path(:personal_name), class: 'bg-rose-50 dark:bg-rose-950') do |hc| + hc.content do + hc.blurb { t('profile.personal_name.blurb') } + hc.list(hc.user_attributes_hash(%i[personal_name full_name]), required: true) + end + end + end +end diff --git a/app/components/home/card/pronouns.rb b/app/components/home/card/pronouns.rb new file mode 100644 index 0000000..e4ce702 --- /dev/null +++ b/app/components/home/card/pronouns.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class Components::Home::Card::Pronouns < Components::Base + def view_template + render Home::Card.new(title: t('.title'), edit: profile_path(:pronouns), class: 'bg-green-50 dark:bg-green-950') do |hc| + hc.content do + hc.blurb { t('profile.pronouns.blurb')&.first } + if current_user.pronoun_sets.any? + render PronounSet::Table.new(pronoun_sets: current_user.pronoun_sets, user: current_user) + else + p { hc.you_must } + end + end + end + end +end diff --git a/app/components/home/card/pronunciation.rb b/app/components/home/card/pronunciation.rb new file mode 100644 index 0000000..39997d6 --- /dev/null +++ b/app/components/home/card/pronunciation.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class Components::Home::Card::Pronunciation < Components::Base + def view_template + render Home::Card.new(title: t('.title'), edit: profile_path(:pronunciation), class: 'bg-rose-50 dark:bg-rose-950') + end +end diff --git a/app/components/home/card/url.rb b/app/components/home/card/url.rb new file mode 100644 index 0000000..c9bde5d --- /dev/null +++ b/app/components/home/card/url.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +class Components::Home::Card::Url < Components::Base + def view_template + render Home::Card.new(title: t('.title'), edit: profile_path(:slug), class: 'md:col-span-2 bg-indigo-50 dark:bg-indigo-950', data: {controller: 'url', url_copied_class: 'is-copied'}) do |hc| + hc.content do + div(class: 'flex flex-col items-center') do + p(class: 'text-2xl font-semibold wrap-anywhere', data: {url_target: 'url'}) { user_url(current_user, only_path: false) } + if current_user.pronoun_sets.present? + p(class: 'text-muted-foreground font-italic mt-6') { t('.or_with_pronouns') } + current_user.pronoun_sets.each do |ps| + p(class: 'text-muted-foreground wrap-anywhere') { user_url(current_user, only_path: false, nominative: ps.nominative, oblique: ps.oblique) } + end + end + end + end + hc.footer do + edit_button(hc) + view_button(hc) + copy_button(hc) + end + end + end + + private + + def edit_button(hc) + hc.footer_button(href: profile_path(:slug)) do + icon 'pencil', class: 'w-5 h-5 fill-current mr-1' + span { t('actions.edit') } + end + end + + def view_button(hc) + if current_user.profile_complete? + hc.footer_button(href: user_url(current_user), target: '_blank') do + icon 'open-in-new', class: 'w-5 h-5 fill-current mr-1' + span { t('.view_your_page') } + end + else + div(title: t('.you_must_complete'), class: 'grow basis-0') do + hc.footer_button(RubyUI::Button, disabled: true, class: 'w-full') do + icon 'open-in-new', class: 'w-5 h-5 fill-current mr-1' + span { t('.view_your_page') } + end + end + end + end + + def copy_button(hc) + if current_user.profile_complete? + hc.footer_button(RubyUI::Button, class: 'group', data: {url_target: 'copyButton', action: 'url#copy'}) do + icon 'clipboard-outline', class: 'group-[.is-copied]:hidden w-5 h-5 fill-current mr-1' + span(class: 'group-[.is-copied]:hidden') { t('.copy_to_clipboard') } + icon 'clipboard-check-outline', class: 'hidden group-[.is-copied]:inline w-5 h-5 fill-current mr-1' + span(class: 'hidden group-[.is-copied]:inline') { t('.copied') } + end + else + div(title: t('.you_must_complete'), class: 'grow basis-0') do + hc.footer_button(RubyUI::Button, disabled: true, class: 'w-full') do + icon 'clipboard-outline', class: 'w-5 h-5 fill-current mr-1' + span { t('.copy_to_clipboard') } + end + end + end + end +end diff --git a/app/components/home/card/variants.rb b/app/components/home/card/variants.rb new file mode 100644 index 0000000..fb92c8e --- /dev/null +++ b/app/components/home/card/variants.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Components::Home::Card::Variants < Components::Base + def view_template + render Home::Card.new(title: t('.title'), edit: profile_path(:variants), class: 'bg-green-50 dark:bg-green-950') do |hc| + hc.content do + hc.blurb { t('profile.variants.blurb') } + attributes = hc.user_attributes_hash %i[envelope_name formal_name email_name] + hc.list(attributes.merge(human_alternate_names)) + end + end + end + + private + + # Convert current_user.alternate_names.grouped_by_category to human-readable form + def human_alternate_names + current_user.alternate_names.grouped_by_category.to_h do |cat,v| + [ + t(cat, scope: 'alternate_name_categories'), + v.map(&:name) + ] + end + end +end diff --git a/app/javascript/controllers/url_controller.ts b/app/javascript/controllers/url_controller.ts index ad55798..e1f972b 100644 --- a/app/javascript/controllers/url_controller.ts +++ b/app/javascript/controllers/url_controller.ts @@ -16,6 +16,6 @@ export default class extends Controller { public async copy (): Promise { await navigator.clipboard.writeText(this.url) - this.copyButtonTarget.classList.add(this.copiedClass, 'animate__animated', 'animate__bounceIn') + this.copyButtonTarget.classList.add(this.copiedClass, 'animate-bounce-in') } } diff --git a/app/views/home/index.rb b/app/views/home/index.rb index 570d58d..2d90213 100644 --- a/app/views/home/index.rb +++ b/app/views/home/index.rb @@ -19,90 +19,25 @@ def view_template def profile_grid section(class: 'grid grid-cols-1 md:grid-cols-2 gap-3') do - url_card + render Home::Card::Url.new div(class: 'flex flex-col gap-3 *:flex-auto') do - name_card - pronouns_card - pronunciation_card + render Home::Card::Name.new + render Home::Card::Pronouns.new + render Home::Card::Pronunciation.new end div(class: 'flex flex-col gap-3 *:flex-auto') do - variants_card - likeness_card - links_card - end - end - end - - def url_card - render Home::Card.new(title: t('.url_card.title'), edit: profile_path(:slug), class: 'md:col-span-2 bg-indigo-50 dark:bg-indigo-950') do - end - end - - def name_card - render Home::Card.new(title: t('.name_card.title'), edit: profile_path(:personal_name), class: 'bg-rose-50 dark:bg-rose-950') do |hc| - hc.content do - hc.blurb { t('profile.personal_name.blurb') } - hc.list(user_attributes_hash %i[personal_name full_name]) - end - end - end - - def pronouns_card - render Home::Card.new(title: t('.pronouns_card.title'), edit: profile_path(:pronouns), class: 'bg-green-50 dark:bg-green-950') do |hc| - hc.content do - hc.blurb { t('profile.pronouns.blurb')&.first } - render PronounSet::Table.new(pronoun_sets: current_user.pronoun_sets, user: current_user) + render Home::Card::Variants.new + render Home::Card::Likeness.new + render Home::Card::Links.new end end end def pronunciation_card - render Home::Card.new(title: t('.pronunciation_card.title'), edit: profile_path(:pronunciation), class: 'bg-rose-50 dark:bg-rose-950') - end - - def variants_card - render Home::Card.new(title: t('.variants_card.title'), edit: profile_path(:variants), class: 'bg-green-50 dark:bg-green-950') do |hc| - hc.content do - hc.blurb { t('profile.variants.blurb') } - attributes = user_attributes_hash %i[envelope_name formal_name email_name] - hc.list(attributes.merge(human_alternate_names)) - end - end - end - - def likeness_card - render Home::Card.new(title: t('.likeness_card.title'), edit: profile_path(:likeness), class: 'bg-indigo-50 dark:bg-indigo-950') do |hc| - hc.content do - hc.blurb { t('profile.likeness.blurb') } - if current_user.likeness.attached? - render Components::User::Likeness.new(user: current_user) - else - hc.not_set - end - end - end + end def links_card - render Home::Card.new(title: t('.links_card.title'), edit: profile_path(:links), class: 'bg-rose-50 dark:bg-rose-950') - end - - # Convert a list of attribute names to a hash of human name to value - def user_attributes_hash(attributes) - attributes.to_h { [ - ::User.human_attribute_name(_1), - current_user.public_send(_1), - ] } - end - - # Convert current_user.alternate_names.grouped_by_category to human-readable form - def human_alternate_names - current_user.alternate_names.grouped_by_category.to_h do |cat,v| - [ - t(cat, scope: 'alternate_name_categories'), - v.map(&:name) - ] - end end end diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index 8020392..d8373d1 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -13,6 +13,29 @@ en: card: not_set: Not set edit: Edit + you_must: You must set this before you can share your page + name: + title: Your name + variants: + title: Variants of your name + pronunciation: + title: How to pronounce your name + name_is_pronounced: '%{name} is pronounced' + ipa: 'IPA pronunciation of %{name}' + audio_clip: Audio clip of '%{name}' + pronouns: + title: Your pronouns + likeness: + title: What you look like + links: + title: Your personal links + url: + title: Your unique URL + view_your_page: View + copy_to_clipboard: Copy + copied: Copied! + you_must_complete: You must complete the highlighted parts before you can preview your page + or_with_pronouns: 'or, if you prefer a URL containing your pronouns:' pronoun_set: table: header: 'Pronouns: %{pronoun}' @@ -236,26 +259,3 @@ en: title: Your name dashboard page_description1: Here you can create a page that describes the different ways you would like to be addressed in English. page_description2: Once you're happy with the page, you can share it with people and put it in your email signature so that everyone knows. - you_must: You must set this before you can share your page - name_card: - title: Your name - variants_card: - title: Variants of your name - pronunciation_card: - title: How to pronounce your name - name_is_pronounced: '%{name} is pronounced' - ipa: 'IPA pronunciation of %{name}' - audio_clip: Audio clip of '%{name}' - pronouns_card: - title: Your pronouns - likeness_card: - title: What you look like - links_card: - title: Your personal links - url_card: - title: Your unique URL - view_your_page: View - copy_to_clipboard: Copy - copied: Copied! - you_must_complete: You must complete the highlighted parts before you can preview your page - or_with_pronouns: 'or, if you prefer a URL containing your pronouns:' From 131e6693537f86fc18b0c3fc3f6e05ba27dbc2cf Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Wed, 13 Aug 2025 12:10:00 +0100 Subject: [PATCH 38/44] Links card and semantic names for these colours --- app/assets/tailwind/application.css | 19 +++++++++++++++++++ app/components/home/card/likeness.rb | 2 +- app/components/home/card/links.rb | 15 ++++++++++++++- app/components/home/card/name.rb | 2 +- app/components/home/card/pronouns.rb | 2 +- app/components/home/card/pronunciation.rb | 6 +++++- app/components/home/card/url.rb | 2 +- app/components/home/card/variants.rb | 2 +- 8 files changed, 43 insertions(+), 7 deletions(-) diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index e1ec62d..91366e1 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -50,6 +50,11 @@ --warning-foreground: hsl(0 0% 100%); --success: hsl(87 100% 37%); --success-foreground: hsl(0 0% 100%); + + /* section accents */ + --section-accent-1: oklch(96.9% 0.015 12.422); + --section-accent-2: oklch(98.2% 0.018 155.826); + --section-accent-3: oklch(96.2% 0.018 272.314); } .dark { @@ -91,6 +96,11 @@ --warning-foreground: hsl(0 0% 100%); --success: hsl(84 81% 44%); --success-foreground: hsl(0 0% 100%); + + /* section accents */ + --section-accent-1: oklch(27.1% 0.105 12.094); + --section-accent-2: oklch(26.6% 0.065 152.934); + --section-accent-3: oklch(25.7% 0.09 281.288); } @theme inline { @@ -137,6 +147,15 @@ --color-success: var(--success); --color-success-foreground: var(--success-foreground); + /* section colors */ + --color-name: var(--section-accent-1); + --color-pronouns: var(--section-accent-2); + --color-pronunciation: var(--section-accent-1); + --color-variants: var(--section-accent-2); + --color-likeness: var(--section-accent-3); + --color-links: var(--section-accent-1); + --color-url: var(--section-accent-3); + /* Taken from animate.css https://github.com/animate-css/animate.css/blob/main/LICENSE */ --animate-bounce-in: bounce-in 0.75s 1; @keyframes bounce-in { diff --git a/app/components/home/card/likeness.rb b/app/components/home/card/likeness.rb index 09e8e73..b575986 100644 --- a/app/components/home/card/likeness.rb +++ b/app/components/home/card/likeness.rb @@ -2,7 +2,7 @@ class Components::Home::Card::Likeness < Components::Base def view_template - render Home::Card.new(title: t('.title'), edit: profile_path(:likeness), class: 'bg-indigo-50 dark:bg-indigo-950') do |hc| + render Home::Card.new(title: t('.title'), edit: profile_path(:likeness), class: 'bg-likeness') do |hc| hc.content do hc.blurb { t('profile.likeness.blurb') } if current_user.likeness.attached? diff --git a/app/components/home/card/links.rb b/app/components/home/card/links.rb index 5dfe1fc..3429272 100644 --- a/app/components/home/card/links.rb +++ b/app/components/home/card/links.rb @@ -2,6 +2,19 @@ class Components::Home::Card::Links < Components::Base def view_template - render Home::Card.new(title: t('.title'), edit: profile_path(:links), class: 'bg-rose-50 dark:bg-rose-950') + render Home::Card.new(title: t('.title'), edit: profile_path(:links), class: 'bg-links') do |hc| + hc.content do + hc.blurb { t('profile.links.blurb') } + if current_user.links.present? + ul(class: 'list-disc pl-5') do + current_user.links.order(:title).each do |link| + li(class: 'mb-4') { Link(class: '-ml-4 py-1 h-auto underline', href: link.url, rel: 'noopener') { link.title } } + end + end + else + p { hc.not_set } + end + end + end end end diff --git a/app/components/home/card/name.rb b/app/components/home/card/name.rb index 175c849..8028346 100644 --- a/app/components/home/card/name.rb +++ b/app/components/home/card/name.rb @@ -2,7 +2,7 @@ class Components::Home::Card::Name < Components::Base def view_template - render Home::Card.new(title: t('.title'), edit: profile_path(:personal_name), class: 'bg-rose-50 dark:bg-rose-950') do |hc| + render Home::Card.new(title: t('.title'), edit: profile_path(:personal_name), class: 'bg-name') do |hc| hc.content do hc.blurb { t('profile.personal_name.blurb') } hc.list(hc.user_attributes_hash(%i[personal_name full_name]), required: true) diff --git a/app/components/home/card/pronouns.rb b/app/components/home/card/pronouns.rb index e4ce702..a372bd4 100644 --- a/app/components/home/card/pronouns.rb +++ b/app/components/home/card/pronouns.rb @@ -2,7 +2,7 @@ class Components::Home::Card::Pronouns < Components::Base def view_template - render Home::Card.new(title: t('.title'), edit: profile_path(:pronouns), class: 'bg-green-50 dark:bg-green-950') do |hc| + render Home::Card.new(title: t('.title'), edit: profile_path(:pronouns), class: 'bg-pronouns') do |hc| hc.content do hc.blurb { t('profile.pronouns.blurb')&.first } if current_user.pronoun_sets.any? diff --git a/app/components/home/card/pronunciation.rb b/app/components/home/card/pronunciation.rb index 39997d6..0cba901 100644 --- a/app/components/home/card/pronunciation.rb +++ b/app/components/home/card/pronunciation.rb @@ -2,6 +2,10 @@ class Components::Home::Card::Pronunciation < Components::Base def view_template - render Home::Card.new(title: t('.title'), edit: profile_path(:pronunciation), class: 'bg-rose-50 dark:bg-rose-950') + render Home::Card.new(title: t('.title'), edit: profile_path(:pronunciation), class: 'bg-pronunciation') do |hc| + hc.content do + hc.blurb { t('profile.pronunciation.blurb') } + end + end end end diff --git a/app/components/home/card/url.rb b/app/components/home/card/url.rb index c9bde5d..ada799e 100644 --- a/app/components/home/card/url.rb +++ b/app/components/home/card/url.rb @@ -2,7 +2,7 @@ class Components::Home::Card::Url < Components::Base def view_template - render Home::Card.new(title: t('.title'), edit: profile_path(:slug), class: 'md:col-span-2 bg-indigo-50 dark:bg-indigo-950', data: {controller: 'url', url_copied_class: 'is-copied'}) do |hc| + render Home::Card.new(title: t('.title'), edit: profile_path(:slug), class: 'md:col-span-2 bg-url', data: {controller: 'url', url_copied_class: 'is-copied'}) do |hc| hc.content do div(class: 'flex flex-col items-center') do p(class: 'text-2xl font-semibold wrap-anywhere', data: {url_target: 'url'}) { user_url(current_user, only_path: false) } diff --git a/app/components/home/card/variants.rb b/app/components/home/card/variants.rb index fb92c8e..616aca6 100644 --- a/app/components/home/card/variants.rb +++ b/app/components/home/card/variants.rb @@ -2,7 +2,7 @@ class Components::Home::Card::Variants < Components::Base def view_template - render Home::Card.new(title: t('.title'), edit: profile_path(:variants), class: 'bg-green-50 dark:bg-green-950') do |hc| + render Home::Card.new(title: t('.title'), edit: profile_path(:variants), class: 'bg-variants') do |hc| hc.content do hc.blurb { t('profile.variants.blurb') } attributes = hc.user_attributes_hash %i[envelope_name formal_name email_name] From 95dc102c25646beb2936414a2009f8343683a208 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Wed, 13 Aug 2025 12:43:49 +0100 Subject: [PATCH 39/44] Build proniunciation box & finish home page --- app/components/home/card.rb | 2 ++ app/components/home/card/links.rb | 2 +- app/components/home/card/pronunciation.rb | 8 ++++++++ app/components/pronunciation/player.rb | 20 ++++++++++++++++++++ config/locales/views.en.yml | 3 +++ 5 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 app/components/pronunciation/player.rb diff --git a/app/components/home/card.rb b/app/components/home/card.rb index 53de32a..9ad24e2 100644 --- a/app/components/home/card.rb +++ b/app/components/home/card.rb @@ -52,6 +52,8 @@ def list_value(v, required: false) ul(class: 'list-disc') do v.each {|name| li { name }} end + when Phlex::HTML + render v else v end diff --git a/app/components/home/card/links.rb b/app/components/home/card/links.rb index 3429272..df852c3 100644 --- a/app/components/home/card/links.rb +++ b/app/components/home/card/links.rb @@ -8,7 +8,7 @@ def view_template if current_user.links.present? ul(class: 'list-disc pl-5') do current_user.links.order(:title).each do |link| - li(class: 'mb-4') { Link(class: '-ml-4 py-1 h-auto underline', href: link.url, rel: 'noopener') { link.title } } + li(class: 'mb-2') { Link(class: '-ml-4 py-1 h-auto underline', href: link.url, rel: 'noopener') { link.title } } end end else diff --git a/app/components/home/card/pronunciation.rb b/app/components/home/card/pronunciation.rb index 0cba901..0c28823 100644 --- a/app/components/home/card/pronunciation.rb +++ b/app/components/home/card/pronunciation.rb @@ -5,6 +5,14 @@ def view_template render Home::Card.new(title: t('.title'), edit: profile_path(:pronunciation), class: 'bg-pronunciation') do |hc| hc.content do hc.blurb { t('profile.pronunciation.blurb') } + hc.list({ + t('.name_is_pronounced', name: current_user.pronunciation_of_name) => + current_user.phonetic, + t('.ipa', name: current_user.pronunciation_of_name) => + current_user.ipa? ? "/#{current_user.ipa}/": nil, + t('.audio_clip', name: current_user.pronunciation_of_name) => + current_user.pronunciation.attached? ? Pronunciation::Player.new(data: {player_url_value: url_for(current_user.pronunciation)}, class: 'mt-1') : nil + }) end end end diff --git a/app/components/pronunciation/player.rb b/app/components/pronunciation/player.rb new file mode 100644 index 0000000..6dfdc62 --- /dev/null +++ b/app/components/pronunciation/player.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class Components::Pronunciation::Player < Components::Base + attr_reader :data, :attributes + + def initialize(data:, **attributes) + @data = data + @attributes = attributes + end + + def view_template + div(data: mix({controller: 'player'}, data)) do + audio(controls: false, data: {player_target: 'media'}) + Button(**mix({data: { player_target: 'play', action: 'player#play' }, type: :primary, disabled: true}, attributes)) do + icon 'play', class: 'h-5 w-5 fill-current -ml-1 mr-1' + span { t('.listen') } + end + end + end +end diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index d8373d1..a6ca79f 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -39,6 +39,9 @@ en: pronoun_set: table: header: 'Pronouns: %{pronoun}' + pronunciation: + player: + listen: Listen user: likeness: expand: Expand From 2f7cee7701c6049038f4d4639e37a8e989bb17d5 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Wed, 13 Aug 2025 14:06:51 +0100 Subject: [PATCH 40/44] Remove the old HAML views --- app/views/home/_likeness_card.html.haml | 15 -------- app/views/home/_links_card.html.haml | 15 -------- app/views/home/_name_card.html.haml | 22 ------------ app/views/home/_name_variants_card.html.haml | 22 ------------ app/views/home/_pronouns_card.html.haml | 14 -------- app/views/home/_pronunciation_card.html.haml | 20 ----------- app/views/home/_url_card.html.haml | 38 -------------------- app/views/home/user_home.html.haml | 23 ------------ config/locales/views.en.yml | 29 --------------- 9 files changed, 198 deletions(-) delete mode 100644 app/views/home/_likeness_card.html.haml delete mode 100644 app/views/home/_links_card.html.haml delete mode 100644 app/views/home/_name_card.html.haml delete mode 100644 app/views/home/_name_variants_card.html.haml delete mode 100644 app/views/home/_pronouns_card.html.haml delete mode 100644 app/views/home/_pronunciation_card.html.haml delete mode 100644 app/views/home/_url_card.html.haml delete mode 100644 app/views/home/user_home.html.haml diff --git a/app/views/home/_likeness_card.html.haml b/app/views/home/_likeness_card.html.haml deleted file mode 100644 index b978d32..0000000 --- a/app/views/home/_likeness_card.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -.cell.card.has-background-likeness-light#likeness - .card-header - .card-header-title= t('.title') - .card-header-icon - = link_to profile_path(:likeness), class: 'icon', title: t('actions.edit') do - = icon 'pencil' - .card-content - %p.mb-4.has-text-grey= t('profile.likeness.blurb') - .content - - if current_user.likeness.attached? - .likeness - = link_to url_for(current_user.likeness), data: {controller: 'lightbox'} do - = image_tag current_user.likeness.variant(resize_to_limit: [300, 300]) - - else - = not_set diff --git a/app/views/home/_links_card.html.haml b/app/views/home/_links_card.html.haml deleted file mode 100644 index 40e9a37..0000000 --- a/app/views/home/_links_card.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -.cell.card.has-background-links-light#links - .card-header - .card-header-title= t('.title') - .card-header-icon - = link_to profile_path(:links), class: 'icon', title: t('actions.edit') do - = icon 'pencil' - .card-content - %p.mb-4.has-text-grey= t('profile.links.blurb') - .content - - if current_user.links.present? - %ul - - current_user.links.order(:title).each do |link| - %li= link_to link.title, link.url, rel: 'noopener' - - else - %p= not_set diff --git a/app/views/home/_name_card.html.haml b/app/views/home/_name_card.html.haml deleted file mode 100644 index 036f8d1..0000000 --- a/app/views/home/_name_card.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -.cell.card.has-background-personal-name-light#personal-name - .card-header - .card-header-title= t('.title') - .card-header-icon - = link_to profile_path(:personal_name), class: 'icon', title: t('actions.edit') do - = icon 'pencil' - .card-content - %p.mb-4.has-text-grey= t('profile.personal_name.blurb') - .content - %dl - %dt= User.human_attribute_name(:personal_name) - %dd - - if current_user.personal_name? - = current_user.personal_name - - else - = you_must - %dt= User.human_attribute_name(:full_name) - %dd - - if current_user.full_name? - = current_user.full_name - - else - = you_must diff --git a/app/views/home/_name_variants_card.html.haml b/app/views/home/_name_variants_card.html.haml deleted file mode 100644 index 84396e9..0000000 --- a/app/views/home/_name_variants_card.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -.cell.card.has-background-variants-light#variants - .card-header - .card-header-title= t('.title') - .card-header-icon - = link_to profile_path(:variants), class: 'icon', title: t('actions.edit') do - = icon 'pencil' - .card-content - %p.mb-4.has-text-grey= t('profile.variants.blurb') - .content - %dl - %dt= User.human_attribute_name(:envelope_name) - %dd= current_user.envelope_name? ? current_user.envelope_name : not_set - %dt= User.human_attribute_name(:formal_name) - %dd= current_user.formal_name? ? current_user.formal_name : not_set - %dt= User.human_attribute_name(:email_name) - %dd= current_user.email_name? ? current_user.email_name : not_set - - current_user.alternate_names.grouped_by_category.each do |category, ans| - %dt= t(category, scope: 'alternate_name_categories') - %dd - %ul.mt-0.ml-0 - - ans.each do |an| - %li= an.name diff --git a/app/views/home/_pronouns_card.html.haml b/app/views/home/_pronouns_card.html.haml deleted file mode 100644 index cbd7a0e..0000000 --- a/app/views/home/_pronouns_card.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -.cell.card.has-background-pronouns-light#pronouns - .card-header - .card-header-title= t('.title') - .card-header-icon - = link_to profile_path(:pronouns), class: 'icon', title: t('actions.edit') do - = icon 'pencil' - .card-content - %p.mb-4.has-text-grey= t('profile.pronouns.blurb')&.first - - if current_user.pronounless_style? - %p.has-text-weight-bold= t(current_user.pronounless_style, scope: 'pronounless_styles') - - elsif current_user.pronoun_sets.any? - = render 'pronoun_sets/table', pronoun_sets: current_user.pronoun_sets, user: current_user - - else - %p= you_must diff --git a/app/views/home/_pronunciation_card.html.haml b/app/views/home/_pronunciation_card.html.haml deleted file mode 100644 index 4a1f46b..0000000 --- a/app/views/home/_pronunciation_card.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -.cell.card.has-background-pronunciation-light#pronunciation - .card-header - .card-header-title= t('.title') - .card-header-icon - = link_to profile_path(:pronunciation), class: 'icon', title: t('actions.edit') do - = icon 'pencil' - .card-content - %p.mb-4.has-text-grey= t('profile.pronunciation.blurb') - .content - %dl - %dt= t('.name_is_pronounced', name: current_user.pronunciation_of_name) - %dd= current_user.phonetic? ? current_user.phonetic : not_set - %dt= t('.ipa', name: current_user.pronunciation_of_name) - %dd.is-ipa= current_user.ipa? ? "/#{current_user.ipa}/" : not_set - %dt= t('.audio_clip', name: current_user.pronunciation_of_name) - %dd - - if current_user.pronunciation.attached? - = render 'users/player', data: {player_url_value: url_for(current_user.pronunciation)} - - else - = not_set diff --git a/app/views/home/_url_card.html.haml b/app/views/home/_url_card.html.haml deleted file mode 100644 index 7aabb28..0000000 --- a/app/views/home/_url_card.html.haml +++ /dev/null @@ -1,38 +0,0 @@ -.cell.card.has-background-url-light#url{data: {controller: 'url', url_copied_class: 'copy-button--copied'}} - .card-header - .card-header-title= t('.title') - .card-header-icon - = link_to profile_path(:slug), class: 'icon', title: t('actions.edit') do - = icon 'pencil' - .card-content - %p.is-size-4.has-text-centered.is-force-wrapped - - if current_user.slug.present? - %b{data: {url_target: 'url'}}= user_url(current_user, only_path: false) - - else - = you_must - - if current_user.pronoun_sets.any? - .mt-4.is-size-6.has-text-grey.has-text-centered - %p - %i= t('.or_with_pronouns') - - current_user.pronoun_sets.each do |ps| - %p.is-force-wrapped= user_url(current_user, only_path: false, nominative: ps.nominative, oblique: ps.oblique) - .card-footer - = link_to profile_path(:slug), class: 'button card-footer-item has-text-link' do - %span.icon= icon 'pencil' - %span= t('actions.edit') - - if current_user.profile_complete? - = link_to user_url(current_user), class: 'button card-footer-item', target: '_blank' do - %span.icon= icon 'open-in-new' - %span= t('.view_your_page') - %button.button.card-footer-item.copy-button#copyButton{data: {url_target: 'copyButton', action: 'url#copy'}} - %span.icon.copy-button__initial= icon 'clipboard-outline' - %span.copy-button__initial= t('.copy_to_clipboard') - %span.icon.copy-button__after= icon 'clipboard-check-outline' - %span.copy-button__after= t('.copied') - - else - %button.button.card-footer-item{disabled: true, title: t('.you_must_complete')} - %span.icon= icon 'open-in-new' - %span= t('.view_your_page') - %button.button.card-footer-item{disabled: true, title: t('.you_must_complete')} - %span.icon= icon 'clipboard-outline' - %span= t('.copy_to_clipboard') diff --git a/app/views/home/user_home.html.haml b/app/views/home/user_home.html.haml deleted file mode 100644 index 1e0b725..0000000 --- a/app/views/home/user_home.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -%section.section - .container - %h1.title= title t('.title') - .content.is-size-5.has-text-grey - %p= t('.page_description1') - %p= t('.page_description2') -%section.section - .container - .fixed-grid.has-1-cols.has-2-cols-tablet - .grid - .cell.is-col-span-2-tablet= render 'url_card' - .cell.is-fullheight - .fixed-grid.has-1-cols.is-fullheight - .grid.is-fullheight - = render 'name_card' - = render 'pronouns_card' - = render 'pronunciation_card' - .cell.is-fullheight - .fixed-grid.has-1-cols.is-fullheight - .grid.is-fullheight - = render 'name_variants_card' - = render 'likeness_card' - = render 'links_card' diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index a6ca79f..b6a66a2 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -68,35 +68,6 @@ en: blurb: Argh! Something happened on the server that shouldn't have happened. If you have time, it would be really helpful to us to hear a description of what you were doing when this happened, so we can fix it! support: report_a_bug: Report a bug on our issues board - home: - user_home: - title: Your name dashboard - page_description1: Here you can create a page that describes the different ways you would like to be addressed in English. - page_description2: Once you're happy with the page, you can share it with people and put it in your email signature so that everyone knows. - you_must: You must set this before you can share your page - not_set: Not set - name_card: - title: Your name - name_variants_card: - title: Variants of your name - pronunciation_card: - title: How to pronounce your name - name_is_pronounced: '%{name} is pronounced' - ipa: 'IPA pronunciation of %{name}' - audio_clip: Audio clip of '%{name}' - pronouns_card: - title: Your pronouns - likeness_card: - title: What you look like - links_card: - title: Your personal links - url_card: - title: Your unique URL - view_your_page: View - copy_to_clipboard: Copy - copied: Copied! - you_must_complete: You must complete the highlighted parts before you can preview your page - or_with_pronouns: 'or, if you prefer a URL containing your pronouns:' mailing_list: show: title: Mailing list preference From 9963345530f741724123d35462bedd30f1f9701e Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 14 Aug 2025 11:45:16 +0100 Subject: [PATCH 41/44] Add page footer --- app/components/page_footer.rb | 42 ++++++++++++++++++++++++++++++++++ app/components/shell_layout.rb | 22 ++++++++++++++++++ config/locales/views.en.yml | 5 ++++ 3 files changed, 69 insertions(+) create mode 100644 app/components/page_footer.rb diff --git a/app/components/page_footer.rb b/app/components/page_footer.rb new file mode 100644 index 0000000..8e60575 --- /dev/null +++ b/app/components/page_footer.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class Components::PageFooter < Components::Base + # From https://github.com/ruby-ui/web/blob/main/app/components/shared/navbar.rb + + def initialize + @left_links = [] + @right_links = [] + end + + def view_template(&) + vanish(&) + header(class: 'supports-backdrop-blur:bg-background/80 sticky top-0 z-50 w-full border-b bg-background/80 backdrop-blur-2xl backdrop-saturate-200') do + div(class: 'px-2 sm:px-4 sm:container flex flex-col sm:flex-row sm:h-14 items-center justify-between') do + div(class: 'mr-4 flex flex-wrap items-center') do + @left_links.each { render_link(it) } + end + div(class: 'flex flex-wrap items-center') do + @right_links.each { render_link(it) } + end + end + end + end + + def left_link(href, **args, &content) + @left_links << {href:, content:, **args} + end + + def right_link(href, **args, &content) + @right_links << {href:, content:, **args} + end + + protected + + def link_class + nil + end + + def render_link(link) + Link(**mix(link.except(:content), {variant: :ghost, class: link_class}), &link[:content]) + end +end diff --git a/app/components/shell_layout.rb b/app/components/shell_layout.rb index 4123bb1..98c9c3b 100644 --- a/app/components/shell_layout.rb +++ b/app/components/shell_layout.rb @@ -26,6 +26,28 @@ def view_template(&) Flashes(class: @flashes_class) yield end + PageFooter do |footer| + footer.left_link(page_path('terms')) do + icon 'file-sign', class: 'h-4 w-4 fill-current mr-1' + span { t('.terms') } + end + footer.left_link(page_path('privacy')) do + icon 'shield-account', class: 'h-4 w-4 fill-current mr-1' + span { t('.privacy') } + end + footer.right_link('https://github.com/fishpercolator/name.pn') do + icon 'github', class: 'h-4 w-4 fill-current mr-1' + span { t('.github') } + end + footer.right_link('https://github.com/orgs/fishpercolator/projects/1') do + icon 'map', class: 'h-4 w-4 fill-current mr-1' + span { t('.roadmap') } + end + footer.right_link(rswag_ui_path) do + icon 'cogs', class: 'h-4 w-4 fill-current mr-1' + span { t('.api') } + end + end end end diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index b6a66a2..8a7e056 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -3,6 +3,11 @@ en: shell_layout: about: About admin: Admin + terms: Terms of use + privacy: Privacy policy + github: GitHub + roadmap: Public roadmap + api: API home: logo: namedot: name. From fcd3913175eb55d80d0e1ebdc91b885a78203583 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 14 Aug 2025 12:00:54 +0100 Subject: [PATCH 42/44] Some style changes for the main layout --- app/components/flashes.rb | 8 ++++---- app/components/page_footer.rb | 2 +- app/components/page_header.rb | 4 ++-- app/components/shell_layout.rb | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/components/flashes.rb b/app/components/flashes.rb index eadb2d1..a3f2f7c 100644 --- a/app/components/flashes.rb +++ b/app/components/flashes.rb @@ -3,15 +3,15 @@ class Components::Flashes < Components::Base include Phlex::Rails::Helpers::Flash - def initialize(class: nil) - @class = grab(class:) + def initialize(**attributes) + @attributes = attributes end def view_template(&) if flash[:notice] || flash[:alert] - div(class: @class) do + div(**@attributes) do if flash[:notice] - Alert do + Alert(variant: :success) do AlertTitle { flash[:notice] } end end diff --git a/app/components/page_footer.rb b/app/components/page_footer.rb index 8e60575..5a6ed9d 100644 --- a/app/components/page_footer.rb +++ b/app/components/page_footer.rb @@ -10,7 +10,7 @@ def initialize def view_template(&) vanish(&) - header(class: 'supports-backdrop-blur:bg-background/80 sticky top-0 z-50 w-full border-b bg-background/80 backdrop-blur-2xl backdrop-saturate-200') do + header(class: 'top-0 z-50 w-full border-b bg-background') do div(class: 'px-2 sm:px-4 sm:container flex flex-col sm:flex-row sm:h-14 items-center justify-between') do div(class: 'mr-4 flex flex-wrap items-center') do @left_links.each { render_link(it) } diff --git a/app/components/page_header.rb b/app/components/page_header.rb index 0178a20..9cec544 100644 --- a/app/components/page_header.rb +++ b/app/components/page_header.rb @@ -10,7 +10,7 @@ def initialize def view_template(&) vanish(&) - header(class: 'supports-backdrop-blur:bg-background/80 sticky top-0 z-50 w-full border-b bg-background/80 backdrop-blur-2xl backdrop-saturate-200') do + header(class: 'sticky top-0 z-50 w-full bg-linear-to-b from-slate-900 to-slate-800 dark:from-slate-800 dark:to-slate-900 text-white') do div(class: 'px-2 sm:px-4 sm:container flex h-14 items-center justify-between') do div(class: 'mr-4 flex items-center') do render_logo @@ -45,7 +45,7 @@ def render_logo end def render_link(link) - Link(**mix(link.except(:content), {variant: :ghost, class: 'hidden md:inline-block'}), &link[:content]) + Link(**mix(link.except(:content), {variant: :ghost, size: :lg, class: 'hidden md:inline-flex'}), &link[:content]) end def render_dropdown_menu diff --git a/app/components/shell_layout.rb b/app/components/shell_layout.rb index 98c9c3b..11323a7 100644 --- a/app/components/shell_layout.rb +++ b/app/components/shell_layout.rb @@ -4,7 +4,7 @@ class Components::ShellLayout < Components::Layout register_value_helper :user_signed_in? register_value_helper :current_user - def initialize(main_class: 'flex flex-col grow px-2 sm:px-4 sm:container py-4 sm:py-8', flashes_class: 'sm:px-4 sm:container py-4 sm:py-8') + def initialize(main_class: 'flex flex-col grow px-2 sm:px-4 sm:container py-4 sm:py-8', flashes_class: '') @main_class = main_class @flashes_class = flashes_class end From c2420700ccabfa132957e665fc5be547afdfd797 Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 21 Aug 2025 16:30:55 +0100 Subject: [PATCH 43/44] Getting there with the stepper --- app/components/page_footer.rb | 2 +- app/components/profile/progress_bar.rb | 40 +++++++++++++++++++++++ app/controllers/application_controller.rb | 5 +++ app/controllers/profile_controller.rb | 10 ++++-- app/views/base.rb | 1 + app/views/profile/personal_name.rb | 15 +++++++++ 6 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 app/components/profile/progress_bar.rb create mode 100644 app/views/profile/personal_name.rb diff --git a/app/components/page_footer.rb b/app/components/page_footer.rb index 5a6ed9d..79a66cb 100644 --- a/app/components/page_footer.rb +++ b/app/components/page_footer.rb @@ -10,7 +10,7 @@ def initialize def view_template(&) vanish(&) - header(class: 'top-0 z-50 w-full border-b bg-background') do + footer(class: 'top-0 z-50 w-full border-b bg-background') do div(class: 'px-2 sm:px-4 sm:container flex flex-col sm:flex-row sm:h-14 items-center justify-between') do div(class: 'mr-4 flex flex-wrap items-center') do @left_links.each { render_link(it) } diff --git a/app/components/profile/progress_bar.rb b/app/components/profile/progress_bar.rb new file mode 100644 index 0000000..452dcf9 --- /dev/null +++ b/app/components/profile/progress_bar.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class Components::Profile::ProgressBar < Components::Base + attr_reader :user, :steps, :current_step + + def initialize(user:, steps:, current_step:) + @user = user + @steps = steps + @current_step = current_step + end + + def view_template + ol(class: 'w-full flex flex-col sm:flex-row') do + steps.each { step _1 } + end + end + + # Thanks to https://ishadeed.com/article/stepper-component-html-css/ for the inspiration + def step(name) + li(**mix({class: 'flex-1 flex sm:flex-col items-center before:h-6 before:w-6 before:relative before:z-1 before:rounded-full before:m-2 after:w-1 after:h-8 sm:after:w-full sm:after:h-1 after:-order-1 after:relative after:top-4 sm:after:top-[22px] after:left-[22px] sm:after:left-[50%] last:after:invisible'}, {class: step_class(name)})) do + Link(href: helpers.wizard_path(name)) { t("profile.#{name}.title") } + end + end + + # If the step is in the future, return a different colour + def step_class(name) + if current_step == name + 'before:bg-primary after:bg-slate-300 after:dark:bg-slate-700' + elsif future_steps.include? name + 'before:bg-slate-300 before:dark:bg-slate-700 after:bg-slate-300 after:dark:bg-slate-700' + else + 'before:bg-primary after:bg-primary' + end + end + + # Get only the steps after current_step + def future_steps + @future_steps ||= steps.slice(steps.index(current_step)+1..-1) + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ad550d6..75a6fbc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -32,4 +32,9 @@ def append_info_to_payload(payload) super payload[:user_id] = current_user&.id end + + # Override this from Phlexible::...::ImplicitRender to not include the 'view' + def phlex_view_path(action_name) + "views/#{controller_path}/#{action_name}" + end end diff --git a/app/controllers/profile_controller.rb b/app/controllers/profile_controller.rb index 8e2542c..0a55425 100644 --- a/app/controllers/profile_controller.rb +++ b/app/controllers/profile_controller.rb @@ -1,7 +1,5 @@ class ProfileController < ApplicationController include Wicked::Wizard - - layout 'application' before_action :authenticate_user! before_action :set_user @@ -56,6 +54,13 @@ def stuck_on_first_step? (@user.full_name.blank? || @user.personal_name.blank?) end helper_method :stuck_on_first_step? + + # Monkeypatch the wizard_value method. phlex_view is actually a private method + # but it works well for converting action names to views - at least for now + def wizard_value(step_name) + orig = super(step_name) + phlex_view(orig) || orig + end private @@ -65,6 +70,7 @@ def set_user # Get the set of pronoun_sets the user hasn't selected and render them as user_pronoun_sets @additional_ups = PronounSet.where.not(id: @user.pronoun_sets).map {|ps| UserPronounSet.new(pronoun_set: ps, user: @user)} + end def pronunciation diff --git a/app/views/base.rb b/app/views/base.rb index eb9b612..c4c1571 100644 --- a/app/views/base.rb +++ b/app/views/base.rb @@ -6,6 +6,7 @@ class Views::Base < Components::Base # By default, it inherits from `Components::Base`, but you # can change that to `Phlex::HTML` if you want to keep views and # components independent. + include Phlexible::Rails::ControllerVariables # Safe version of the equivalent helper in MetaTags def title(title = nil) diff --git a/app/views/profile/personal_name.rb b/app/views/profile/personal_name.rb new file mode 100644 index 0000000..79705f8 --- /dev/null +++ b/app/views/profile/personal_name.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class Views::Profile::PersonalName < Views::Base + include Phlex::Rails::Helpers::FormFor + + controller_variable :user + + def view_template + ShellLayout do + render Profile::ProgressBar.new(user: @user, steps: helpers.wizard_steps, current_step: helpers.step) + form_for(@user, url: helpers.wizard_path, method: :patch) do |f| + end + end + end +end From 299f940725d2960e69671041c96f1de49364df1b Mon Sep 17 00:00:00 2001 From: Quinn Daley Date: Thu, 21 Aug 2025 16:37:50 +0100 Subject: [PATCH 44/44] Add the link (still needs to be disabled for incomplete profiles) --- app/components/profile/progress_bar.rb | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/app/components/profile/progress_bar.rb b/app/components/profile/progress_bar.rb index 452dcf9..f6d60b6 100644 --- a/app/components/profile/progress_bar.rb +++ b/app/components/profile/progress_bar.rb @@ -17,19 +17,24 @@ def view_template # Thanks to https://ishadeed.com/article/stepper-component-html-css/ for the inspiration def step(name) - li(**mix({class: 'flex-1 flex sm:flex-col items-center before:h-6 before:w-6 before:relative before:z-1 before:rounded-full before:m-2 after:w-1 after:h-8 sm:after:w-full sm:after:h-1 after:-order-1 after:relative after:top-4 sm:after:top-[22px] after:left-[22px] sm:after:left-[50%] last:after:invisible'}, {class: step_class(name)})) do - Link(href: helpers.wizard_path(name)) { t("profile.#{name}.title") } + li(**mix({class: 'flex-1 flex sm:flex-col items-center after:w-1 after:h-8 sm:after:w-full sm:after:h-1 after:-order-1 after:relative after:top-4 sm:after:top-[22px] after:left-[22px] sm:after:left-[50%] last:after:invisible'}, {class: connector_class(name)})) do + Link(**mix({class: 'flex sm:flex-col items-center flex-1 p-0 before:h-6 before:w-6 before:relative before:z-1 before:rounded-full before:m-2'}, {class: pip_class(name)}), href: helpers.wizard_path(name)) { t("profile.#{name}.title") } end end # If the step is in the future, return a different colour - def step_class(name) - if current_step == name - 'before:bg-primary after:bg-slate-300 after:dark:bg-slate-700' - elsif future_steps.include? name - 'before:bg-slate-300 before:dark:bg-slate-700 after:bg-slate-300 after:dark:bg-slate-700' + def connector_class(name) + if current_step == name || future_steps.include?(name) + 'after:bg-slate-300 after:dark:bg-slate-700' else - 'before:bg-primary after:bg-primary' + 'after:bg-primary' + end + end + def pip_class(name) + if future_steps.include?(name) + 'before:bg-slate-300 before:dark:bg-slate-700' + else + 'before:bg-primary' end end