feat: add marketing video mode and Remotion marketing video project

Add debug-only Marketing Video Mode toggle that enables hands-free
screen recording across the app: auto-scrolling Featured Trips carousel,
auto-filling trip wizard, smooth trip detail scrolling via CADisplayLink,
and trip options auto-sort with scroll.

Add Remotion marketing video project with 6 scene compositions using
image sequences extracted from screen recordings, varied phone entrance
animations, and deduped frames for smooth playback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-02-13 12:07:35 -06:00
parent 67965cbac6
commit 5f5b137e64
655 changed files with 4008 additions and 63 deletions

View File

@@ -12,6 +12,7 @@ struct AdaptiveHomeContent: View {
@Binding var showNewTrip: Bool
@Binding var selectedTab: Int
@Binding var selectedSuggestedTrip: SuggestedTrip?
@Binding var marketingAutoScroll: Bool
let savedTrips: [SavedTrip]
let suggestedTripsGenerator: SuggestedTripsGenerator
@@ -24,6 +25,7 @@ struct AdaptiveHomeContent: View {
showNewTrip: $showNewTrip,
selectedTab: $selectedTab,
selectedSuggestedTrip: $selectedSuggestedTrip,
marketingAutoScroll: $marketingAutoScroll,
savedTrips: savedTrips,
suggestedTripsGenerator: suggestedTripsGenerator,
displayedTips: displayedTips
@@ -34,6 +36,7 @@ struct AdaptiveHomeContent: View {
showNewTrip: $showNewTrip,
selectedTab: $selectedTab,
selectedSuggestedTrip: $selectedSuggestedTrip,
marketingAutoScroll: $marketingAutoScroll,
savedTrips: savedTrips,
suggestedTripsGenerator: suggestedTripsGenerator,
displayedTips: displayedTips

View File

@@ -18,6 +18,9 @@ struct HomeView: View {
@State private var selectedSuggestedTrip: SuggestedTrip?
@State private var displayedTips: [PlanningTip] = []
@State private var showProPaywall = false
#if DEBUG
@State private var marketingAutoScroll = false
#endif
var body: some View {
TabView(selection: $selectedTab) {
@@ -27,6 +30,7 @@ struct HomeView: View {
showNewTrip: $showNewTrip,
selectedTab: $selectedTab,
selectedSuggestedTrip: $selectedSuggestedTrip,
marketingAutoScroll: marketingAutoScrollBinding,
savedTrips: savedTrips,
suggestedTripsGenerator: suggestedTripsGenerator,
displayedTips: displayedTips
@@ -98,6 +102,13 @@ struct HomeView: View {
let oldName = oldTab < tabNames.count ? tabNames[oldTab] : nil
AnalyticsManager.shared.track(.tabSwitched(tab: newName, previousTab: oldName))
AnalyticsManager.shared.trackScreen(newName)
#if DEBUG
if newTab == 0 && UserDefaults.standard.bool(forKey: "marketingVideoMode") {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
marketingAutoScroll = true
}
}
#endif
}
.sheet(isPresented: $showNewTrip) {
TripWizardView()
@@ -131,6 +142,14 @@ struct HomeView: View {
}
}
private var marketingAutoScrollBinding: Binding<Bool> {
#if DEBUG
return $marketingAutoScroll
#else
return .constant(false)
#endif
}
// MARK: - Hero Card
private var heroCard: some View {

View File

@@ -15,6 +15,7 @@ struct HomeContent_Classic: View {
@Binding var showNewTrip: Bool
@Binding var selectedTab: Int
@Binding var selectedSuggestedTrip: SuggestedTrip?
@Binding var marketingAutoScroll: Bool
let savedTrips: [SavedTrip]
let suggestedTripsGenerator: SuggestedTripsGenerator
@@ -124,36 +125,50 @@ struct HomeContent_Classic: View {
.padding(.horizontal, Theme.Spacing.md)
// Horizontal carousel grouped by region
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Theme.Spacing.lg) {
ForEach(suggestedTripsGenerator.tripsByRegion, id: \.region) { regionGroup in
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
// Region header
HStack(spacing: Theme.Spacing.xs) {
Image(systemName: regionGroup.region.iconName)
.font(.caption)
.accessibilityHidden(true)
Text(regionGroup.region.shortName)
.font(.subheadline)
}
.foregroundStyle(Theme.textSecondary(colorScheme))
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Theme.Spacing.lg) {
ForEach(suggestedTripsGenerator.tripsByRegion, id: \.region) { regionGroup in
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
// Region header
HStack(spacing: Theme.Spacing.xs) {
Image(systemName: regionGroup.region.iconName)
.font(.caption)
.accessibilityHidden(true)
Text(regionGroup.region.shortName)
.font(.subheadline)
}
.foregroundStyle(Theme.textSecondary(colorScheme))
// Trip cards for this region
HStack(spacing: Theme.Spacing.md) {
ForEach(regionGroup.trips) { suggestedTrip in
Button {
selectedSuggestedTrip = suggestedTrip
} label: {
SuggestedTripCard(suggestedTrip: suggestedTrip)
// Trip cards for this region
HStack(spacing: Theme.Spacing.md) {
ForEach(regionGroup.trips) { suggestedTrip in
Button {
selectedSuggestedTrip = suggestedTrip
} label: {
SuggestedTripCard(suggestedTrip: suggestedTrip)
}
.buttonStyle(.plain)
}
.buttonStyle(.plain)
}
}
.id(regionGroup.region)
}
}
}
.contentMargins(.horizontal, Theme.Spacing.md, for: .scrollContent)
.onChange(of: marketingAutoScroll) { _, shouldScroll in
if shouldScroll,
let lastRegion = suggestedTripsGenerator.tripsByRegion.last?.region {
withAnimation(.easeInOut(duration: 3.0)) {
proxy.scrollTo(lastRegion, anchor: .trailing)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) {
marketingAutoScroll = false
}
}
}
}
.contentMargins(.horizontal, Theme.Spacing.md, for: .scrollContent)
}
} else if let error = suggestedTripsGenerator.error {
// Error state

View File

@@ -15,6 +15,7 @@ struct HomeContent_ClassicAnimated: View {
@Binding var showNewTrip: Bool
@Binding var selectedTab: Int
@Binding var selectedSuggestedTrip: SuggestedTrip?
@Binding var marketingAutoScroll: Bool
let savedTrips: [SavedTrip]
let suggestedTripsGenerator: SuggestedTripsGenerator
@@ -123,36 +124,50 @@ struct HomeContent_ClassicAnimated: View {
.padding(.horizontal, Theme.Spacing.md)
// Horizontal carousel grouped by region
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Theme.Spacing.lg) {
ForEach(suggestedTripsGenerator.tripsByRegion, id: \.region) { regionGroup in
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
// Region header
HStack(spacing: Theme.Spacing.xs) {
Image(systemName: regionGroup.region.iconName)
.font(.caption)
.accessibilityHidden(true)
Text(regionGroup.region.shortName)
.font(.subheadline)
}
.foregroundStyle(Theme.textSecondary(colorScheme))
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Theme.Spacing.lg) {
ForEach(suggestedTripsGenerator.tripsByRegion, id: \.region) { regionGroup in
VStack(alignment: .leading, spacing: Theme.Spacing.sm) {
// Region header
HStack(spacing: Theme.Spacing.xs) {
Image(systemName: regionGroup.region.iconName)
.font(.caption)
.accessibilityHidden(true)
Text(regionGroup.region.shortName)
.font(.subheadline)
}
.foregroundStyle(Theme.textSecondary(colorScheme))
// Trip cards for this region
HStack(spacing: Theme.Spacing.md) {
ForEach(regionGroup.trips) { suggestedTrip in
Button {
selectedSuggestedTrip = suggestedTrip
} label: {
SuggestedTripCard(suggestedTrip: suggestedTrip)
// Trip cards for this region
HStack(spacing: Theme.Spacing.md) {
ForEach(regionGroup.trips) { suggestedTrip in
Button {
selectedSuggestedTrip = suggestedTrip
} label: {
SuggestedTripCard(suggestedTrip: suggestedTrip)
}
.buttonStyle(.plain)
}
.buttonStyle(.plain)
}
}
.id(regionGroup.region)
}
}
}
.contentMargins(.horizontal, Theme.Spacing.md, for: .scrollContent)
.onChange(of: marketingAutoScroll) { _, shouldScroll in
if shouldScroll,
let lastRegion = suggestedTripsGenerator.tripsByRegion.last?.region {
withAnimation(.easeInOut(duration: 3.0)) {
proxy.scrollTo(lastRegion, anchor: .trailing)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) {
marketingAutoScroll = false
}
}
}
}
.contentMargins(.horizontal, Theme.Spacing.md, for: .scrollContent)
}
} else if let error = suggestedTripsGenerator.error {
// Error state