diff --git a/Gemfile b/Gemfile index 0cde951b..b7215262 100644 --- a/Gemfile +++ b/Gemfile @@ -12,6 +12,7 @@ gem "csv" gem "herb", "~> 0.8.9" gem "image_processing", "~> 1.14" gem "importmap-rails" +gem "inline_svg" gem "jbuilder" gem "kamal", require: false gem "mission_control-jobs" diff --git a/Gemfile.lock b/Gemfile.lock index f8dec835..c476b4c9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -236,6 +236,9 @@ GEM actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) + inline_svg (1.10.0) + activesupport (>= 3.0) + nokogiri (>= 1.6) io-console (0.8.2) irb (1.16.0) pp (>= 0.6.0) @@ -635,6 +638,7 @@ DEPENDENCIES hotwire-spark image_processing (~> 1.14) importmap-rails + inline_svg jbuilder kamal letter_opener diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index dfb9ac40..a08eeeb3 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -19,3 +19,10 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; background-color: #f9fafb; } + +@media (prefers-reduced-motion: reduce) { + * { + animation: none !important; + transition: none !important; + } +} diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index e2152a86..c55530db 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -1,137 +1,6 @@ @import "tailwindcss"; @import "./components/sidebar.css"; - -/* Tom Select Tailwind Styles */ - -/* Hide the original select element */ -.ts-wrapper .ts-control + select, -.ts-wrapper + select { - @apply hidden; -} - -select[data-select-tags-target="tagList"] { - @apply hidden; -} - -.ts-wrapper { - @apply relative; -} - -.ts-wrapper.single .ts-control, -.ts-wrapper.multi .ts-control { - @apply w-full px-3.5 py-2.5 border border-gray-300 rounded-lg text-sm bg-white transition-all duration-200; - min-height: 42px; -} - -.ts-wrapper.multi .ts-control { - @apply flex flex-wrap items-center gap-1.5; - padding: 0.375rem 0.625rem; -} - -.ts-wrapper .ts-control:focus-within { - @apply border-blue-500 outline-none ring-4 ring-blue-500/10; -} - -.ts-wrapper.single .ts-control:hover:not(:focus-within), -.ts-wrapper.multi .ts-control:hover:not(:focus-within) { - @apply border-gray-400 bg-gray-50; -} - -.ts-wrapper .ts-control > input { - @apply flex-grow outline-none bg-transparent text-sm; - min-width: 60px; - padding: 0.25rem; -} - -.ts-wrapper .ts-control > input::placeholder { - @apply text-gray-400; -} - -/* Selected items (tags/badges) */ -.ts-wrapper.multi .ts-control > div { - @apply inline-flex items-center gap-1.5 px-2.5 py-1 bg-blue-500 text-white text-sm rounded-md; - max-width: 100%; -} - -.ts-wrapper.multi .ts-control > div.active { - @apply bg-blue-600; -} - -/* Remove button */ -.ts-wrapper .ts-control .remove { - @apply inline-flex items-center justify-center ml-1 text-white/80 hover:text-white cursor-pointer; - font-size: 1.125rem; - line-height: 1; - padding: 0; - border: none; - background: none; -} - -.ts-wrapper .ts-control .remove:hover { - @apply text-white; -} - -/* Dropdown */ -.ts-dropdown { - @apply absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg overflow-hidden; - max-height: 280px; - overflow-y: auto; -} - -.ts-dropdown .ts-dropdown-content { - @apply py-1; -} - -/* Dropdown options */ -.ts-dropdown .option { - @apply px-3.5 py-2 text-sm text-gray-700 cursor-pointer transition-colors duration-150; -} - -.ts-dropdown .option:hover, -.ts-dropdown .option.active { - @apply bg-blue-500 text-white; -} - -.ts-dropdown .option.selected { - @apply hidden; -} - -/* No results message */ -.ts-dropdown .no-results { - @apply px-3.5 py-2 text-sm text-gray-500 italic; -} - -/* Loading state */ -.ts-wrapper.loading::after { - content: ""; - @apply absolute right-3 top-1/2 w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin; - margin-top: -0.5rem; -} - -/* Disabled state */ -.ts-wrapper.disabled .ts-control { - @apply bg-gray-100 text-gray-500 cursor-not-allowed border-gray-200; -} - -/* Single select caret */ -.ts-wrapper.single .ts-control::after { - content: ""; - @apply absolute right-3 top-1/2 w-0 h-0; - border-left: 5px solid transparent; - border-right: 5px solid transparent; - border-top: 5px solid #6b7280; - margin-top: -2.5px; - pointer-events: none; -} - -.ts-wrapper.single.input-active .ts-control::after { - border-top-color: #3b82f6; -} - -/* Focus visible for accessibility */ -.ts-wrapper .ts-control:focus-visible { - @apply outline-none ring-4 ring-blue-500/10; -} +@import "./components/tom-select.css"; /* Form Styles */ .form-label { @@ -285,7 +154,7 @@ nav.pagy a.gap { } .content-wrapper { - @apply max-w-7xl mx-auto overflow-x-hidden p-4 min-h-screen; + @apply max-w-7xl mx-auto overflow-x-hidden p-4 pb-16 min-h-screen; } .section-spacing { diff --git a/app/assets/tailwind/components/tom-select.css b/app/assets/tailwind/components/tom-select.css new file mode 100644 index 00000000..0a707e3d --- /dev/null +++ b/app/assets/tailwind/components/tom-select.css @@ -0,0 +1,131 @@ +/* Tom Select Tailwind Styles, used for Tags management */ + +/* Hide the original select element */ +.ts-wrapper .ts-control + select, +.ts-wrapper + select { + @apply hidden; +} + +select[data-select-tags-target="tagList"] { + @apply hidden; +} + +.ts-wrapper { + @apply relative; +} + +.ts-wrapper.single .ts-control, +.ts-wrapper.multi .ts-control { + @apply w-full px-3.5 py-2.5 border border-gray-300 rounded-lg text-sm bg-white transition-all duration-200; + min-height: 42px; +} + +.ts-wrapper.multi .ts-control { + @apply flex flex-wrap items-center gap-1.5; + padding: 0.375rem 0.625rem; +} + +.ts-wrapper .ts-control:focus-within { + @apply border-blue-500 outline-none ring-4 ring-blue-500/10; +} + +.ts-wrapper.single .ts-control:hover:not(:focus-within), +.ts-wrapper.multi .ts-control:hover:not(:focus-within) { + @apply border-gray-400 bg-gray-50; +} + +.ts-wrapper .ts-control > input { + @apply flex-grow outline-none bg-transparent text-sm; + min-width: 60px; + padding: 0.25rem; +} + +.ts-wrapper .ts-control > input::placeholder { + @apply text-gray-400; +} + +/* Selected items (tags/badges) */ +.ts-wrapper.multi .ts-control > div { + @apply inline-flex items-center gap-1.5 px-2.5 py-1 bg-blue-500 text-white text-sm rounded-md; + max-width: 100%; +} + +.ts-wrapper.multi .ts-control > div.active { + @apply bg-blue-600; +} + +/* Remove button */ +.ts-wrapper .ts-control .remove { + @apply inline-flex items-center justify-center ml-1 text-white/80 hover:text-white cursor-pointer; + font-size: 1.125rem; + line-height: 1; + padding: 0; + border: none; + background: none; +} + +.ts-wrapper .ts-control .remove:hover { + @apply text-white; +} + +/* Dropdown */ +.ts-dropdown { + @apply absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg overflow-hidden; + max-height: 280px; + overflow-y: auto; +} + +.ts-dropdown .ts-dropdown-content { + @apply py-1; +} + +/* Dropdown options */ +.ts-dropdown .option { + @apply px-3.5 py-2 text-sm text-gray-700 cursor-pointer transition-colors duration-150; +} + +.ts-dropdown .option:hover, +.ts-dropdown .option.active { + @apply bg-blue-500 text-white; +} + +.ts-dropdown .option.selected { + @apply hidden; +} + +/* No results message */ +.ts-dropdown .no-results { + @apply px-3.5 py-2 text-sm text-gray-500 italic; +} + +/* Loading state */ +.ts-wrapper.loading::after { + content: ""; + @apply absolute right-3 top-1/2 w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin; + margin-top: -0.5rem; +} + +/* Disabled state */ +.ts-wrapper.disabled .ts-control { + @apply bg-gray-100 text-gray-500 cursor-not-allowed border-gray-200; +} + +/* Single select caret */ +.ts-wrapper.single .ts-control::after { + content: ""; + @apply absolute right-3 top-1/2 w-0 h-0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #6b7280; + margin-top: -2.5px; + pointer-events: none; +} + +.ts-wrapper.single.input-active .ts-control::after { + border-top-color: #3b82f6; +} + +/* Focus visible for accessibility */ +.ts-wrapper .ts-control:focus-visible { + @apply outline-none ring-4 ring-blue-500/10; +} diff --git a/app/javascript/controllers/home_controller.js b/app/javascript/controllers/home_controller.js index 5a8ef171..76444425 100644 --- a/app/javascript/controllers/home_controller.js +++ b/app/javascript/controllers/home_controller.js @@ -3,28 +3,7 @@ import { Controller } from "@hotwired/stimulus"; // Connects to data-controller="home" export default class extends Controller { connect() { - this.setupNavbarScroll(); this.setupSmoothScroll(); - this.setupIntersectionObserver(); - this.setupLoadingStates(); - this.setupIconErrorHandling(); - } - - disconnect() { - window.removeEventListener("scroll", this.handleNavbarScroll); - } - - // Navbar scroll effect - setupNavbarScroll() { - this.handleNavbarScroll = () => { - const navbar = document.querySelector(".navbar"); - if (navbar && window.scrollY > 50) { - navbar.classList.add("scrolled"); - } else if (navbar) { - navbar.classList.remove("scrolled"); - } - }; - window.addEventListener("scroll", this.handleNavbarScroll); } // Smooth scroll for anchor links @@ -46,74 +25,4 @@ export default class extends Controller { }); }); } - - // Add loading animation for cards with intersection observer - setupIntersectionObserver() { - const observerOptions = { - threshold: 0.1, - rootMargin: "0px 0px -50px 0px", - }; - - const observer = new IntersectionObserver((entries) => { - entries.forEach((entry, index) => { - if (entry.isIntersecting) { - setTimeout(() => { - entry.target.style.opacity = "1"; - entry.target.style.transform = "translateY(0)"; - }, index * 100); - observer.unobserve(entry.target); - } - }); - }, observerOptions); - - // Observe cards - const cards = document.querySelectorAll(".card"); - cards.forEach((card) => { - card.style.opacity = "0"; - card.style.transform = "translateY(20px)"; - card.style.transition = "all 0.6s ease"; - observer.observe(card); - }); - - // Handle statistics animation - const statistics = document.querySelectorAll(".statistic h3"); - statistics.forEach((stat) => { - observer.observe(stat.parentElement); - }); - } - - // Handle button loading states - setupLoadingStates() { - document - .querySelectorAll('a[href*="session"], button[type="submit"]') - .forEach((button) => { - button.addEventListener("click", function () { - if (!this.classList.contains("loading")) { - this.classList.add("loading"); - const originalText = this.innerHTML; - this.innerHTML = - 'Loading...'; - - // Reset after 3 seconds if still loading - setTimeout(() => { - if (this.classList.contains("loading")) { - this.classList.remove("loading"); - this.innerHTML = originalText; - } - }, 3000); - } - }); - }); - } - - // Add error handling for missing icons - setupIconErrorHandling() { - const icons = document.querySelectorAll('i[class*="bi-"]'); - icons.forEach((icon) => { - const computedStyle = window.getComputedStyle(icon, "::before"); - if (!computedStyle.content || computedStyle.content === "none") { - console.warn("Icon not loading:", icon.className); - } - }); - } } diff --git a/app/views/layouts/_sidebar.html.erb b/app/views/layouts/_sidebar.html.erb index 0a4528c3..c65b9a46 100644 --- a/app/views/layouts/_sidebar.html.erb +++ b/app/views/layouts/_sidebar.html.erb @@ -3,7 +3,7 @@