diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e7c41fa..aa117e10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,26 +39,16 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20' - - - name: Find yarn cache location - id: yarn-cache - run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - - - name: JS package cache - uses: actions/cache@v4 - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- + cache: 'yarn' + cache-dependency-path: yarn.lock + - name: Install packages - run: | - yarn install --pure-lockfile + run: yarn install --frozen-lockfile - name: Build JS and CSS assets run: | bin/rails javascript:build - bin/rails css:build + bin/rails tailwindcss:build - name: Setup database env: diff --git a/.tool-versions b/.tool-versions index 7efc92d5..6d972f5a 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -nodejs 20.9.0 +nodejs 22.15.1 ruby 3.4.7 \ No newline at end of file diff --git a/Gemfile b/Gemfile index 4b4afacd..98addb94 100644 --- a/Gemfile +++ b/Gemfile @@ -9,9 +9,9 @@ gem 'active_storage_validations', '~> 1.0' # Active Storage gems for validating gem 'aws-sdk-s3', '~> 1.208', require: false # Official AWS Ruby gem for Amazon S3 gem 'bootsnap', require: false # Reduces boot times through caching; required in config/boot.rb gem 'cancancan', '~> 3.4' # Authorization library which restricts what resources a given user is allowed to access -gem 'cssbundling-rails' # Bundle and process CSS [https://github.com/rails/cssbundling-rails] gem 'devise' # Devise 4.0 works with Rails 4.1 onwards. gem 'geocoder', '~> 1.8' # Complete geocoding solution for Ruby +gem 'tailwindcss-rails' # Use Tailwind via Rails asset pipeline # OAuth integration gem 'omniauth' gem 'omniauth-github' diff --git a/Gemfile.lock b/Gemfile.lock index cd98c774..1ff44369 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,31 +1,31 @@ GEM remote: https://rubygems.org/ specs: - action_text-trix (2.1.16) + action_text-trix (2.1.19) railties - actioncable (8.1.2) - actionpack (= 8.1.2) - activesupport (= 8.1.2) + actioncable (8.1.3.1) + actionpack (= 8.1.3.1) + activesupport (= 8.1.3.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.1.2) - actionpack (= 8.1.2) - activejob (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + actionmailbox (8.1.3.1) + actionpack (= 8.1.3.1) + activejob (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) mail (>= 2.8.0) - actionmailer (8.1.2) - actionpack (= 8.1.2) - actionview (= 8.1.2) - activejob (= 8.1.2) - activesupport (= 8.1.2) + actionmailer (8.1.3.1) + actionpack (= 8.1.3.1) + actionview (= 8.1.3.1) + activejob (= 8.1.3.1) + activesupport (= 8.1.3.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.1.2) - actionview (= 8.1.2) - activesupport (= 8.1.2) + actionpack (8.1.3.1) + actionview (= 8.1.3.1) + activesupport (= 8.1.3.1) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -33,16 +33,16 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.1.2) + actiontext (8.1.3.1) action_text-trix (~> 2.1.15) - actionpack (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + actionpack (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.1.2) - activesupport (= 8.1.2) + actionview (8.1.3.1) + activesupport (= 8.1.3.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) @@ -53,22 +53,22 @@ GEM activestorage (>= 6.1.4) activesupport (>= 6.1.4) marcel (>= 1.0.3) - activejob (8.1.2) - activesupport (= 8.1.2) + activejob (8.1.3.1) + activesupport (= 8.1.3.1) globalid (>= 0.3.6) - activemodel (8.1.2) - activesupport (= 8.1.2) - activerecord (8.1.2) - activemodel (= 8.1.2) - activesupport (= 8.1.2) + activemodel (8.1.3.1) + activesupport (= 8.1.3.1) + activerecord (8.1.3.1) + activemodel (= 8.1.3.1) + activesupport (= 8.1.3.1) timeout (>= 0.4.0) - activestorage (8.1.2) - actionpack (= 8.1.2) - activejob (= 8.1.2) - activerecord (= 8.1.2) - activesupport (= 8.1.2) + activestorage (8.1.3.1) + actionpack (= 8.1.3.1) + activejob (= 8.1.3.1) + activerecord (= 8.1.3.1) + activesupport (= 8.1.3.1) marcel (~> 1.0) - activesupport (8.1.2) + activesupport (8.1.3.1) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) @@ -81,19 +81,23 @@ GEM securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) - addressable (2.8.8) + addressable (2.9.0) public_suffix (>= 2.0.2, < 8.0) - airbrussh (1.6.0) + airbrussh (1.6.2) sshkit (>= 1.6.1, != 1.7.0) + anonymous_loader (0.1.3) + version_gem (~> 1.1, >= 1.1.14) ar_lazy_preload (1.1.2) rails (>= 5.2) ast (2.4.3) audited (5.8.0) activerecord (>= 5.2, < 8.2) activesupport (>= 5.2, < 8.2) + auth-sanitizer (0.2.3) + version_gem (~> 1.1, >= 1.1.14) aws-eventstream (1.4.0) - aws-partitions (1.1220.0) - aws-sdk-core (3.242.0) + aws-partitions (1.1276.0) + aws-sdk-core (3.254.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -101,19 +105,19 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.122.0) - aws-sdk-core (~> 3, >= 3.241.4) + aws-sdk-kms (1.130.0) + aws-sdk-core (~> 3, >= 3.254.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.213.0) - aws-sdk-core (~> 3, >= 3.241.4) + aws-sdk-s3 (1.228.2) + aws-sdk-core (~> 3, >= 3.254.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) base64 (0.3.0) - bcrypt (3.1.21) - bigdecimal (4.0.1) - bootsnap (1.23.0) + bcrypt (3.1.22) + bigdecimal (4.1.2) + bootsnap (1.24.6) msgpack (~> 1.2) brevo (4.0.0) addressable (~> 2.3, >= 2.3.0) @@ -121,7 +125,7 @@ GEM typhoeus (~> 1.0, >= 1.0.1) builder (3.3.0) cancancan (3.6.1) - capistrano (3.20.0) + capistrano (3.20.1) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) @@ -152,55 +156,54 @@ GEM regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) childprocess (4.1.0) - concurrent-ruby (1.3.6) + concurrent-ruby (1.3.8) connection_pool (3.0.2) - crass (1.0.6) - css_parser (2.0.0) + crass (1.0.7) + css_parser (3.0.0) addressable - cssbundling-rails (1.4.3) - railties (>= 6.0.0) - csv (3.3.5) + ssrf_filter (~> 1.5) + csv (3.3.6) date (3.5.1) debug (1.10.0) irb (~> 1.10) reline (>= 0.3.8) - devise (5.0.2) + devise (5.0.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 7.0) responders warden (~> 1.2.3) - docile (1.4.1) dockerfile-rails (1.7.10) rails (>= 3.0.0) drb (2.2.3) - erb (6.0.2) + erb (6.0.7) erubi (1.13.1) - et-orbi (1.4.0) + et-orbi (1.4.1) tzinfo - ethon (0.15.0) + ethon (0.18.0) ffi (>= 1.15.0) - faker (3.6.0) + logger + faker (3.8.0) i18n (>= 1.8.11, < 2) - faraday (2.14.1) + faraday (2.14.3) faraday-net_http (>= 2.0, < 3.5) json logger - faraday-net_http (3.4.2) + faraday-net_http (3.4.4) net-http (~> 0.5) - ffi (1.17.3-x86_64-linux-gnu) - fugit (1.12.1) + ffi (1.17.4-x86_64-linux-gnu) + fugit (1.13.0) et-orbi (~> 1.4) raabro (~> 1.4) geocoder (1.8.6) base64 (>= 0.1.0) csv (>= 3.0.0) - globalid (1.3.0) + globalid (1.4.0) activesupport (>= 6.1) hashie (5.1.0) logger htmlentities (4.4.2) - i18n (1.14.8) + i18n (1.15.2) concurrent-ruby (~> 1.0) image_processing (1.14.0) mini_magick (>= 4.9.5, < 6) @@ -208,44 +211,44 @@ GEM invisible_captcha (2.3.0) rails (>= 5.2) io-console (0.8.2) - irb (1.17.0) + irb (1.18.0) pp (>= 0.6.0) prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - jbuilder (2.14.1) + jbuilder (2.15.1) actionview (>= 7.0.0) activesupport (>= 7.0.0) jmespath (1.6.2) jsbundling-rails (1.3.1) railties (>= 6.0.0) - json (2.18.1) - jwt (3.1.2) + json (2.21.2) + jwt (3.2.0) base64 - language_server-protocol (3.17.0.5) + language_server-protocol (3.17.0.6) launchy (2.5.2) addressable (~> 2.8) letter_opener (1.10.0) launchy (>= 2.2, < 4) lint_roller (1.1.0) logger (1.7.0) - loofah (2.25.0) + loofah (2.25.2) crass (~> 1.0.2) nokogiri (>= 1.12.0) - mail (2.9.0) + mail (2.9.1) logger mini_mime (>= 0.1.1) net-imap net-pop net-smtp - marcel (1.1.0) + marcel (1.2.1) matrix (0.4.3) mini_magick (4.13.2) mini_mime (1.1.5) - minitest (6.0.2) + minitest (6.0.6) drb (~> 2.0) prism (~> 1.5) - mocha (3.0.2) + mocha (3.1.0) ruby2_keywords (>= 0.0.5) motor-admin (0.5.0) ar_lazy_preload (~> 1.0) @@ -254,12 +257,12 @@ GEM csv (>= 3.0) fugit (~> 1.0) rails (>= 5.2) - msgpack (1.8.0) - multi_xml (0.8.1) + msgpack (1.8.4) + multi_xml (0.9.1) bigdecimal (>= 3.1, < 5) net-http (0.9.1) uri (>= 0.11.1) - net-imap (0.6.3) + net-imap (0.6.6) date net-protocol net-pop (0.1.2) @@ -272,18 +275,20 @@ GEM net-ssh (>= 5.0.0, < 8.0.0) net-smtp (0.5.1) net-protocol - net-ssh (7.3.0) + net-ssh (7.3.3) nio4r (2.7.5) - nokogiri (1.19.1-x86_64-linux-gnu) + nokogiri (1.19.4-x86_64-linux-gnu) racc (~> 1.4) - oauth2 (2.0.18) + oauth2 (2.0.25) + anonymous_loader (~> 0.1, >= 0.1.3) + auth-sanitizer (~> 0.2, >= 0.2.3) faraday (>= 0.17.3, < 4.0) jwt (>= 1.0, < 4.0) logger (~> 1.2) multi_xml (~> 0.5) rack (>= 1.2, < 4) - snaky_hash (~> 2.0, >= 2.0.3) - version_gem (~> 1.1, >= 1.1.9) + snaky_hash (~> 2.0, >= 2.0.7) + version_gem (~> 1.1, >= 1.1.14) omniauth (2.1.4) hashie (>= 3.4.6) logger @@ -301,14 +306,14 @@ GEM orm_adapter (0.5.0) ostruct (0.6.3) pagy (9.4.0) - parallel (1.27.0) - parser (3.3.10.2) + parallel (1.28.0) + parser (3.3.12.0) ast (~> 2.4.1) racc pg (1.6.3-x86_64-linux) - pp (0.6.3) + pp (0.6.4) prettyprint - premailer (1.28.0) + premailer (1.29.0) addressable css_parser (>= 1.19.0) htmlentities (>= 4.0.0) @@ -318,55 +323,52 @@ GEM premailer (~> 1.7, >= 1.7.9) prettyprint (0.2.0) prism (1.9.0) - psych (5.3.1) - date - stringio - public_suffix (7.0.2) + public_suffix (7.0.5) puma (6.6.1) nio4r (~> 2.0) - raabro (1.4.0) + raabro (1.5.0) racc (1.8.1) - rack (3.2.5) + rack (3.2.6) rack-attack (6.8.0) rack (>= 1.0, < 4) rack-protection (4.2.1) base64 (>= 0.1.0) logger (>= 1.6.0) rack (>= 3.0.0, < 4) - rack-session (2.1.1) + rack-session (2.1.2) base64 (>= 0.1.0) rack (>= 3.0.0) rack-test (2.2.0) rack (>= 1.3) rackup (2.3.1) rack (>= 3) - rails (8.1.2) - actioncable (= 8.1.2) - actionmailbox (= 8.1.2) - actionmailer (= 8.1.2) - actionpack (= 8.1.2) - actiontext (= 8.1.2) - actionview (= 8.1.2) - activejob (= 8.1.2) - activemodel (= 8.1.2) - activerecord (= 8.1.2) - activestorage (= 8.1.2) - activesupport (= 8.1.2) + rails (8.1.3.1) + actioncable (= 8.1.3.1) + actionmailbox (= 8.1.3.1) + actionmailer (= 8.1.3.1) + actionpack (= 8.1.3.1) + actiontext (= 8.1.3.1) + actionview (= 8.1.3.1) + activejob (= 8.1.3.1) + activemodel (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) bundler (>= 1.15.0) - railties (= 8.1.2) + railties (= 8.1.3.1) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.7.0) - loofah (~> 2.25) + rails-html-sanitizer (1.7.1) + loofah (~> 2.25, >= 2.25.2) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) rails_cloudflare_turnstile (0.5.0) faraday (>= 1.0, < 3.0) rails (>= 6.0, < 8.2) - railties (8.1.2) - actionpack (= 8.1.2) - activesupport (= 8.1.2) + railties (8.1.3.1) + actionpack (= 8.1.3.1) + activesupport (= 8.1.3.1) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -374,16 +376,21 @@ GEM tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.3.1) - rdoc (7.2.0) + rake (13.4.2) + rbs (4.1.2) + logger + prism (>= 1.6.0) + tsort + rdoc (8.0.0) erb - psych (>= 4.0.0) + prism (>= 1.6.0) + rbs (>= 4.0.0) tsort redis (5.4.1) redis-client (>= 0.22.0) - redis-client (0.26.4) + redis-client (0.30.1) connection_pool - regexp_parser (2.11.3) + regexp_parser (2.12.0) reline (0.6.3) io-console (~> 0.5) responders (3.2.0) @@ -401,7 +408,7 @@ GEM rubocop-ast (>= 1.46.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.49.0) + rubocop-ast (1.50.0) parser (>= 3.3.7.2) prism (~> 1.7) rubocop-performance (1.25.0) @@ -419,39 +426,34 @@ GEM ffi (~> 1.12) logger ruby2_keywords (0.0.5) - rubyzip (3.2.2) + rubyzip (3.4.1) securerandom (0.4.1) selenium-webdriver (4.1.0) childprocess (>= 0.5, < 5.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2) - sentry-rails (6.4.0) + sentry-rails (6.6.2) railties (>= 5.2.0) - sentry-ruby (~> 6.4.0) - sentry-ruby (6.4.0) + sentry-ruby (~> 6.6.2) + sentry-ruby (6.6.2) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) logger - sidekiq (8.1.1) + sidekiq (8.1.6) connection_pool (>= 3.0.0) json (>= 2.16.0) logger (>= 1.7.0) rack (>= 3.2.0) - redis-client (>= 0.26.0) + redis-client (>= 0.29.0) simple_form (5.4.1) actionpack (>= 7.0) activemodel (>= 7.0) - simplecov (0.22.0) - docile (~> 1.1) - simplecov-html (~> 0.11) - simplecov_json_formatter (~> 0.1) - simplecov-html (0.13.2) - simplecov_json_formatter (0.1.4) - sitemap_generator (6.3.0) + simplecov (1.0.3) + sitemap_generator (7.1.1) builder (~> 3.0) - snaky_hash (2.0.3) + snaky_hash (2.0.7) hashie (>= 0.1.0, < 6) - version_gem (>= 1.1.8, < 3) + version_gem (~> 1.1, >= 1.1.14) sprockets (4.2.2) concurrent-ruby (~> 1.0) logger @@ -460,24 +462,28 @@ GEM actionpack (>= 6.1) activesupport (>= 6.1) sprockets (>= 3.0.0) - sshkit (1.25.0) + sshkit (1.25.1) base64 logger net-scp (>= 1.1.2) net-sftp (>= 2.1.2) net-ssh (>= 2.8.0) ostruct + ssrf_filter (1.5.0) stimulus-rails (1.3.4) railties (>= 6.0.0) - stringio (3.2.0) + tailwindcss-rails (4.4.0) + railties (>= 7.0.0) + tailwindcss-ruby (~> 4.0) + tailwindcss-ruby (4.2.1-x86_64-linux-gnu) thor (1.5.0) - timeout (0.6.0) + timeout (0.6.1) tsort (0.2.0) turbo-rails (2.0.23) actionpack (>= 7.1.0) railties (>= 7.1.0) - typhoeus (1.5.0) - ethon (>= 0.9.0, < 0.16.0) + typhoeus (1.6.0) + ethon (>= 0.18.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (3.2.0) @@ -485,20 +491,20 @@ GEM unicode-emoji (4.2.0) uri (1.1.1) useragent (0.16.11) - version_gem (1.1.9) + version_gem (1.1.14) warden (1.2.9) rack (>= 2.0.9) webdrivers (5.3.1) nokogiri (~> 1.6) rubyzip (>= 1.3.0) selenium-webdriver (~> 4.0, < 4.11) - websocket-driver (0.8.0) + websocket-driver (0.8.2) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.7.5) + zeitwerk (2.8.3) PLATFORMS x86_64-linux @@ -516,7 +522,6 @@ DEPENDENCIES capistrano-sidekiq capistrano-webpacker-precompile capybara - cssbundling-rails debug (~> 1.10.0) devise dockerfile-rails (>= 1.2) @@ -552,6 +557,7 @@ DEPENDENCIES sitemap_generator sprockets-rails (>= 3.5) stimulus-rails (>= 1.3) + tailwindcss-rails turbo-rails (>= 2.0) tzinfo-data webdrivers diff --git a/Procfile.dev b/Procfile.dev index 2b0b260f..2f95f84e 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,3 +1,3 @@ web: bin/rails server -p 3000 js: yarn build --watch -css: yarn build:css --watch +css: yarn watch:css diff --git a/README.md b/README.md index c9f198e7..01696b18 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,17 @@ The Africa Ruby Community (ARC) Platform is a project aimed at creating a hub fo *Note: This project is open source, and contributions are encouraged.* +## Tech Stack + +* Ruby 3.4.7 +* Rails 8.1 +* PostgreSQL +* Tailwind CSS (`tailwindcss-rails`) +* DaisyUI +* Turbo +* Stimulus +* esbuild (JavaScript bundling) + ## Application Setup ### Dev Containers @@ -106,31 +117,32 @@ Confirm the default Ruby version matches the version you just installed. which ruby #=> /Users/username/.asdf/shims/ruby ruby -v -#=> 3.4.1 +#=> 3.4.7 ``` -Install Node.js for handling Javascript in our Rails app +#### Installing Node.js and Yarn + +Node.js is required for JavaScript bundling with **esbuild**. Tailwind CSS is managed separately by **`tailwindcss-rails`**. ```sh -asdf install nodejs 20.9.0 -asdf global nodejs 20.9.0 +asdf install nodejs 22.23.2 +asdf global nodejs 22.23.2 which node #=> /Users/username/.asdf/shims/node -node -v -#=> 20.9.0 -# Install yarn for Rails jsbundling/cssbundling or webpacker -npm install -g yarn +node -v +#=> 22.23.2 ``` -To switch to a different Ruby and Node version for a specific project, you can use the following command to set the Ruby or Node version for that project. You should be in the project directory. +To switch to a different Ruby or Node version for this project: ```sh -asdf local ruby # eg 3.4.7 -asdf local nodejs # eg 20.9.0 +asdf local ruby # e.g. 3.4.7 +asdf local nodejs # e.g. 22.23.2 ``` + ### Step 2: Create a Fork & Branch 1. Create a fork of this repository from the main branch. @@ -154,28 +166,42 @@ $ rails db:create $ rails db:migrate ``` -### Step 4: Install Yarn Dependencies +### Step 5: Install Yarn Dependencies ```sh $ yarn install ``` -### Step 5: Start Server +This installs JavaScript dependencies. Tailwind CSS is compiled by `tailwindcss-rails` and is started automatically by `./bin/dev` in development. + +### Step 6: Start Server ```sh $ ./bin/dev ``` -### Step 6: Creating a Pull Request +> Clear cache: `rails assets:clobber` the yarn build + +### Step 7: Creating a Pull Request 1. Make changes locally on your branch. 2. Push your changes to your branch on GitHub. 3. Create a pull request to the main branch of the main repository. -### Step 7: Merging +### Step 8: Merging Once your changes are reviewed, they will be merged into the main branch. +## Troubleshooting +### Clearing Asset & Style Caches + + +```sh +rails assets:clobber +yarn build +bundle exec rails tailwindcss:build +``` + ## Contributing Thank you for considering contributing to the Africa Ruby Community (ARC) Platform! We welcome contributions from developers of all experience levels. diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index 7be382fe..dd2e7209 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -1,5 +1,5 @@ //= link_tree ../images //= link_tree ../builds +//= link tailwind.css //= link mailgun_mails.css -//= link application.tailwind.css //= link_tree ../../javascript .js diff --git a/app/assets/images/brands_facebook.png b/app/assets/images/brands_facebook.png index 96338877..f922b038 100644 Binary files a/app/assets/images/brands_facebook.png and b/app/assets/images/brands_facebook.png differ diff --git a/app/assets/images/brands_github.png b/app/assets/images/brands_github.png index 0b2d4ed3..698a29f1 100644 Binary files a/app/assets/images/brands_github.png and b/app/assets/images/brands_github.png differ diff --git a/app/assets/images/brands_instagram.png b/app/assets/images/brands_instagram.png index 1a3bcec3..0f418ef0 100644 Binary files a/app/assets/images/brands_instagram.png and b/app/assets/images/brands_instagram.png differ diff --git a/app/assets/images/brands_linkedin.png b/app/assets/images/brands_linkedin.png index e32db512..80383ff1 100644 Binary files a/app/assets/images/brands_linkedin.png and b/app/assets/images/brands_linkedin.png differ diff --git a/app/assets/images/brands_x.png b/app/assets/images/brands_x.png new file mode 100644 index 00000000..9b44c1fe Binary files /dev/null and b/app/assets/images/brands_x.png differ diff --git a/app/assets/images/countries-black.png b/app/assets/images/countries-black.png new file mode 100644 index 00000000..5460bed5 Binary files /dev/null and b/app/assets/images/countries-black.png differ diff --git a/app/assets/images/hero-community.jpg b/app/assets/images/hero-community.jpg new file mode 100644 index 00000000..ae16e08d Binary files /dev/null and b/app/assets/images/hero-community.jpg differ diff --git a/app/assets/images/icons/facebook.svg b/app/assets/images/icons/facebook.svg new file mode 100644 index 00000000..6e2a5b05 --- /dev/null +++ b/app/assets/images/icons/facebook.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/app/assets/images/icons/github.svg b/app/assets/images/icons/github.svg new file mode 100644 index 00000000..2cfe90a1 --- /dev/null +++ b/app/assets/images/icons/github.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/app/assets/images/icons/instagram.svg b/app/assets/images/icons/instagram.svg new file mode 100644 index 00000000..9f95b5bf --- /dev/null +++ b/app/assets/images/icons/instagram.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/app/assets/images/icons/linkedin.svg b/app/assets/images/icons/linkedin.svg new file mode 100644 index 00000000..08c0731c --- /dev/null +++ b/app/assets/images/icons/linkedin.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/app/assets/images/icons/telegram.svg b/app/assets/images/icons/telegram.svg new file mode 100644 index 00000000..950e0ab4 --- /dev/null +++ b/app/assets/images/icons/telegram.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/app/assets/images/icons/x.svg b/app/assets/images/icons/x.svg new file mode 100644 index 00000000..7aec6e6a --- /dev/null +++ b/app/assets/images/icons/x.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/app/assets/images/rubyconf.png b/app/assets/images/rubyconf.png new file mode 100644 index 00000000..9742a860 Binary files /dev/null and b/app/assets/images/rubyconf.png differ diff --git a/app/assets/images/sponsors/current/fata.png b/app/assets/images/sponsors/current/fata.png new file mode 100644 index 00000000..2349ef96 Binary files /dev/null and b/app/assets/images/sponsors/current/fata.png differ diff --git a/app/assets/images/sponsors/current/kodees.png b/app/assets/images/sponsors/current/kodees.png new file mode 100644 index 00000000..714f6a0d Binary files /dev/null and b/app/assets/images/sponsors/current/kodees.png differ diff --git a/app/assets/images/sponsors/current/rubycon_italy.png b/app/assets/images/sponsors/current/rubycon_italy.png new file mode 100644 index 00000000..9e3b4ddd Binary files /dev/null and b/app/assets/images/sponsors/current/rubycon_italy.png differ diff --git a/app/assets/images/sponsors/current/senga.png b/app/assets/images/sponsors/current/senga.png new file mode 100644 index 00000000..e6a64cda Binary files /dev/null and b/app/assets/images/sponsors/current/senga.png differ diff --git a/app/assets/images/sponsors/current/sidekiq.png b/app/assets/images/sponsors/current/sidekiq.png new file mode 100644 index 00000000..b541c88a Binary files /dev/null and b/app/assets/images/sponsors/current/sidekiq.png differ diff --git a/app/assets/images/sponsors/current/solutech_official.svg b/app/assets/images/sponsors/current/solutech_official.svg index 42c66838..5822592d 100644 --- a/app/assets/images/sponsors/current/solutech_official.svg +++ b/app/assets/images/sponsors/current/solutech_official.svg @@ -1,377 +1,377 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/images/sponsors/current/ustechnology.jpeg b/app/assets/images/sponsors/current/ustechnology.jpeg new file mode 100644 index 00000000..5d6bc8de Binary files /dev/null and b/app/assets/images/sponsors/current/ustechnology.jpeg differ diff --git a/app/assets/images/sponsors/current/app_signal.png b/app/assets/images/sponsors/previous/app_signal.png similarity index 100% rename from app/assets/images/sponsors/current/app_signal.png rename to app/assets/images/sponsors/previous/app_signal.png diff --git a/app/assets/images/sponsors/current/daystar.png b/app/assets/images/sponsors/previous/daystar.png similarity index 100% rename from app/assets/images/sponsors/current/daystar.png rename to app/assets/images/sponsors/previous/daystar.png diff --git a/app/assets/images/sponsors/current/gurzu.png b/app/assets/images/sponsors/previous/gurzu.png similarity index 100% rename from app/assets/images/sponsors/current/gurzu.png rename to app/assets/images/sponsors/previous/gurzu.png diff --git a/app/assets/images/sponsors/previous/solutech_official.svg b/app/assets/images/sponsors/previous/solutech_official.svg new file mode 100644 index 00000000..5822592d --- /dev/null +++ b/app/assets/images/sponsors/previous/solutech_official.svg @@ -0,0 +1,377 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css deleted file mode 100644 index e6061901..00000000 --- a/app/assets/stylesheets/application.tailwind.css +++ /dev/null @@ -1,84 +0,0 @@ -/* 1. The main Tailwind 4 import */ -@import "tailwindcss"; - -/* 2. Load your Plugins */ -@plugin "@tailwindcss/typography"; -@plugin "daisyui"; - -/* 3. Fonts and Theme Overrides */ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap'); - -@theme { - --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; - --color-primary: var(--color-red-600); - --color-primary-content: var(--color-white); - --color-primary-focus: #B01820; - --color-primary-active: #B01820; - /* If you had custom colors in tailwind.config.js, put them here: */ - /* --color-brand-primary: #xxxxxx; */ -} - -/* 4. Custom Animations & Utilities */ -@utility alert_custom_class { - animation: appear-then-fade 6s both; - margin-top: 50px; -} - -@keyframes appear-then-fade { - 0%, 100% { opacity: 0; } - 5%, 60% { opacity: 1; } -} - -/* 5. Custom Component Logic */ -@layer components { - .turbo-progress-bar { - background: linear-gradient(to right, var(--color-primary), var(--color-base-100)); - } - - .text-error { - font-size: .875rem; - color: var(--color-error); - } - - /* Override DaisyUI btn-primary to use red */ - .btn-primary { - background-color: #D82028 !important; - border-color: #D82028 !important; - color: white !important; - } - - .btn-primary:hover { - background-color: #B01820 !important; - border-color: #B01820 !important; - } - - /* Table Styles */ - .resp-table { width: 100%; display: table; } - .resp-table-body { display: table-row-group; } - .resp-table-row { display: table-row; } - .table-body-cell { - display: table-cell; - border: 1px solid #dddddd; - padding: 8px; - line-height: 1.42857143; - vertical-align: top; - } - - .rounded-box { - position: relative; - overflow: hidden; - - & .absolute { - position: absolute; - top: 0; left: 0; right: 0; bottom: 0; - display: flex; - align-items: center; - justify-content: center; - z-index: 10; - } - - & img { - display: block; - } - } -} diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css new file mode 100644 index 00000000..33b97628 --- /dev/null +++ b/app/assets/tailwind/application.css @@ -0,0 +1,1024 @@ +/* ============================================================ + RubyCommunity.africa — Global Design System + Stack: Tailwind CSS v4 + DaisyUI + ============================================================ */ + +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap"); +@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"); + +@import "tailwindcss"; +@plugin "@tailwindcss/typography"; +@plugin "daisyui"; + +/* Force JetBrains Mono globally — overrides DaisyUI base layer */ +* { + font-family: "JetBrains Mono", monospace !important; +} + +.fa, .fas, .far, .fab, i[class*="fa-"] { + font-family: "Font Awesome 6 Free" !important; +} + +/* ============================================================ + Semantic CSS Variables — respond to data-theme + ============================================================ */ +:root { + --font-sans: "JetBrains Mono", "Inter", sans-serif; + --font-mono: "JetBrains Mono", monospace; +} + +:root, +[data-theme="mytheme"] { + --bg-base: #fdf6f0; + --bg-page: #ffffff; + --bg-card: #ffffff; + --bg-muted: #f3f4f6; + --bg-subtle: #f9fafb; + --bg-nav: #ffffff; + --bg-nav-border: rgba(229, 231, 235, 0.8); + --backdrop-filter: none; + --bg-dropdown: #ffffff; + + --text-base: #1a1a1a; + --text-muted: #101214; + --text-faint: #6b7280; + --text-xfaint: #9ca3af; + + --border-color: #e5e7eb; + --border-subtle: #f3f4f6; + + --shadow-card: 0 2px 12px rgba(0, 0, 0, 0.07); + --shadow-hover: 0 8px 30px rgba(0, 0, 0, 0.12); + --shadow-nav: 0 1px 8px rgba(0, 0, 0, 0.06); +} + +[data-theme="dark"] { + --bg-base: #0f172a; + --bg-page: #111827; + --bg-card: #1e293b; + --bg-muted: #1f2937; + --bg-subtle: #263244; + --bg-nav: #0f172a; + --bg-nav-border: rgba(55, 65, 81, 0.5); + --bg-dropdown: #1e293b; + + --text-base: #f1f5f9; + --text-muted: #cbd5e1; + --text-faint: #94a3b8; + --text-xfaint: #64748b; + + --border-color: rgba(255, 255, 255, 0.08); + --border-subtle: rgba(255, 255, 255, 0.04); + + --shadow-card: 0 2px 12px rgba(0, 0, 0, 0.25); + --shadow-hover: 0 8px 30px rgba(0, 0, 0, 0.4); + --shadow-nav: 0 1px 8px rgba(0, 0, 0, 0.3); + + color-scheme: dark; +} + +/* ============================================================ + Keyframes + ============================================================ */ +@keyframes typing { + from { + width: 0; + } + + to { + width: 100%; + } +} + +@keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes scale-in { + from { + opacity: 0; + transform: scale(0.95); + } + + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes shake { + 0%, + 100% { + transform: translateX(0); + } + + 20% { + transform: translateX(-6px); + } + + 40% { + transform: translateX(6px); + } + + 60% { + transform: translateX(-4px); + } + + 80% { + transform: translateX(4px); + } +} + +@keyframes appear-then-fade { + 0%, + 100% { + opacity: 0; + } + + 5%, + 60% { + opacity: 1; + } +} + +@keyframes arc-page-out { + from { opacity: 1; transform: translateY(0); } + to { opacity: 0; transform: translateY(-8px); } +} + +@keyframes arc-page-in { + from { opacity: 0; transform: translateY(16px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes marquee { + from { + transform: translateX(0); + } + + to { + transform: translateX(-50%); + } +} + +/* ============================================================ + Utility Classes + ============================================================ */ +@utility alert_custom_class { + animation: appear-then-fade 6s both; + margin-top: 50px; +} + +/* ============================================================ + Components + ============================================================ */ +@layer components { + /* ── Base page ── */ + html { + font-size: 16px; + } + + body { + background-color: var(--bg-base); + color: var(--text-base); + font-family: var(--font-sans) !important; + font-size: 1rem; + line-height: 1.6; + transition: + background-color 0.25s ease, + color 0.2s ease; + } + + body, + p, + span, + div, + a, + button, + input, + textarea, + select, + label { + font-family: var(--font-sans) !important; + } + + code, + pre, + kbd, + samp, + .font-mono { + font-family: var(--font-mono) !important; + } + + .fa, + .fas, + .far, + .fab, + i[class*="fa-"] { + font-family: "Font Awesome 6 Free" !important; + } + + /* ── Section backgrounds — light uses alternating warm/white, dark uses stepped grays ── */ + .arc-section-white { + background-color: var(--bg-page); + } + + .arc-section-muted { + background-color: var(--bg-muted); + } + + .arc-section-subtle { + background-color: var(--bg-subtle); + } + + /* ── Turbo ── */ + .turbo-progress-bar { + background: linear-gradient(to right, #cc342d, #f4a261); + } + + .text-error { + font-size: 0.875rem; + color: #f87272; + } + + /* ── Semantic background utilities ── */ + .arc-bg-base { + background-color: var(--bg-base); + } + + .arc-bg-card { + background-color: var(--bg-card); + } + + .arc-bg-muted { + background-color: var(--bg-muted); + } + + .arc-bg-subtle { + background-color: var(--bg-subtle); + } + + /* ── Semantic text utilities ── */ + .arc-text-base { + color: var(--text-base); + } + + .arc-text-muted { + color: var(--text-muted); + } + + .arc-text-faint { + color: var(--text-faint); + } + + /* ── Semantic border ── */ + .arc-border { + border-color: var(--border-color); + } + + /* ── DaisyUI btn-primary → ruby red ── */ + .btn-primary { + background-color: #cc342d !important; + border-color: #cc342d !important; + color: white !important; + border-radius: 9999px; + font-weight: 600; + transition: + background-color 200ms ease, + transform 100ms ease, + box-shadow 200ms ease; + } + + .btn-primary:hover { + background-color: #b01820 !important; + border-color: #b01820 !important; + box-shadow: 0 4px 15px rgba(204, 52, 45, 0.35); + } + + .btn-primary:active { + transform: scale(0.97); + } + + /* ── Ruby CTA Button ── */ + .arc-btn-ruby { + display: inline-flex; + align-items: center; + gap: 0.5rem; + background-color: #cc342d; + color: white !important; + font-weight: 600; + font-size: 0.875rem; + padding: 0.7rem 1.6rem; + border-radius: 9999px; + border: none; + cursor: pointer; + transition: + background-color 200ms ease, + transform 100ms ease, + box-shadow 200ms ease; + text-decoration: none; + } + + .arc-btn-ruby:hover { + background-color: #b01820; + box-shadow: 0 4px 15px rgba(204, 52, 45, 0.35); + color: white !important; + } + + .arc-btn-ruby:active { + transform: scale(0.96); + } + + /* ── Ruby Outline Button ── */ + .arc-btn-outline { + display: inline-flex; + align-items: center; + gap: 0.5rem; + background-color: transparent; + color: #cc342d; + font-weight: 600; + font-size: 0.875rem; + padding: 0.68rem 1.6rem; + border-radius: 9999px; + border: 2px solid #cc342d; + cursor: pointer; + transition: all 200ms ease; + text-decoration: none; + } + + .arc-btn-outline:hover { + background-color: #cc342d; + color: white; + } + + /* ── Arc Card ── */ + .arc-card { + background-color: var(--bg-card); + color: var(--text-base); + border-radius: 1rem; + box-shadow: var(--shadow-card); + border: 1px solid var(--border-color); + transition: + box-shadow 250ms ease, + transform 250ms ease, + background-color 0.2s ease; + overflow: hidden; + } + + .arc-card:hover { + box-shadow: var(--shadow-hover); + transform: translateY(-3px); + } + + /* ── Navbar ── */ + #main-navbar .arc-navbar-bg { + background-color: var(--bg-nav); + border-bottom-color: var(--bg-nav-border); + box-shadow: var(--shadow-nav); + transition: + background-color 0.25s ease, + border-color 0.25s ease; + } + + /* Hamburger icon inherits text color */ + #main-navbar svg { + color: var(--text-base); + } + + /* ── Nav link ── */ + .arc-nav-link { + position: relative; + font-size: 0.875rem; + font-weight: 600; + color: var(--text-muted); + padding: 0.25rem 0; + text-decoration: none; + transition: color 200ms ease; + white-space: nowrap; + } + + .arc-nav-link::after { + content: ""; + position: absolute; + bottom: -4px; + left: 0; + width: 0; + height: 2px; + background: #cc342d; + border-radius: 9999px; + transition: width 250ms ease; + } + + .arc-nav-link:hover { + color: #cc342d; + } + + .arc-nav-link:hover::after { + width: 100%; + } + + .arc-nav-link.active { + color: #cc342d; + } + + .arc-nav-link.active::after { + width: 100%; + } + + /* ── Dropdown menus ── */ + .dropdown-content.menu { + background-color: var(--bg-dropdown) !important; + border-color: var(--border-color) !important; + color: var(--text-base); + } + + .dropdown-content.menu a, + .dropdown-content.menu button { + color: var(--text-muted); + } + + .dropdown-content.menu a:hover, + .dropdown-content.menu button:hover { + background-color: var(--bg-muted); + color: var(--text-base); + } + + .menu-title { + color: var(--text-xfaint) !important; + } + + /* ── Badges ── */ + .arc-badge { + display: inline-flex; + align-items: center; + font-size: 0.65rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 0.2rem 0.6rem; + border-radius: 9999px; + } + + .arc-badge-ruby { + background-color: rgba(204, 52, 45, 0.1); + color: #cc342d; + border: 1px solid rgba(204, 52, 45, 0.25); + } + + .arc-badge-green { + background-color: rgba(22, 163, 74, 0.1); + color: #16a34a; + border: 1px solid rgba(22, 163, 74, 0.25); + } + + .arc-badge-amber { + background-color: rgba(217, 119, 6, 0.1); + color: #d97706; + border: 1px solid rgba(217, 119, 6, 0.25); + } + + .arc-badge-hiring { + background-color: rgba(34, 197, 94, 0.12); + color: #15803d; + border: 1px solid rgba(34, 197, 94, 0.3); + } + + /* ── Section heading ── */ + .arc-heading { + font-size: 2.25rem; + font-weight: 800; + color: var(--text-base); + position: relative; + display: inline-block; + margin-bottom: 0.5rem; + line-height: 1.2; + } + + .arc-heading::after { + content: ""; + display: block; + width: 3rem; + height: 4px; + background: linear-gradient(to right, #cc342d, #f4a261); + border-radius: 9999px; + margin-top: 0.5rem; + } + + .arc-heading-center::after { + margin: 0.5rem auto 0; + } + + /* ── Typing animation ── */ + .arc-typing { + display: inline-block; + vertical-align: bottom; + overflow: hidden; + white-space: nowrap; + width: 0; + animation: typing 1.2s steps(11, end) 0.3s forwards; + } + + /* ── Form input ── */ + .arc-input { + width: 100%; + height: 3rem; + padding: 0 1rem; + border-radius: 0.75rem; + border: 1.5px solid var(--border-color); + background-color: var(--bg-muted); + color: var(--text-base); + font-size: 0.95rem; + transition: + border-color 200ms ease, + box-shadow 200ms ease, + background-color 0.2s ease; + outline: none; + } + + .arc-input:focus { + border-color: #cc342d; + box-shadow: 0 0 0 3px rgba(204, 52, 45, 0.12); + background-color: var(--bg-card); + } + + .arc-input::placeholder { + color: var(--text-xfaint); + } + + /* ── Auth page background container ── */ + .arc-auth-container { + min-height: 100vh; + width: 100%; + display: flex !important; + align-items: center; + justify-content: center; + padding: 4rem 1rem; + background: linear-gradient(135deg, #fdf6f0 0%, #fff5f5 50%, #ffffff 100%); + transition: background-color 0.3s ease; + } + + /* ── Auth card ── */ + .arc-auth-card { + background-color: var(--bg-card); + border-radius: 1.25rem; + box-shadow: var(--shadow-hover); + overflow: hidden; + transition: background-color 0.2s ease; + } + + .arc-auth-card .card-body { + padding: 0; + } + + /* ── Hero section with hero-community.jpg background & theme overlays ── */ + .arc-hero-landing { + background-image: + linear-gradient( + 135deg, + rgba(255, 255, 255, 0.92) 0%, + rgba(254, 242, 242, 0.86) 50%, + rgba(255, 255, 255, 0.94) 100% + ), + var(--hero-img); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + transition: background-image 0.3s ease; + } + + [data-theme="dark"] .arc-hero-landing { + background-image: + linear-gradient( + 135deg, + rgba(13, 3, 5, 0.92) 0%, + rgba(24, 6, 10, 0.88) 50%, + rgba(13, 3, 5, 0.95) 100% + ), + var(--hero-img); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + } + + /* ── Hero ruby strip ── */ + .arc-hero-ruby { + background: linear-gradient(135deg, #cc342d 0%, #8b1219 55%, #1c0a0b 100%); + position: relative; + overflow: hidden; + } + + .arc-hero-ruby::before { + content: ""; + position: absolute; + inset: 0; + background-image: repeating-linear-gradient( + 45deg, + rgba(255, 255, 255, 0.04) 0px, + rgba(255, 255, 255, 0.04) 1px, + transparent 1px, + transparent 20px + ); + pointer-events: none; + } + + /* ── Sponsor logo: Full original color on small viewports, grayscale hover on desktop ── */ + .arc-sponsor-logo { + filter: grayscale(0) opacity(1); + transition: + filter 300ms ease, + transform 250ms ease; + } + + @media (min-width: 768px) { + .arc-sponsor-logo { + filter: grayscale(1) opacity(0.65); + } + + .arc-sponsor-logo:hover { + filter: grayscale(0) opacity(1); + transform: scale(1.06); + } + } + + /* ── Date badge on event cards ── */ + .arc-date-badge { + position: absolute; + top: 0.75rem; + left: 0.75rem; + background: white; + border-radius: 0.5rem; + padding: 0.4rem 0.65rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18); + text-align: center; + line-height: 1; + z-index: 10; + min-width: 2.75rem; + } + + .arc-date-badge .day { + display: block; + font-size: 1.35rem; + font-weight: 800; + color: #cc342d; + line-height: 1; + } + + .arc-date-badge .month { + display: block; + font-size: 0.6rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #6b7280; + margin-top: 2px; + } + + /* ── Timeline ── */ + .arc-timeline { + position: relative; + padding-left: 2rem; + } + + .arc-timeline::before { + content: ""; + position: absolute; + left: 0.625rem; + top: 0.5rem; + bottom: 0.5rem; + width: 2px; + background: linear-gradient(to bottom, #cc342d, rgba(204, 52, 45, 0.1)); + } + + .arc-timeline-item { + position: relative; + margin-bottom: 2rem; + } + + .arc-timeline-item::before { + content: ""; + position: absolute; + left: -1.875rem; + top: 0.375rem; + width: 0.875rem; + height: 0.875rem; + border-radius: 9999px; + background: #cc342d; + border: 2px solid var(--bg-card); + box-shadow: 0 0 0 3px rgba(204, 52, 45, 0.2); + } + + /* ── Pull quote ── */ + + .arc-pull-quote { + position: relative; + display: block; + padding: 1.75rem 1.75rem 1.5rem; + border-radius: 1rem; + border: 1px solid rgba(148, 163, 184, 0.35); + background: rgba(248, 250, 252, 0.95); + color: #111827; + font-size: 1.05rem; + font-weight: 500; + line-height: 1.8; + box-shadow: 0 20px 45px rgba(15, 23, 42, 0.06); + } + + .arc-pull-quote.text-gray-800 { + color: #111827 !important; + } + + [data-theme="dark"] .arc-pull-quote { + background: rgba(15, 23, 42, 0.7); + border-color: rgba(148, 163, 184, 0.15); + color: #f8fafc; + } + + [data-theme="dark"] .arc-pull-quote.dark\:text-gray-100 { + color: #f8fafc !important; + } + + .arc-pull-quote::before { + content: "\201C"; + position: absolute; + top: -0.5rem; + left: 0.75rem; + font-size: 4rem; + color: rgba(204, 52, 45, 0.15); + font-family: Georgia, serif; + line-height: 1; + } + + /* ── Scroll reveal ── */ + [data-reveal] { + opacity: 0; + transform: translateY(28px); + transition: + opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1), + transform 0.6s cubic-bezier(0.16, 1, 0.3, 1); + } + + [data-reveal="left"] { + transform: translateX(-32px); + } + + [data-reveal="right"] { + transform: translateX(32px); + } + + [data-reveal="scale"] { + transform: scale(0.94); + } + + [data-reveal].arc-revealed { + opacity: 1; + transform: none; + } + + /* stagger children */ + [data-reveal-stagger] > * { + opacity: 0; + transform: translateY(20px); + transition: + opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), + transform 0.5s cubic-bezier(0.16, 1, 0.3, 1); + } + + [data-reveal-stagger].arc-revealed > *:nth-child(1) { transition-delay: 0ms; } + [data-reveal-stagger].arc-revealed > *:nth-child(2) { transition-delay: 80ms; } + [data-reveal-stagger].arc-revealed > *:nth-child(3) { transition-delay: 160ms; } + [data-reveal-stagger].arc-revealed > *:nth-child(4) { transition-delay: 240ms; } + [data-reveal-stagger].arc-revealed > *:nth-child(5) { transition-delay: 320ms; } + [data-reveal-stagger].arc-revealed > *:nth-child(6) { transition-delay: 400ms; } + [data-reveal-stagger].arc-revealed > *:nth-child(n+7) { transition-delay: 480ms; } + + [data-reveal-stagger].arc-revealed > * { + opacity: 1; + transform: none; + } + + /* ── Turbo page transitions ── */ + .turbo-animate-out { + animation: arc-page-out 0.18s ease-in forwards; + } + + .turbo-animate-in { + animation: arc-page-in 0.32s cubic-bezier(0.16, 1, 0.3, 1) forwards; + } + + /* ── Marquee ── */ + .arc-marquee-track { + display: flex; + gap: 3rem; + width: max-content; + animation: marquee 32s linear infinite; + } + + .arc-marquee-track:hover { + animation-play-state: paused; + } + + /* ── Footer ── */ + footer.arc-footer { + background: transparent !important; + background-color: transparent !important; + color: var(--text-faint) !important; + transition: none; + } + + /* ── Table styles (preserved) ── */ + .resp-table { + width: 100%; + display: table; + } + + .resp-table-body { + display: table-row-group; + } + + .resp-table-row { + display: table-row; + } + + .table-body-cell { + display: table-cell; + border: 1px solid var(--border-color); + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + } + + .rounded-box { + position: relative; + overflow: hidden; + + & .absolute { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + } + + & img { + display: block; + } + } + + /* ── Dark mode: override hardcoded Tailwind bg/text utilities ── */ + [data-theme="dark"] body, + [data-theme="dark"] main { + background-color: var(--bg-base); + color: var(--text-base); + } + + /* Section backgrounds */ + [data-theme="dark"] section.arc-section-white, + [data-theme="dark"] .arc-section-white { + background-color: var(--bg-page) !important; + } + + [data-theme="dark"] .arc-section-muted { + background-color: var(--bg-muted) !important; + } + + [data-theme="dark"] .arc-section-subtle { + background-color: var(--bg-subtle) !important; + } + + /* Tailwind utility overrides for dark mode */ + [data-theme="dark"] .bg-white { + background-color: var(--bg-card) !important; + } + + [data-theme="dark"] .bg-gray-50 { + background-color: var(--bg-muted) !important; + } + + [data-theme="dark"] .bg-gray-100 { + background-color: var(--bg-subtle) !important; + } + + [data-theme="dark"] .text-gray-900 { + color: var(--text-base) !important; + } + + [data-theme="dark"] .text-gray-800 { + color: var(--text-base) !important; + } + + [data-theme="dark"] .text-gray-700 { + color: var(--text-muted) !important; + } + + [data-theme="dark"] .text-gray-600 { + color: var(--text-muted) !important; + } + + [data-theme="dark"] .text-gray-500 { + color: var(--text-faint) !important; + } + + [data-theme="dark"] .text-gray-400 { + color: var(--text-xfaint) !important; + } + + [data-theme="dark"] .border-gray-100, + [data-theme="dark"] .border-gray-200 { + border-color: var(--border-color) !important; + } + + /* Cards */ + [data-theme="dark"] .arc-card { + background-color: var(--bg-card) !important; + color: var(--text-base); + } + + [data-theme="dark"] .arc-auth-container { + background: linear-gradient( + 135deg, + #0c0305 0%, + #16060a 50%, + #0a0204 100% + ) !important; + } + + [data-theme="dark"] .arc-auth-card { + background-color: var(--bg-card) !important; + border: 1px solid var(--border-color); + } + + /* Ruby-50 tinted sections */ + [data-theme="dark"] .bg-ruby-50 { + background-color: rgba(204, 52, 45, 0.08) !important; + } + + /* Inputs */ + [data-theme="dark"] .arc-input { + background-color: var(--bg-muted); + border-color: var(--border-color); + color: var(--text-base); + } + + /* DaisyUI base */ + [data-theme="dark"] .bg-base-100 { + background-color: var(--bg-page) !important; + } + + /* Outline button in dark */ + [data-theme="dark"] .arc-btn-outline { + border-color: rgba(204, 52, 45, 0.7); + color: #fc8181; + } + + [data-theme="dark"] .arc-btn-outline:hover { + background-color: rgba(204, 52, 45, 0.15); + color: #fc8181; + border-color: #fc8181; + } + + /* Pull quote dark */ + [data-theme="dark"] .arc-pull-quote { + background: rgba(204, 52, 45, 0.08); + } + + /* Stats / info boxes in light sections */ + [data-theme="dark"] .rounded-2xl.border.border-gray-100 { + background-color: var(--bg-subtle) !important; + border-color: var(--border-color) !important; + } + + /*Chapters*/ + + [data-theme="light"] .chapters-dark { + display: none; + } + [data-theme="dark"] .chapters-light { + display: none; + } + + /* Who we are stats grid */ + [data-theme="dark"] .hover\:bg-ruby-50:hover { + background-color: rgba(204, 52, 45, 0.1) !important; + } + + [data-theme="dark"] .hover\:border-ruby-200:hover { + border-color: rgba(204, 52, 45, 0.3) !important; + } + + /* Timeline dot border */ + [data-theme="dark"] .arc-timeline-item::before { + border-color: var(--bg-card); + } +} diff --git a/app/controllers/companies_controller.rb b/app/controllers/companies_controller.rb new file mode 100644 index 00000000..431575c4 --- /dev/null +++ b/app/controllers/companies_controller.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class CompaniesController < ApplicationController + before_action :authenticate_user!, except: [:index] + + def index + @companies = Company.published.with_attached_logo + + @companies = @companies.where('name ILIKE ? OR description ILIKE ?', "%#{params[:query]}%", "%#{params[:query]}%") if params[:query].present? + + @companies = @companies.where(country: params[:country]) if params[:country].present? + + @countries = Company.published.distinct.pluck(:country).compact.sort + end + + def new + @company = Company.new + end + + def create + @company = Company.new(company_params) + @company.published = false # Ensure it's not published by default + + if @company.save + redirect_to built_with_ruby_path, notice: t('.success') + else + render :new, status: :unprocessable_content + end + end + + private + + def company_params + params.expect(company: %i[name website_url careers_url country city description logo]) + end +end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 6e2c5965..d08a4151 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -43,7 +43,7 @@ def create redirect_to @event, notice: 'Event was successfully created.' else @chapters = current_user.organization_admin? ? Chapter.all : current_user.chapters - render :new, status: :unprocessable_entity + render :new, status: :unprocessable_content end end @@ -53,7 +53,7 @@ def update redirect_to @event, notice: 'Event was successfully updated.' else @chapters = current_user.organization_admin? ? Chapter.all : current_user.chapters - render :edit, status: :unprocessable_entity + render :edit, status: :unprocessable_content end end diff --git a/app/controllers/landing_controller.rb b/app/controllers/landing_controller.rb index c5287e7e..c3387ea5 100644 --- a/app/controllers/landing_controller.rb +++ b/app/controllers/landing_controller.rb @@ -2,7 +2,7 @@ class LandingController < ApplicationController # People should not require authentication for following actions - skip_before_action :authenticate_user!, only: %i[index about learn] + skip_before_action :authenticate_user!, only: %i[index about learn code_of_conduct] def index; end ## @@ -12,4 +12,8 @@ def about; end ## # Featured learning materials def learn; end + + ## + # Code of Conduct page + def code_of_conduct; end end diff --git a/app/helpers/chapters_helper.rb b/app/helpers/chapters_helper.rb index ffff924a..f3fa20f7 100644 --- a/app/helpers/chapters_helper.rb +++ b/app/helpers/chapters_helper.rb @@ -19,52 +19,75 @@ module ChaptersHelper ].freeze FEATURED_SPONSORS = [ - { image: 'sponsors/current/solutech_official.svg', link: 'https://solutech.co.ke', - alt_key: 'sponsors.current.solutech' }, - { image: 'sponsors/current/app_signal.png', link: 'https://www.appsignal.com', - alt_key: 'sponsors.current.app_signal' }, - { image: 'sponsors/current/ruby_central.png', link: 'https://rubycentral.org/', - alt_key: 'sponsors.current.ruby_central' }, + { image: 'sponsors/current/kopo_kopo.png', link: 'https://kopokopo.co.ke', alt_key: 'sponsors.current.kopo_kopo' }, - { image: 'sponsors/current/finplus.png', link: 'https://finplusgroup.com', alt_key: 'sponsors.current.finplus' }, + { image: 'sponsors/current/typesense-logo.png', link: 'https://typesense.org/', alt_key: 'sponsors.current.typesense' }, - { image: 'sponsors/current/daystar.png', link: 'https://www.daystar.ac.ke/', alt_key: 'sponsors.current.daystar' }, - { image: 'sponsors/current/prosper.png', link: 'https://www.prosperhedge.com/', - alt_key: 'sponsors.current.prosper' }, - { image: 'sponsors/current/gurzu.png', link: 'https://gurzu.com/', alt_key: 'sponsors.current.gurzu' }, - { image: 'sponsors/current/must-company.png', link: 'https://must.company/', alt_key: 'sponsors.current.must_company' } + { image: 'sponsors/current/sidekiq.png', link: 'https://sidekiq.org/', + alt_key: 'sponsors.current.sidekiq' }, + { image: 'sponsors/current/fata.png', link: 'https://fata.dev/', + alt_key: 'sponsors.current.fata' }, + { image: 'sponsors/current/senga.png', link: 'https://senga.co/', + alt_key: 'sponsors.current.senga' }, + { image: 'sponsors/current/ustechnology.jpeg', link: 'https://ustechmiami.com/', + alt_key: 'sponsors.current.ustechnologies' }, + { image: 'sponsors/current/kodees.png', link: 'https://www.kodees.co.ke/', alt_key: 'sponsors.current.kodees' }, + { image: 'sponsors/current/rubycon_italy.png', link: 'https://rubycon.it/', + alt_key: 'sponsors.current.rubycon_italy' } ].freeze PREVIOUS_SPONSORS = [ - { image: 'sponsors/previous/shopify.webp', link: 'https://www.shopify.com/', alt_key: 'sponsors.previous.shopify' }, - { image: 'sponsors/previous/microverse.png', link: 'https://www.microverse.org/', - alt_key: 'sponsors.previous.microverse' }, - { image: 'sponsors/previous/planet_argon.png', link: 'https://www.planetargon.com', - alt_key: 'sponsors.previous.planet_argon' }, - { image: 'sponsors/previous/nairobits.png', link: 'https://www.nairobits.com/', - alt_key: 'sponsors.previous.nairobits' }, - { image: 'sponsors/previous/turing.png', link: 'https://www.turing.com/', alt_key: 'sponsors.previous.turing' }, - { image: 'sponsors/previous/kwara.png', link: 'https://kwara.com/', alt_key: 'sponsors.previous.kwara' }, - { image: 'sponsors/previous/ihub.png', link: 'https://ihub.co.ke/', alt_key: 'sponsors.previous.ihub' }, - { image: 'sponsors/previous/friendly_rb.jpg', link: 'https://friendlyrb.com/', - alt_key: 'sponsors.previous.friendly_rb' }, - { image: 'sponsors/previous/kca.png', link: 'https://www.kcau.ac.ke', alt_key: 'sponsors.previous.kca' }, - { image: 'sponsors/previous/andela.png', link: 'https://andela.com/', alt_key: 'sponsors.previous.andela' } + + { image: 'sponsors/current/ruby_central.png', link: 'https://rubycentral.org/', + alt_key: 'sponsors.current.ruby_central' }, + { image: 'sponsors/current/finplus.png', link: 'https://finplusgroup.com', alt_key: 'sponsors.current.finplus' }, + { image: 'sponsors/previous/solutech_official.svg', link: 'https://solutech.co.ke', + alt_key: 'sponsors.current.solutech' }, + { image: 'sponsors/previous/daystar.png', link: 'https://www.daystar.ac.ke/', alt_key: 'sponsors.current.daystar' }, + { image: 'sponsors/current/prosper.png', link: 'https://www.prosperhedge.com/', + alt_key: 'sponsors.current.prosper' }, + { image: 'sponsors/previous/gurzu.png', link: 'https://gurzu.com/', alt_key: 'sponsors.current.gurzu' }, + { image: 'sponsors/current/must-company.png', link: 'https://must.company/', alt_key: 'sponsors.current.must_company' } ].freeze SOCIALS = [ - { alt_key: 'social_media.twitter', link: 'https://twitter.com/ruby_african', image: 'brands_twitter.png', - show: true }, - { alt_key: 'social_media.telegram', link: '#', image: 'brands_telegram.png', - show: FeatureFlag.find_by(name: 'telegram')&.enabled }, - { alt_key: 'social_media.facebook', link: 'https://www.facebook.com/rubycommunity.africa', - image: 'brands_facebook.png', - show: true }, - { alt_key: 'social_media.instagram', link: 'https://www.instagram.com/africanruby_community/', image: 'brands_instagram.png', show: true }, - { alt_key: 'social_media.linkedin', link: 'https://www.linkedin.com/company/african-ruby-community/', - image: 'brands_linkedin.png', show: true }, - { alt_key: 'social_media.github', link: 'https://github.com/nairuby', image: 'brands_github.png', show: true } + { + alt_key: 'social_media.x', + link: 'https://x.com/ruby_african', + icon: 'icons/x.svg', + show: true + }, + { + alt_key: 'social_media.telegram', + link: '#', + icon: 'icons/telegram.svg', + show: FeatureFlag.find_by(name: 'telegram')&.enabled + }, + { + alt_key: 'social_media.facebook', + link: 'https://www.facebook.com/rubycommunity.africa', + icon: 'icons/facebook.svg', + show: true + }, + { + alt_key: 'social_media.instagram', + link: 'https://www.instagram.com/africanruby_community/', + icon: 'icons/instagram.svg', + show: true + }, + { + alt_key: 'social_media.linkedin', + link: 'https://www.linkedin.com/company/african-ruby-community/', + icon: 'icons/linkedin.svg', + show: true + }, + { + alt_key: 'social_media.github', + link: 'https://github.com/nairuby', + icon: 'icons/github.svg', + show: true + } ].freeze def activities @@ -161,6 +184,7 @@ def country_icon(country_name) icon_map[country_name] end + # rubocop:enable Metrics/MethodLength end # rubocop:enable Metrics/ModuleLength diff --git a/app/helpers/icon_helper.rb b/app/helpers/icon_helper.rb new file mode 100644 index 00000000..d31a002e --- /dev/null +++ b/app/helpers/icon_helper.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# app/helpers/icon_helper.rb + +module IconHelper + def inline_svg(path, options = {}) + return ''.html_safe if path.blank? + + file = Rails.root.join('app/assets/images', path) + return ''.html_safe unless File.exist?(file) + + svg = File.read(file) + + if options[:class].present? + svg = svg.sub( + / { + document.querySelector("main")?.classList.add("turbo-animate-out") +}) + +document.addEventListener("turbo:load", () => { + const main = document.querySelector("main") + if (main) { + main.classList.remove("turbo-animate-out") + main.classList.add("turbo-animate-in") + main.addEventListener("animationend", () => main.classList.remove("turbo-animate-in"), { once: true }) + } +}) + function renderTurnstileIfReady() { const el = document.querySelector(".cf-turnstile") if (!el) return diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index 4edf7a32..cfb07c37 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -15,3 +15,6 @@ application.register("turbo_frames", TurboFramesController) import TurnstileController from "./turnstile_controller.js" application.register("turnstile", TurnstileController) + +import ThemeController from "./theme_controller.js" +application.register("theme", ThemeController) diff --git a/app/javascript/controllers/scroll_reveal_controller.js b/app/javascript/controllers/scroll_reveal_controller.js new file mode 100644 index 00000000..7c92c303 --- /dev/null +++ b/app/javascript/controllers/scroll_reveal_controller.js @@ -0,0 +1,25 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + connect() { + this.observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add("arc-revealed") + this.observer.unobserve(entry.target) + } + }) + }, + { threshold: 0.12, rootMargin: "0px 0px -40px 0px" } + ) + + this.element.querySelectorAll("[data-reveal]").forEach((el) => { + this.observer.observe(el) + }) + } + + disconnect() { + this.observer?.disconnect() + } +} diff --git a/app/javascript/controllers/theme_controller.js b/app/javascript/controllers/theme_controller.js new file mode 100644 index 00000000..ef135cab --- /dev/null +++ b/app/javascript/controllers/theme_controller.js @@ -0,0 +1,40 @@ +import { Controller } from "@hotwired/stimulus" + +// Manages light/dark theme toggle with cookie persistence +export default class extends Controller { + connect() { + const saved = this.#readCookie("arc_theme") || "mytheme" + this.#applyTheme(saved) + } + + toggle() { + const current = document.documentElement.getAttribute("data-theme") || "mytheme" + const next = current === "dark" ? "mytheme" : "dark" + this.#applyTheme(next) + this.#writeCookie("arc_theme", next, 365) + } + + #applyTheme(theme) { + const isDark = theme === "dark" + document.documentElement.setAttribute("data-theme", theme) + + // Toggle moon/sun icons across ALL toggle buttons on the page + document.querySelectorAll("[data-theme-moon]").forEach(el => { + el.style.display = isDark ? "none" : "inline" + }) + document.querySelectorAll("[data-theme-sun]").forEach(el => { + el.style.display = isDark ? "inline" : "none" + }) + } + + #readCookie(name) { + const match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)")) + return match ? match[2] : null + } + + #writeCookie(name, value, days) { + const d = new Date() + d.setTime(d.getTime() + days * 86400000) + document.cookie = `${name}=${value};expires=${d.toUTCString()};path=/;SameSite=Lax` + } +} diff --git a/app/models/company.rb b/app/models/company.rb new file mode 100644 index 00000000..893d198e --- /dev/null +++ b/app/models/company.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class Company < ApplicationRecord + has_one_attached :logo + + validates :name, presence: true + validates :country, presence: true + validates :slug, presence: true, uniqueness: true + + before_validation :generate_slug, if: -> { name.present? && (slug.blank? || name_changed?) } + + scope :published, -> { where(published: true) } + scope :featured, -> { where(featured: true) } + + def to_param + slug + end + + private + + def generate_slug + base_slug = name.parameterize + candidate_slug = base_slug + counter = 1 + + while Company.where(slug: candidate_slug).where.not(id: id).exists? + candidate_slug = "#{base_slug}-#{counter}" + counter += 1 + end + + self.slug = candidate_slug + end +end diff --git a/app/views/chapters/index.html.erb b/app/views/chapters/index.html.erb index 64ea38e5..b29dae6b 100644 --- a/app/views/chapters/index.html.erb +++ b/app/views/chapters/index.html.erb @@ -1,150 +1,133 @@ <% content_for(:title, t('chapters.index.page_title')) %> <% content_for(:description, t('chapters.index.page_description')) %> <% require 'digest' %> -
-
- -
- + +<%# ── HERO STRIP ── %> +
+
+
+ + <%= t('chapters.index.mini_title') %>
+

<%= t('chapters.index.title') %>

+

<%= t('chapters.index.page_description') %>Find your local Ruby community and connect with developers in your city.

+
+
-

- <%= t('chapters.index.title') %> -

+<%# ── FILTER ── %> +
+
+
+
-
+<%# ── CHAPTERS GRID ── %> +
+
<% @chapters.each do |chapter| %> -
-
- <%= image_tag chapter.image.attached? ? chapter.image.url : image_path('chapter.jpg'), - alt: chapter.name, - class: "w-full h-full object-cover transform group-hover:scale-110 transition-transform duration-500" - %> +
+ <%# Image area %> +
+ <%= image_tag(chapter.image.attached? ? chapter.image.url : image_path('chapter.jpg'), + alt: chapter.name, + class: "w-full h-full object-cover group-hover:scale-105 transition-transform duration-500") %> +
+
+

<%= chapter.name %>

+
- -
-

<%= chapter.name %>

+ <%# Card footer %> +
+
+ + Members +
+ Active
<% end %>
<% if @country_param.present? %> - - + <%# Featured Members — hidden by design but structure preserved %>