diff --git a/app/controllers/path_controller.rb b/app/controllers/path_controller.rb index d2a0e025d..3d5c7759c 100644 --- a/app/controllers/path_controller.rb +++ b/app/controllers/path_controller.rb @@ -14,6 +14,12 @@ class PathController < ApplicationController next 0 unless current_user && !current_user.trial? StreakDay.current_streak(current_user) } + inertia_share unsubmitted_hours: -> { # Drives the "logged but not submitted" nudge banner on the path page + next nil unless current_user && !current_user.trial? # Qualification is a full-user concept; trial users see the sign-up CTA instead + next nil if current_user.ships.exists? # Already submitted at least one ship — nothing to nudge + next nil if current_logged_seconds < 20 * 3600 # Below the 20h nudge floor + (current_logged_seconds / 3600.0).round + } def index mail_intro_id = deliver_mail_intro || deliver_auto_open_mail @@ -48,6 +54,11 @@ def index private + # Memoized so the unsubmitted_hours share and pending_dialog_key compute the batch hours query at most once per request. + def current_logged_seconds + @current_logged_seconds ||= current_user.total_time_logged_seconds + end + NUDGE_INTERVAL_DAYS = 12 def deliver_auto_open_mail @@ -90,7 +101,7 @@ def pending_dialog_key(journal_entries) sixty = campaigns_by_key["sixty_hours"] return "sixty_hours" if sixty && !sixty.seen? - if sixty.nil? && current_user.total_time_logged_seconds >= 60 * 3600 + if sixty.nil? && current_logged_seconds >= 60 * 3600 current_user.dialog_campaigns.create!(key: "sixty_hours") return "sixty_hours" end diff --git a/app/frontend/components/announcements/AnnouncementCard.tsx b/app/frontend/components/announcements/AnnouncementCard.tsx index 004aefb80..e3b846350 100644 --- a/app/frontend/components/announcements/AnnouncementCard.tsx +++ b/app/frontend/components/announcements/AnnouncementCard.tsx @@ -1,4 +1,5 @@ import { motion, type Variants } from 'motion/react' +import { ModalLink } from '@inertiaui/modal-react' import type { Announcement, AnnouncementKind } from './types' import { dismissAnnouncement } from './storage' @@ -6,15 +7,13 @@ type Props = { announcement: Announcement index: number stacked: boolean - isOnly: boolean } const STYLES: Record = { critical: - 'bg-brown text-beige py-2 px-3 lg:px-6 text-sm sm:text-lg shadow-[0_3px_0_rgba(97,69,58,0.35)] underline decoration-1 underline-offset-2 hover:bg-light-brown hover:text-dark-brown', - info: 'bg-light-brown text-dark-brown py-2 px-3 lg:px-5 text-sm sm:text-base shadow-[0_2px_0_rgba(97,69,58,0.3)] hover:bg-yellow', - promo: - 'bg-yellow text-dark-brown py-1.5 px-3 lg:px-5 text-xs sm:text-sm shadow-[0_2px_0_rgba(97,69,58,0.25)] hover:bg-light-brown', + 'bg-brown text-beige py-1.5 px-3 text-xs sm:text-sm shadow-[0_3px_0_rgba(97,69,58,0.35)] underline decoration-1 underline-offset-2 hover:bg-light-brown hover:text-dark-brown', + info: 'bg-light-brown text-dark-brown py-1.5 px-3 text-xs sm:text-sm shadow-[0_2px_0_rgba(97,69,58,0.3)] hover:bg-yellow', + promo: 'bg-yellow text-dark-brown py-1 px-3 text-xs shadow-[0_2px_0_rgba(97,69,58,0.25)] hover:bg-light-brown', } const TACK_COLOR: Record = { @@ -37,12 +36,12 @@ const cardVariants: Variants = { exit: { opacity: 0, y: -8, scale: 0.96, transition: { duration: 0.18 } }, } -export default function AnnouncementCard({ announcement, index, stacked, isOnly }: Props) { +export default function AnnouncementCard({ announcement, index, stacked }: Props) { const tilt = tiltFor(index, stacked) const isCritical = announcement.kind === 'critical' const baseClass = - 'relative pointer-events-auto block border border-dark-brown rounded-xs transition-colors text-center font-medium overflow-hidden' + 'relative pointer-events-auto block border border-dark-brown rounded-xs transition-colors text-left font-medium overflow-hidden' const styleClass = STYLES[announcement.kind] const paddingShift = isCritical ? 'pl-4' : '' // Make room for the coral accent stripe. @@ -55,8 +54,8 @@ export default function AnnouncementCard({ announcement, index, stacked, isOnly className={`absolute -top-1.5 left-1/2 -translate-x-1/2 size-2.5 rounded-full ring-1 ring-dark-brown/40 shadow-[0_1px_0_rgba(97,69,58,0.5)] ${TACK_COLOR[announcement.kind]}`} /> )} - - {announcement.message} + + {announcement.message} {announcement.dismissible && ( - -
- koi - {koiBalance} - - Koi Balance - -
-
- gold - {goldBalance} - - Gold Balance - +
+
+ + +
+ koi + {koiBalance} + + Koi Balance + +
+
+ gold + {goldBalance} + + Gold Balance + +
+
) diff --git a/app/frontend/pages/path/index.tsx b/app/frontend/pages/path/index.tsx index fc79473b2..f9e6ecaad 100644 --- a/app/frontend/pages/path/index.tsx +++ b/app/frontend/pages/path/index.tsx @@ -10,7 +10,6 @@ import SignUpCta from '@/components/path/SignUpCta' import BgmPlayer from '@/components/path/BgmPlayer' import Header from '@/components/path/Header' import SubmissionCountdown from '@/components/path/SubmissionCountdown' -import AnnouncementsBar from '@/components/announcements/AnnouncementsBar' import FlashMessages from '@/components/FlashMessages' import { notify } from '@/lib/notifications' import { useLiveReload } from '@/lib/useLiveReload' @@ -470,7 +469,6 @@ export default function PathIndex() { style={{ pointerEvents: pathIntro.hudVisible ? 'auto' : 'none' }} >
- = 24h (e.g. a 30h video is "P1DT6H1S"), + # which the old PT-only regex couldn't match. + match = duration_string.match(/\AP(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?\z/) return nil unless match - (match[1].to_i * 3600) + (match[2].to_i * 60) + match[3].to_i + days, hours, minutes, seconds = match.captures.map(&:to_i) + (days * 86_400) + (hours * 3_600) + (minutes * 60) + seconds end def youtube_api_key