diff --git a/Gemfile b/Gemfile index 0e2276e..4afc0be 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' @@ -49,15 +50,22 @@ 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 +gem 'phlex-rails', '~> 2.2' +gem 'phlexible', '~> 3.0' 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 'stimulus-rails', '~> 1.3' +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 +94,7 @@ group :development do gem 'better_errors' gem 'foreman' gem 'haml-rails' + gem 'ruby_ui', require: false end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 5089b64..8b77729 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -166,6 +166,14 @@ GEM cocoon (1.2.15) coderay (1.1.3) colorize (1.1.0) + commonmarker (2.3.0) + rb_sys (~> 0.9) + commonmarker (2.3.0-aarch64-linux) + commonmarker (2.3.0-aarch64-linux-musl) + commonmarker (2.3.0-arm64-darwin) + commonmarker (2.3.0-x86_64-darwin) + commonmarker (2.3.0-x86_64-linux) + commonmarker (2.3.0-x86_64-linux-musl) concurrent-ruby (1.3.5) connection_pool (2.5.3) crack (1.0.0) @@ -238,6 +246,16 @@ GEM webrick (~> 1.7) websocket-driver (~> 0.7) ffi (1.17.2) + ffi (1.17.2-aarch64-linux-gnu) + ffi (1.17.2-aarch64-linux-musl) + ffi (1.17.2-arm-linux-gnu) + ffi (1.17.2-arm-linux-musl) + ffi (1.17.2-arm64-darwin) + ffi (1.17.2-x86-linux-gnu) + ffi (1.17.2-x86-linux-musl) + ffi (1.17.2-x86_64-darwin) + ffi (1.17.2-x86_64-linux-gnu) + ffi (1.17.2-x86_64-linux-musl) figaro (1.3.0) thor (>= 0.14.0, < 2) foreman (0.88.1) @@ -318,11 +336,23 @@ GEM childprocess (~> 5.0) logger (~> 1.6) libdatadog (18.1.0.1.0) + libdatadog (18.1.0.1.0-aarch64-linux) + libdatadog (18.1.0.1.0-x86_64-linux) libddwaf (1.24.1.0.3) ffi (~> 1.0) + libddwaf (1.24.1.0.3-aarch64-linux) + ffi (~> 1.0) + libddwaf (1.24.1.0.3-arm64-darwin) + ffi (~> 1.0) + libddwaf (1.24.1.0.3-x86_64-darwin) + ffi (~> 1.0) + libddwaf (1.24.1.0.3-x86_64-linux) + ffi (~> 1.0) listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) + literal (1.8.1) + zeitwerk logger (1.7.0) loofah (2.24.1) crass (~> 1.0.2) @@ -359,9 +389,36 @@ GEM nokogiri (1.18.9) mini_portile2 (~> 2.8.2) racc (~> 1.4) + nokogiri (1.18.9-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.9-aarch64-linux-musl) + racc (~> 1.4) + nokogiri (1.18.9-arm-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.9-arm-linux-musl) + racc (~> 1.4) + nokogiri (1.18.9-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.9-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.18.9-x86_64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.9-x86_64-linux-musl) + racc (~> 1.4) orm_adapter (0.5.0) ostruct (0.6.3) pg (1.5.9) + phlex (2.3.1) + zeitwerk (~> 2.7) + phlex-rails (2.3.1) + 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) @@ -434,6 +491,7 @@ GEM thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) rake (13.3.0) + rake-compiler-dock (1.9.1) ransack (4.3.0) activerecord (>= 6.1.5) activesupport (>= 6.1.5) @@ -441,6 +499,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.14.2) erb psych (>= 4.0.0) @@ -489,12 +549,14 @@ GEM ruby-vips (2.2.4) 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) @@ -512,7 +574,21 @@ 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.3.1) + sin_lru_redux (~> 2.5) + tailwindcss-rails (4.2.3) + railties (>= 7.0.0) + tailwindcss-ruby (~> 4.0) + tailwindcss-ruby (4.1.10) + tailwindcss-ruby (4.1.10-aarch64-linux-gnu) + tailwindcss-ruby (4.1.10-aarch64-linux-musl) + tailwindcss-ruby (4.1.10-arm64-darwin) + tailwindcss-ruby (4.1.10-x86_64-darwin) + tailwindcss-ruby (4.1.10-x86_64-linux-gnu) + tailwindcss-ruby (4.1.10-x86_64-linux-musl) temple (0.10.3) thor (1.4.0) tilt (2.6.1) @@ -553,7 +629,19 @@ GEM zeitwerk (2.7.3) PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-gnu + arm-linux-musl + arm64-darwin ruby + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl DEPENDENCIES active_storage_base64 (~> 3.0) @@ -573,6 +661,7 @@ DEPENDENCIES byebug capybara-screenshot cocoon (~> 1.2) + commonmarker (~> 2.3) cssbundling-rails (~> 1.1) cuprite database_cleaner @@ -595,10 +684,13 @@ 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) pg (>= 0.18, < 2.0) + phlex-rails (~> 2.2) + phlexible (~> 3.0) pry-rails pry-rescue puma (~> 6.0) @@ -613,11 +705,16 @@ 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) + stimulus-rails (~> 1.3) + tailwind_merge (~> 1.2) + tailwindcss-rails (~> 4.2) + tailwindcss-ruby (~> 4.1) timecop turbo-rails (~> 2.0) web-console (>= 3.3.0) diff --git a/Tiltfile b/Tiltfile index 15d27db..474054d 100644 --- a/Tiltfile +++ b/Tiltfile @@ -25,11 +25,16 @@ 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'], 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']), @@ -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/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 new file mode 100644 index 0000000..91366e1 --- /dev/null +++ b/app/assets/tailwind/application.css @@ -0,0 +1,226 @@ +@import "tailwindcss"; + +@plugin "@tailwindcss/forms"; +@plugin "@tailwindcss/typography"; + +@import "tw-animate-css"; +@import "luminous-lightbox/dist/luminous-basic"; + +@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); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --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.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); + --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%); + + /* 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 { + --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.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.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); + --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%); + + /* 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 { + --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); + + /* 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 { + 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 */ +@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; + } + 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/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/components/base.rb b/app/components/base.rb new file mode 100644 index 0000000..f0287f6 --- /dev/null +++ b/app/components/base.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class Components::Base < Phlex::HTML + extend Literal::Properties + include Components + 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 + + 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}" } + super + end + end + + def container_classes + %w[mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8] + 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..a4ae57d --- /dev/null +++ b/app/components/centered_form.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class Components::CenteredForm < Components::Base + def view_template(&) + 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 + + def title(&) + h1(class: 'text-3xl font-bold mx-3 sm:mx-0', &) + end + + def section(&) + Box(class: 'mx-3 sm:mx-0 my-6 space-y-4', &) + end + + def footer(&) + div(class: 'mx-3 sm:mx-0 my-6 space-y-4', &) + end +end diff --git a/app/components/flashes.rb b/app/components/flashes.rb new file mode 100644 index 0000000..a3f2f7c --- /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(**attributes) + @attributes = attributes + end + + def view_template(&) + if flash[:notice] || flash[:alert] + div(**@attributes) do + if flash[:notice] + Alert(variant: :success) 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/home/card.rb b/app/components/home/card.rb new file mode 100644 index 0000000..9ad24e2 --- /dev/null +++ b/app/components/home/card.rb @@ -0,0 +1,79 @@ +# 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(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 + end + end + + def content(&) + CardContent(&) + end + + def footer(&) + 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, &) + Text(**mix({class: 'mb-4 text-muted-foreground'}, attributes), &) + end + + # Provide a list of key/value pairs to render as a dl. + 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, required: } + end + end + end + + # If value is a list, it will be rendered as a ul + def list_value(v, required: false) + case v + when nil, '' + required ? you_must : not_set + when Array + ul(class: 'list-disc') do + v.each {|name| li { name }} + end + when Phlex::HTML + render v + else + v + end + end + + 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..b575986 --- /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-likeness') 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..df852c3 --- /dev/null +++ b/app/components/home/card/links.rb @@ -0,0 +1,20 @@ +# 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-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-2') { 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 new file mode 100644 index 0000000..8028346 --- /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-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) + 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..a372bd4 --- /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-pronouns') 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..0c28823 --- /dev/null +++ b/app/components/home/card/pronunciation.rb @@ -0,0 +1,19 @@ +# 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-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 +end diff --git a/app/components/home/card/url.rb b/app/components/home/card/url.rb new file mode 100644 index 0000000..ada799e --- /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-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) } + 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..616aca6 --- /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-variants') 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/components/home/logo.rb b/app/components/home/logo.rb new file mode 100644 index 0000000..c717b08 --- /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-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-accent') { t('.subtitle') } + end + end +end diff --git a/app/components/layout.rb b/app/components/layout.rb new file mode 100644 index 0000000..85ce45f --- /dev/null +++ b/app/components/layout.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +class Components::Layout < Components::Base + include Phlex::Rails::Layout + + register_output_helper :display_meta_tags + + def view_template + doctype + html(lang: 'en', class: html_class) do + render_head + body(class: body_class) do + yield + end + end + end + + protected + + def html_class + 'h-full' + end + + def body_class + 'bg-background text-foreground' + end + + def render_head + head do + 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' + 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'), + 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/components/page_footer.rb b/app/components/page_footer.rb new file mode 100644 index 0000000..79a66cb --- /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(&) + 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) } + 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/page_header.rb b/app/components/page_header.rb new file mode 100644 index 0000000..9cec544 --- /dev/null +++ b/app/components/page_header.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +class Components::PageHeader < Components::Base + # From https://github.com/ruby-ui/web/blob/main/app/components/shared/navbar.rb + + def initialize + @main_links = [] + @action_links = [] + end + + def view_template(&) + vanish(&) + 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 + @main_links.each { render_link(it) } + end + div(class: 'flex items-center gap-x-2 divide-x md:divide-none') do + div(class: 'flex items-center') do + @action_links.each { render_link(it) } + render_theme_toggle + end + render_dropdown_menu + end + end + end + end + + def main_link(href, **args, &content) + @main_links << {href:, content:, **args} + end + + 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, size: :lg, class: 'hidden md:inline-flex'}), &link[:content]) + end + + 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 do |toggle| + SetLightMode(class: 'flex items-center') do + 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) { icon('weather-night', class: 'fill-current p-2') } + 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/profile/progress_bar.rb b/app/components/profile/progress_bar.rb new file mode 100644 index 0000000..f6d60b6 --- /dev/null +++ b/app/components/profile/progress_bar.rb @@ -0,0 +1,45 @@ +# 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 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 connector_class(name) + if current_step == name || future_steps.include?(name) + 'after:bg-slate-300 after:dark:bg-slate-700' + else + '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 + + # 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/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/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/app/components/prose.rb b/app/components/prose.rb new file mode 100644 index 0000000..eb86c07 --- /dev/null +++ b/app/components/prose.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Components::Prose < Components::Base + CM_OPTIONS = { + render: { + unsafe: true, + }, + extension: { + description_lists: true, + header_ids: "", + table: true, + } + } + + def initialize(md) + @md = md + end + + def view_template + rendered = Commonmarker.to_html(@md, options: CM_OPTIONS) + div(class: 'prose dark:prose-invert prose-headings:font-bold') do + raw safe(rendered) + 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/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/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/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/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/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/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 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/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/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/components/shell_layout.rb b/app/components/shell_layout.rb new file mode 100644 index 0000000..11323a7 --- /dev/null +++ b/app/components/shell_layout.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +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: '') + @main_class = main_class + @flashes_class = flashes_class + end + + def view_template(&) + super do + 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(class: @main_class) do + 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 + + def body_class + '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 + 'min-h-screen scroll-pt-15' + 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/controllers/application_controller.rb b/app/controllers/application_controller.rb index 31664c0..75a6fbc 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 false + def authenticate_admin! if !current_user&.role_admin? sign_out @@ -16,7 +20,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 @@ -28,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/home_controller.rb b/app/controllers/home_controller.rb index fd74da9..6567e70 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -7,9 +7,9 @@ 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 Views::Home::Index else - render 'anon_home' + render Views::Home::Anon end end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb new file mode 100644 index 0000000..2e5867c --- /dev/null +++ b/app/controllers/pages_controller.rb @@ -0,0 +1,21 @@ +class PagesController < ApplicationController + skip_after_action :verify_authorized + + layout -> { Components::ShellLayout } + + # 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 PAGES.key? params[:path] + render PAGES[params[:path]] + else + raise AbstractController::ActionNotFound + end + end +end diff --git a/app/controllers/profile_controller.rb b/app/controllers/profile_controller.rb index a18062f..0a55425 100644 --- a/app/controllers/profile_controller.rb +++ b/app/controllers/profile_controller.rb @@ -54,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 @@ -63,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/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/javascript/controllers/index.ts b/app/javascript/controllers/index.js similarity index 72% rename from app/javascript/controllers/index.ts rename to app/javascript/controllers/index.js index 198f6de..e3b3d79 100644 --- a/app/javascript/controllers/index.ts +++ b/app/javascript/controllers/index.js @@ -1,27 +1,47 @@ +// 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__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) + +import UrlController from "./url_controller" application.register("url", UrlController) 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/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/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/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/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/base.rb b/app/views/base.rb new file mode 100644 index 0000000..c4c1571 --- /dev/null +++ b/app/views/base.rb @@ -0,0 +1,16 @@ +# 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. + include Phlexible::Rails::ControllerVariables + + # 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/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.html.erb b/app/views/devise/registrations/new.html.erb new file mode 100644 index 0000000..c745bc3 --- /dev/null +++ b/app/views/devise/registrations/new.html.erb @@ -0,0 +1 @@ +<%= render Views::Devise::Registrations::New.new(resource:, resource_name:) %> \ 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..e97584a --- /dev/null +++ b/app/views/devise/registrations/new.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class Views::Devise::Registrations::New < Views::Devise::Base + def view_template + CenteredForm do |cf| + cf.title { title t('devise.registrations.new.title') } + 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 new file mode 100644 index 0000000..2a0092d --- /dev/null +++ b/app/views/devise/sessions/new.html.erb @@ -0,0 +1 @@ +<%= render Views::Devise::Sessions::New.new(resource:, resource_name:) %> \ 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..e5f356c --- /dev/null +++ b/app/views/devise/sessions/new.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +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') } + 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/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/anon.rb b/app/views/home/anon.rb new file mode 100644 index 0000000..4b8de72 --- /dev/null +++ b/app/views/home/anon.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class Views::Home::Anon < Views::Base + def view_template + 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 + end + + private + + def promo(title:, icon_name:, description:) + 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 } + end + end +end 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/app/views/home/index.rb b/app/views/home/index.rb new file mode 100644 index 0000000..2d90213 --- /dev/null +++ b/app/views/home/index.rb @@ -0,0 +1,43 @@ +# 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 + render Home::Card::Url.new + div(class: 'flex flex-col gap-3 *:flex-auto') do + 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 + render Home::Card::Variants.new + render Home::Card::Likeness.new + render Home::Card::Links.new + end + end + end + + def pronunciation_card + + end + + def links_card + end + +end 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/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..1e67a2b --- /dev/null +++ b/app/views/pages/about.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +class Views::Pages::About < Views::Base + def view_template + 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. + + 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 + div(class: 'py-5') do + cta(new_user_registration_path, variant: :primary) do + 'Sign up for your free name.pn page' + end + end + 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 + 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 <<~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 + 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/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/privacy.rb b/app/views/pages/privacy.rb new file mode 100644 index 0000000..c8ac5cd --- /dev/null +++ b/app/views/pages/privacy.rb @@ -0,0 +1,237 @@ +# 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: |
  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. + + 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](#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 + + 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](#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. + + ## 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](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 + + 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.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/app/views/pages/terms.rb b/app/views/pages/terms.rb new file mode 100644 index 0000000..fe118af --- /dev/null +++ b/app/views/pages/terms.rb @@ -0,0 +1,266 @@ +# frozen_string_literal: true + +class Views::Pages::Terms < Views::Base + def view_template + Prose <<~MD + # #{title '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-#{Date.today.year} 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 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 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/application.rb b/config/application.rb index d6c0ae5..ff64030 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.join('app/builders') # Configuration for the application, engines, and railties goes here. # 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/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." diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index f7adb4e..8a7e056 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -1,4 +1,56 @@ en: + components: + shell_layout: + about: About + admin: Admin + terms: Terms of use + privacy: Privacy policy + github: GitHub + roadmap: Public roadmap + api: API + home: + logo: + namedot: name. + pn: pn + pronouns: pronouns + pronounce: pronunciation + subtitle: Your name – your rules + 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}' + pronunciation: + player: + listen: Listen + user: + likeness: + expand: Expand + alt: A picture of %{name} layouts: navbar: label: main navigation @@ -21,51 +73,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: - 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. - 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 @@ -210,3 +217,24 @@ 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 + sign_up: Sign up – it's free + 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. 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"] diff --git a/config/routes.rb b/config/routes.rb index 5726dbf..0d3e094 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,7 +15,6 @@ 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 resources :profile resources :clients, only: %i[create destroy] @@ -25,7 +24,8 @@ 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 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: diff --git a/package.json b/package.json index 6bb6e33..5e390a6 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,13 @@ "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", "@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 +26,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 c06a8ec..1891897 100644 --- a/yarn.lock +++ b/yarn.lock @@ -145,6 +145,26 @@ 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.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.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.1" + "@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" @@ -336,6 +356,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 +971,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 +1057,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 +1222,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 +1525,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.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" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e"