diff --git a/app/Livewire/Services/Upgrade.php b/app/Livewire/Services/Upgrade.php index d4be2bc42..2d3762ca5 100644 --- a/app/Livewire/Services/Upgrade.php +++ b/app/Livewire/Services/Upgrade.php @@ -118,6 +118,15 @@ class Upgrade extends Component ->first(); if (!$plan) { $fail(__('Invalid upgrade.')); + return; + } + + // Prevent downgrades - only allow upgrades where new price >= current price + $currentPrice = $this->service->plan->price()->price; + $newPrice = $plan->price()->price; + + if ($newPrice < $currentPrice) { + $fail(__('Downgrades are not allowed. You may only upgrade to a higher-priced plan.')); } }, ], @@ -146,6 +155,12 @@ class Upgrade extends Component $this->validate(); $upgradePlan = $this->upgradeProduct->availablePlans()->where('billing_period', $this->service->plan->billing_period)->where('billing_unit', $this->service->plan->billing_unit)->first(); + + // Additional check to prevent downgrades + if ($upgradePlan && $upgradePlan->price()->price < $this->service->plan->price()->price) { + $this->notify('Downgrades are not allowed. You may only upgrade to a higher-priced plan.', 'error'); + return; + } // The old config options must be in upgradableConfigOptions $serviceConfigOptions = $this->service->configs->pluck('config_value_id', 'config_option_id')->toArray(); @@ -177,7 +192,9 @@ class Upgrade extends Component } $price = $upgrade->calculatePrice(); - if ($price->price <= 0) { + // Only process free upgrades immediately (price == 0) + // Negative prices (downgrades) are blocked by validation + if ($price->price == 0) { $upgrade->status = ServiceUpgrade::STATUS_COMPLETED; $upgrade->save(); @@ -203,26 +220,7 @@ class Upgrade extends Component UpgradeJob::dispatch($this->service); } - // Check if user has credits in this currency - /** @var \App\Models\User */ - $user = Auth::user(); - $credit = $user->credits()->where('currency_code', $price->currency->code)->first(); - - if ($credit) { - // Increment the credits, `abs()` ensures the amount to add is positive - $credit->increment('amount', abs($price->price)); - } else { - $user->credits()->create([ - 'currency_code' => $price->currency->code, - 'amount' => $price->price, - ]); - } - - if ($price->price < 0) { - $this->notify('The upgrade has been completed. We\'ve added the remaining amount to your account balance.', 'success'); - } else { - $this->notify('The upgrade has been completed.', 'success'); - } + $this->notify('The upgrade has been completed.', 'success'); return $this->redirect(route('services.show', $this->service), true); } diff --git a/app/Models/Service.php b/app/Models/Service.php index d7f9d56ac..b011364e2 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -191,8 +191,14 @@ class Service extends Model // Only get the upgrades that have the exact same billing cycle as the service if ($plan->count() > 0) { $product->plan = $plan->first(); - - return $product; + + // Only allow upgrades (not downgrades) - check if new price is greater than or equal to current price + $currentPrice = $this->plan->price()->price; + $newPrice = $product->plan->price()->price; + + if ($newPrice >= $currentPrice) { + return $product; + } } return null; diff --git a/themes/lucentui/views/home.blade.php b/themes/lucentui/views/home.blade.php index 67496bb0d..01e809eac 100644 --- a/themes/lucentui/views/home.blade.php +++ b/themes/lucentui/views/home.blade.php @@ -2,6 +2,7 @@ /* ============================================ LUCENTUI HOMEPAGE - Professional Theme Uses theme CSS variables for consistency + Enhanced with accessibility & performance ============================================ */ #lucent-home { @@ -50,6 +51,51 @@ /* Shadows using theme primary */ --lucent-shadow-glow: 0 0 30px hsl(var(--color-primary) / 0.15); --lucent-shadow-glow-strong: 0 0 50px hsl(var(--color-primary) / 0.25); + + /* Focus states for accessibility */ + --lucent-focus-ring: 0 0 0 3px hsl(var(--color-primary) / 0.4); + --lucent-focus-ring-offset: 0 0 0 2px hsl(var(--color-background)); +} + +/* Skip to content link for accessibility */ +.lucent-skip-link { + position: absolute; + top: -100px; + left: 1rem; + z-index: 10000; + padding: 0.75rem 1.5rem; + background: var(--lucent-primary); + color: white; + font-weight: 600; + text-decoration: none; + border-radius: var(--lucent-radius-md); + transition: top 0.3s ease; +} + +.lucent-skip-link:focus { + top: 1rem; + outline: none; + box-shadow: var(--lucent-focus-ring), var(--lucent-focus-ring-offset); +} + +/* Screen reader only content */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +/* Enhanced focus states for all interactive elements */ +a:focus-visible, +button:focus-visible { + outline: none; + box-shadow: var(--lucent-focus-ring), var(--lucent-focus-ring-offset); } /* Base Reset for Homepage */ @@ -110,6 +156,9 @@ filter: blur(80px); pointer-events: none; animation: lucent-orb-float 20s ease-in-out infinite; + will-change: transform; + transform: translateZ(0); /* GPU acceleration */ + backface-visibility: hidden; } .lucent-orb-1 { @@ -164,6 +213,8 @@ background: rgba(255, 255, 255, 0.12); border-radius: 50%; animation: lucent-particle-float linear infinite; + will-change: transform, opacity; + transform: translateZ(0); /* GPU acceleration */ } .lucent-particle:nth-child(1) { left: 10%; animation-duration: 22s; animation-delay: 0s; width: 8px; height: 8px; } @@ -223,6 +274,8 @@ animation: lucent-badge-float 4s ease-in-out infinite; text-transform: uppercase; letter-spacing: 0.05em; + will-change: transform; + transform: translateZ(0); } .lucent-hero-badge:hover { @@ -468,6 +521,8 @@ transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; + will-change: transform, box-shadow; + transform: translateZ(0); } .lucent-stat-card::before { @@ -565,6 +620,112 @@ opacity: 0.7; } +/* ============================================ + TRUST INDICATORS SECTION + ============================================ */ + +.lucent-trust-section { + padding: clamp(2rem, 6vw, 3.5rem) 0; + background: linear-gradient(180deg, transparent 0%, rgba(59, 130, 246, 0.02) 50%, transparent 100%); + border-top: 1px solid rgba(255, 255, 255, 0.05); + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.lucent-trust-header { + text-align: center; + margin-bottom: 2rem; +} + +.lucent-trust-label { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + opacity: 0.6; + margin-bottom: 1.5rem; +} + +.lucent-trust-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 2rem; + align-items: center; + justify-items: center; +} + +@media (max-width: 1024px) { + .lucent-trust-grid { + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; + } +} + +@media (max-width: 640px) { + .lucent-trust-grid { + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + } +} + +.lucent-trust-badge { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.75rem; + padding: 1.25rem; + background: linear-gradient(145deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: var(--lucent-radius-lg); + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.lucent-trust-badge::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, var(--lucent-primary), transparent); + opacity: 0; + transition: opacity 0.3s ease; +} + +.lucent-trust-badge:hover { + transform: translateY(-4px); + border-color: rgba(59, 130, 246, 0.3); + background: linear-gradient(145deg, rgba(59, 130, 246, 0.05) 0%, rgba(59, 130, 246, 0.02) 100%); + box-shadow: 0 8px 24px rgba(59, 130, 246, 0.1); +} + +.lucent-trust-badge:hover::before { + opacity: 1; +} + +.lucent-trust-icon { + font-size: 2rem; + color: var(--lucent-primary); + transition: transform 0.3s ease; +} + +.lucent-trust-badge:hover .lucent-trust-icon { + transform: scale(1.1); +} + +.lucent-trust-text { + font-size: 0.75rem; + font-weight: 600; + text-align: center; + opacity: 0.8; + transition: opacity 0.3s ease; +} + +.lucent-trust-badge:hover .lucent-trust-text { + opacity: 1; +} + /* ============================================ NODE STATUS SECTION - Proxmox Style ============================================ */ @@ -1801,6 +1962,107 @@ transform-origin: bottom; } +/* ============================================ + FAQ SECTION + ============================================ */ + +.lucent-faq-section { + padding: clamp(3rem, 8vw, 5rem) 0; + position: relative; +} + +.lucent-faq-container { + max-width: 56rem; + margin: 0 auto; +} + +.lucent-faq-item { + background: linear-gradient(145deg, var(--background-secondary, #1e293b) 0%, rgba(30, 41, 59, 0.5) 100%); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: var(--lucent-radius-xl); + margin-bottom: 1rem; + overflow: hidden; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.lucent-faq-item:hover { + border-color: rgba(59, 130, 246, 0.3); + box-shadow: 0 4px 16px rgba(59, 130, 246, 0.08); +} + +.lucent-faq-question { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1.5rem; + background: transparent; + border: none; + color: inherit; + font-size: 1.125rem; + font-weight: 600; + text-align: left; + cursor: pointer; + transition: all 0.3s ease; +} + +.lucent-faq-question:hover { + color: var(--lucent-primary); +} + +.lucent-faq-question:focus-visible { + outline: none; + box-shadow: inset var(--lucent-focus-ring); +} + +.lucent-faq-icon { + flex-shrink: 0; + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + color: var(--lucent-primary); +} + +.lucent-faq-item.active .lucent-faq-icon { + transform: rotate(180deg); +} + +.lucent-faq-answer { + max-height: 0; + overflow: hidden; + transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), padding 0.4s ease; + padding: 0 1.5rem; +} + +.lucent-faq-item.active .lucent-faq-answer { + max-height: 500px; + padding: 0 1.5rem 1.5rem; +} + +.lucent-faq-answer-content { + font-size: 0.9375rem; + line-height: 1.7; + opacity: 0.85; + padding-top: 0.5rem; + border-top: 1px solid rgba(255, 255, 255, 0.06); +} + +.lucent-faq-answer-content p { + margin: 0 0 0.75rem 0; +} + +.lucent-faq-answer-content p:last-child { + margin-bottom: 0; +} + +.lucent-faq-answer-content ul { + margin: 0.5rem 0; + padding-left: 1.5rem; +} + +.lucent-faq-answer-content li { + margin-bottom: 0.375rem; +} + /* ============================================ CTA SECTION ============================================ */ @@ -2336,6 +2598,9 @@
+ + Skip to main content + @if(theme('custom_homepage_html'))
@@ -2344,7 +2609,7 @@ @endif -
+ -
+
+

Platform Statistics

-
-
+
+ -
99.9%
+
99.9%
{{ theme('stats_uptime_title', 'Uptime SLA') }}
{{ theme('stats_uptime_subtitle', 'Enterprise-grade reliability') }}
-
+ -
-
+
+
24/7
{{ theme('stats_support_title', 'Technical Support') }}
{{ theme('stats_support_subtitle', 'Expert assistance available') }}
-
+ -
-
+
+ -
{{ number_format(\App\Models\User::count()+1439) }}+
+
{{ number_format(\App\Models\User::count()+1439) }}+
{{ theme('stats_users_title', 'Active Users') }}
{{ theme('stats_users_subtitle', 'Developers & businesses') }}
-
+ -
-
+
+ -
{{ number_format(\App\Models\Service::count()+2560) }}+
+
{{ number_format(\App\Models\Service::count()+2560) }}+
{{ theme('stats_servers_title', 'VPS Instances') }}
{{ theme('stats_servers_subtitle', 'Powering production workloads') }}
+
+
+
+
+ + +
+
+
+
{{ theme('trust_label', 'Trusted & Certified') }}
+
+
+ + + + +
-
+