Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions RubyEvents.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
973DD3652D95F8BE008E0AED /* CachedAsyncImage in Frameworks */ = {isa = PBXBuildFile; productRef = 973DD3642D95F8BE008E0AED /* CachedAsyncImage */; };
97613B4C2D0635C600220031 /* HotwireNative in Frameworks */ = {isa = PBXBuildFile; productRef = 97613B4B2D0635C600220031 /* HotwireNative */; };
97613B572D06512900220031 /* YouTubePlayerKit in Frameworks */ = {isa = PBXBuildFile; productRef = 97613B562D06512900220031 /* YouTubePlayerKit */; };
97AAF2552FFF19EC009DB8C2 /* Typesense in Frameworks */ = {isa = PBXBuildFile; productRef = 97AAF2542FFF19EC009DB8C2 /* Typesense */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand Down Expand Up @@ -43,6 +44,7 @@
buildActionMask = 2147483647;
files = (
97613B572D06512900220031 /* YouTubePlayerKit in Frameworks */,
97AAF2552FFF19EC009DB8C2 /* Typesense in Frameworks */,
973DD3652D95F8BE008E0AED /* CachedAsyncImage in Frameworks */,
97613B4C2D0635C600220031 /* HotwireNative in Frameworks */,
);
Expand Down Expand Up @@ -90,6 +92,7 @@
97613B4B2D0635C600220031 /* HotwireNative */,
97613B562D06512900220031 /* YouTubePlayerKit */,
973DD3642D95F8BE008E0AED /* CachedAsyncImage */,
97AAF2542FFF19EC009DB8C2 /* Typesense */,
);
productName = RubyEvents;
productReference = 97613B162D0634F500220031 /* RubyEvents.app */;
Expand Down Expand Up @@ -123,6 +126,7 @@
97613B4A2D0635C600220031 /* XCRemoteSwiftPackageReference "hotwire-native-ios" */,
97613B552D06512900220031 /* XCRemoteSwiftPackageReference "YouTubePlayerKit" */,
973DD3632D95F8BE008E0AED /* XCRemoteSwiftPackageReference "CachedAsyncImage" */,
97AAF2532FFF19EC009DB8C2 /* XCRemoteSwiftPackageReference "typesense-swift" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 97613B172D0634F500220031 /* Products */;
Expand Down Expand Up @@ -386,6 +390,14 @@
minimumVersion = 1.9.0;
};
};
97AAF2532FFF19EC009DB8C2 /* XCRemoteSwiftPackageReference "typesense-swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/typesense/typesense-swift";
requirement = {
branch = master;
kind = branch;
};
};
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
Expand All @@ -404,6 +416,11 @@
package = 97613B552D06512900220031 /* XCRemoteSwiftPackageReference "YouTubePlayerKit" */;
productName = YouTubePlayerKit;
};
97AAF2542FFF19EC009DB8C2 /* Typesense */ = {
isa = XCSwiftPackageProductDependency;
package = 97AAF2532FFF19EC009DB8C2 /* XCRemoteSwiftPackageReference "typesense-swift" */;
productName = Typesense;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97613B0E2D0634F500220031 /* Project object */;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
59 changes: 46 additions & 13 deletions RubyEvents/controllers/TabBarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import HotwireNative
import SwiftUI
import UIKit

struct TabBarConfigurationItem {
Expand All @@ -24,6 +25,7 @@ class TabBarController: UITabBarController {
var configuration: TabBarConfiguration?
var navigators: [Navigator] = []
var isSetup: Bool = false
private var searchPlaceholder: UIViewController?

init(app: App) {
self.app = app
Expand Down Expand Up @@ -110,12 +112,6 @@ class TabBarController: UITabBarController {
icon: "music.mic",
url: Router.instance.talks_url(),
position: 2
),
TabBarConfigurationItem(
title: "Speakers",
icon: "person.fill",
url: Router.instance.speakers_url(),
position: 3
)
]
)
Expand All @@ -138,17 +134,26 @@ class TabBarController: UITabBarController {
tabBarItems.append(fixedTabBarItemFrom(item: item))
}

viewControllers = navigators.map(\.rootViewController)
var controllers: [UIViewController] = navigators.map(\.rootViewController)

tabBarItems.enumerated().forEach { index, item in
if let viewController = self.viewControllers?[index] {
// viewController.tabBarController?.tabBar.tintColor = .white
// viewController.tabBarController?.tabBar.barTintColor = .black
viewController.tabBarItem = item
if index < controllers.count {
controllers[index].tabBarItem = item
}
}

// NavBar styling
let searchController = UIViewController()
searchController.tabBarItem = UITabBarItem(
title: "Search",
image: UIImage(systemName: "magnifyingglass"),
tag: controllers.count
)
self.searchPlaceholder = searchController
controllers.append(searchController)

viewControllers = controllers
self.delegate = self

let navigationBarAppearance = UINavigationBarAppearance()
navigationBarAppearance.configureWithDefaultBackground()
UINavigationBar.appearance().standardAppearance = navigationBarAppearance
Expand All @@ -157,7 +162,6 @@ class TabBarController: UITabBarController {
UINavigationBar.appearance().isTranslucent = true
navigationBarAppearance.titleTextAttributes = [.foregroundColor: UIColor.black]

// TabBar styling
let tabBarAppearance = UITabBarAppearance()
tabBarAppearance.configureWithDefaultBackground()
tabBarAppearance.backgroundColor = UIColor.white
Expand All @@ -182,4 +186,33 @@ class TabBarController: UITabBarController {
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
}

private func presentSearch() {
let searchView = SearchView(
navigator: currentNavigator,
onDismiss: { [weak self] in self?.dismiss(animated: true) }
)

let hosting = UIHostingController(rootView: searchView)
hosting.modalPresentationStyle = .pageSheet
if let sheet = hosting.sheetPresentationController {
sheet.detents = [.large()]
sheet.prefersGrabberVisible = true
}
present(hosting, animated: true)
}
}

extension TabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if viewController === searchPlaceholder {
presentSearch()
return false
}
return true
}
}
139 changes: 139 additions & 0 deletions RubyEvents/search/SearchModels.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//
// SearchModels.swift
// RubyEvents
//

import Foundation

// Config from /hotwire/native/v1/search_config.json

struct SearchConfig: Decodable {
struct Node: Decodable {
let host: String
let port: Int
let `protocol`: String
}

struct CollectionConfig: Decodable {
let collection: String
let query_by: String
let query_by_weights: String?
let filter_by: String?
}

let nodes: [Node]
let nearest_node: Node?
let search_api_key: String?
let per_page: Int?
let talks: CollectionConfig
let speakers: CollectionConfig
let events: CollectionConfig
}

struct SearchTalk: Codable {
let id: String?
let title: String
let slug: String
let thumbnail_url: String?
let duration_in_seconds: Int32?
let video_provider: String?
let video_id: String?
let event_name: String?
let speakers: [SearchDocSpeaker]?

struct SearchDocSpeaker: Codable {
let id: Int64?
let name: String
let slug: String
}

private var derivedVideoURL: String? {
switch video_provider {
case "mp4": return video_id
case "youtube": return video_id.map { "https://www.youtube.com/watch?v=\($0)" }
case "vimeo": return video_id.map { "https://vimeo.com/video/\($0)" }
default: return nil
}
}

func toTalk() -> Talk {
Talk(
id: Int64(id ?? "") ?? 0,
title: title,
speakers: (speakers ?? []).map { Speaker(id: $0.id ?? 0, name: $0.name, slug: $0.slug, avatar_url: nil) },
duration_in_seconds: duration_in_seconds,
event_name: event_name ?? "",
url: Router.instance.root_url().appendingPathComponent("/talks/\(slug)"),
thumbnail_url: thumbnail_url.flatMap { URL(string: $0) },
slug: slug
// TODO: native player — add when the player branch's Talk model is integrated:
// , video_provider: video_provider, video_id: video_id, video_url: derivedVideoURL
)
}
}

struct SearchSpeaker: Codable {
let id: String?
let name: String
let slug: String
let avatar_url: String?
let github_handle: String?

var avatarURL: URL? { avatar_url.flatMap { URL(string: $0) } }
var profileURL: URL { Router.instance.speaker_url(slug: slug) }
}

struct SearchEvent: Codable {
let id: String?
let name: String
let slug: String
let city: String?
let country_name: String?
let avatar_url: String?
let start_date_timestamp: Int64?
let end_date_timestamp: Int64?

var eventURL: URL { Router.instance.root_url().appendingPathComponent("/events/\(slug)") }

var avatarURL: URL? {
guard let avatar_url, !avatar_url.isEmpty else { return nil }
if avatar_url.hasPrefix("http") { return URL(string: avatar_url) }

return URL(string: Router.instance.root_url().appendingPathComponent(avatar_url).absoluteString)
}

var locationText: String? {
let parts = [city, country_name].compactMap { $0 }.filter { !$0.isEmpty }

return parts.isEmpty ? nil : parts.joined(separator: ", ")
}

var dateText: String? {
guard let start = start_date_timestamp, start > 0 else { return nil }
let startDate = Date(timeIntervalSince1970: TimeInterval(start))

let full = DateFormatter()
full.dateFormat = "MMM d, yyyy"

if let end = end_date_timestamp, end > 0 {
let endDate = Date(timeIntervalSince1970: TimeInterval(end))

if Calendar.current.isDate(startDate, inSameDayAs: endDate) {
return full.string(from: startDate)
}

let short = DateFormatter()
short.dateFormat = "MMM d"

return "\(short.string(from: startDate)) – \(full.string(from: endDate))"
}

return full.string(from: startDate)
}

var subtitle: String? {
[dateText, locationText].compactMap { $0 }.first.flatMap { _ in
[dateText, locationText].compactMap { $0 }.joined(separator: " · ")
}
}
}
Loading