Initial commit: nightly iOS app + Supabase backend
iOS SwiftUI app with Supabase auth/realtime, Node.js backend, Docker/Supabase self-hosted infrastructure, and APNs scheduler. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainTabView: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
@StateObject private var realtime = RealtimeService()
|
||||
@State private var selectedTab = 0
|
||||
@State private var showCompose = false
|
||||
@State private var showSettings = false
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottom) {
|
||||
Color.nightBase.ignoresSafeArea()
|
||||
|
||||
// Content
|
||||
TabContent(
|
||||
selectedTab: selectedTab,
|
||||
realtime: realtime
|
||||
)
|
||||
.environmentObject(appState)
|
||||
|
||||
// Floating Tab Bar
|
||||
FloatingTabBar(
|
||||
selectedTab: $selectedTab,
|
||||
windowState: appState.windowState,
|
||||
onCompose: { showCompose = true },
|
||||
onSettings: { showSettings = true }
|
||||
)
|
||||
}
|
||||
.ignoresSafeArea(edges: .bottom)
|
||||
.sheet(isPresented: $showCompose) {
|
||||
ComposeView().environmentObject(appState)
|
||||
}
|
||||
.sheet(isPresented: $showSettings) {
|
||||
SettingsView().environmentObject(appState)
|
||||
}
|
||||
.onDisappear {
|
||||
Task { await realtime.stopListening() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Tab Content
|
||||
|
||||
private struct TabContent: View {
|
||||
let selectedTab: Int
|
||||
@ObservedObject var realtime: RealtimeService
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
FeedView(realtime: realtime)
|
||||
.environmentObject(appState)
|
||||
.opacity(selectedTab == 0 ? 1 : 0)
|
||||
.allowsHitTesting(selectedTab == 0)
|
||||
|
||||
DiaryView()
|
||||
.environmentObject(appState)
|
||||
.opacity(selectedTab == 1 ? 1 : 0)
|
||||
.allowsHitTesting(selectedTab == 1)
|
||||
|
||||
ProfileView(
|
||||
user: appState.currentUser ?? .preview,
|
||||
isCurrentUser: true
|
||||
)
|
||||
.environmentObject(appState)
|
||||
.opacity(selectedTab == 2 ? 1 : 0)
|
||||
.allowsHitTesting(selectedTab == 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Floating Tab Bar
|
||||
|
||||
struct FloatingTabBar: View {
|
||||
@Binding var selectedTab: Int
|
||||
let windowState: AppState.WindowState
|
||||
let onCompose: () -> Void
|
||||
let onSettings: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
// Feed
|
||||
TabIcon(icon: "moon.stars", activeIcon: "moon.stars.fill", isSelected: selectedTab == 0) {
|
||||
selectedTab = 0
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Diary
|
||||
TabIcon(icon: "book.closed", activeIcon: "book.closed.fill", isSelected: selectedTab == 1) {
|
||||
selectedTab = 1
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Center: Compose
|
||||
ComposeTabButton(windowState: windowState, onTap: onCompose)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Profile
|
||||
TabIcon(icon: "person", activeIcon: "person.fill", isSelected: selectedTab == 2) {
|
||||
selectedTab = 2
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Settings
|
||||
TabIcon(icon: "gearshape", activeIcon: "gearshape.fill", isSelected: false) {
|
||||
onSettings()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 10)
|
||||
.padding(.bottom, 18)
|
||||
.background(
|
||||
Rectangle()
|
||||
.fill(.ultraThinMaterial.opacity(0.8))
|
||||
.background(Color.nightBase.opacity(0.85))
|
||||
.ignoresSafeArea()
|
||||
)
|
||||
.overlay(
|
||||
Rectangle().fill(Color.nightBorder).frame(height: 1),
|
||||
alignment: .top
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct TabIcon: View {
|
||||
let icon: String
|
||||
let activeIcon: String
|
||||
let isSelected: Bool
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
Image(systemName: isSelected ? activeIcon : icon)
|
||||
.font(.system(size: 21))
|
||||
.foregroundColor(isSelected ? .nightPrimary : .nightSecondary)
|
||||
.frame(width: 44, height: 44)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ComposeTabButton: View {
|
||||
let windowState: AppState.WindowState
|
||||
let onTap: () -> Void
|
||||
@State private var glow = false
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
guard windowState == .open else { return }
|
||||
onTap()
|
||||
} label: {
|
||||
ZStack {
|
||||
if windowState == .open {
|
||||
Circle()
|
||||
.fill(Color.nightPurple.opacity(0.18))
|
||||
.frame(width: 62, height: 62)
|
||||
.scaleEffect(glow ? 1.15 : 1.0)
|
||||
.animation(.easeInOut(duration: 2.2).repeatForever(autoreverses: true), value: glow)
|
||||
}
|
||||
Circle()
|
||||
.fill(buttonFill)
|
||||
.frame(width: 50, height: 50)
|
||||
Image(systemName: buttonIcon)
|
||||
.font(.system(size: 19, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
.onAppear { glow = true }
|
||||
.animation(.easeInOut(duration: 0.4), value: windowState)
|
||||
}
|
||||
|
||||
var buttonFill: AnyShapeStyle {
|
||||
switch windowState {
|
||||
case .open:
|
||||
return AnyShapeStyle(LinearGradient(
|
||||
colors: [Color(hex: "8B5CF6"), Color(hex: "6D28D9")],
|
||||
startPoint: .topLeading, endPoint: .bottomTrailing
|
||||
))
|
||||
case .posted:
|
||||
return AnyShapeStyle(LinearGradient(
|
||||
colors: [Color(hex: "059669"), Color(hex: "047857")],
|
||||
startPoint: .top, endPoint: .bottom
|
||||
))
|
||||
default:
|
||||
return AnyShapeStyle(Color.nightRaised)
|
||||
}
|
||||
}
|
||||
|
||||
var buttonIcon: String {
|
||||
switch windowState {
|
||||
case .open: return "plus"
|
||||
case .posted: return "checkmark"
|
||||
default: return "moon.zzz"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user