Closer/iphone/Closer/Dates/DateViews.swift

398 lines
15 KiB
Swift

import SwiftUI
// MARK: - Date Match (Swipe)
struct DateMatchView: View {
@State private var currentIndex = 0
@State private var offset: CGSize = .zero
@State private var showMatch = false
@State private var matched: DateIdea?
let dateIdeas: [DateIdea] = [
DateIdea(id: "1", title: "Sunset Picnic", description: "Pack a basket and watch the sunset together at your favorite spot.", category: "romance", cost: "low", duration: "medium", location: "outdoor"),
DateIdea(id: "2", title: "Cooking Challenge", description: "Pick a cuisine you've never tried and cook it together.", category: "fun", cost: "medium", duration: "long", location: "indoor"),
DateIdea(id: "3", title: "Board Game Night", description: "Pull out your favorite board games and make it a tournament.", category: "fun", cost: "free", duration: "medium", location: "indoor"),
DateIdea(id: "4", title: "Stargazing", description: "Find a dark spot, bring blankets, and watch the stars.", category: "romance", cost: "free", duration: "medium", location: "outdoor"),
DateIdea(id: "5", title: "Art Class Together", description: "Take a pottery or painting class as a couple.", category: "creative", cost: "medium", duration: "long", location: "indoor"),
]
var body: some View {
VStack(spacing: CloserSpacing.xl) {
if showMatch, let idea = matched {
VStack(spacing: CloserSpacing.lg) {
Image(systemName: "heart.fill")
.font(.system(size: 72))
.foregroundColor(.closerDanger)
Text("It's a Match!")
.font(CloserFont.title1)
.foregroundColor(.closerText)
Text(idea.title)
.font(CloserFont.title2)
.foregroundColor(.closerPrimary)
Text(idea.description ?? "")
.font(CloserFont.callout)
.foregroundColor(.closerTextSecondary)
.multilineTextAlignment(.center)
Button("Plan This Date") {
// Navigate to date builder
}
.buttonStyle(PrimaryButtonStyle())
Button("Keep Swiping") {
withAnimation { showMatch = false }
}
.buttonStyle(SecondaryButtonStyle())
}
} else {
// Card stack
ZStack {
ForEach(dateIdeas.indices, id: \.self) { index in
if index >= currentIndex && index < currentIndex + 3 {
DateSwipeCard(
idea: dateIdeas[index],
offset: index == currentIndex ? $offset : .constant(.zero),
isTop: index == currentIndex
)
.scaleEffect(index == currentIndex ? 1 : 1 - CGFloat(index - currentIndex) * 0.05)
.offset(y: index == currentIndex ? 0 : CGFloat(index - currentIndex) * 10)
}
}
}
.frame(height: 400)
// Action buttons
HStack(spacing: CloserSpacing.xxl) {
Button(action: { swipe(.left) }) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 56))
.foregroundColor(.closerDanger)
}
Button(action: { swipe(.right) }) {
Image(systemName: "heart.circle.fill")
.font(.system(size: 56))
.foregroundColor(.closerSuccess)
}
}
Text("Swipe right to match, left to pass")
.font(CloserFont.caption)
.foregroundColor(.closerTextSecondary)
}
}
.closerPadding()
.background(Color.closerBackground)
.navigationTitle("Date Ideas")
.navigationBarTitleDisplayMode(.inline)
}
private func swipe(_ direction: SwipeDirection) {
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
offset = direction == .right ? CGSize(width: 500, height: 0) : CGSize(width: -500, height: 0)
}
if direction == .right {
matched = dateIdeas[currentIndex]
showMatch = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
offset = .zero
currentIndex += 1
if currentIndex >= dateIdeas.count {
currentIndex = 0
}
}
}
enum SwipeDirection {
case left, right
}
}
// MARK: - Date Swipe Card
struct DateSwipeCard: View {
let idea: DateIdea
@Binding var offset: CGSize
let isTop: Bool
var body: some View {
VStack(alignment: .leading, spacing: CloserSpacing.md) {
// Icon area
ZStack {
RoundedRectangle(cornerRadius: CloserRadius.large)
.fill(Color.closerPrimary.opacity(0.1))
.frame(height: 200)
Image(systemName: iconForDate(idea))
.font(.system(size: 60))
.foregroundColor(.closerPrimary)
}
VStack(alignment: .leading, spacing: CloserSpacing.sm) {
Text(idea.title)
.font(CloserFont.title2)
.foregroundColor(.closerText)
Text(idea.description ?? "")
.font(CloserFont.callout)
.foregroundColor(.closerTextSecondary)
.lineLimit(3)
HStack(spacing: CloserSpacing.md) {
TagView(text: idea.cost ?? "")
TagView(text: idea.location ?? "")
TagView(text: idea.duration ?? "")
}
}
.padding(CloserSpacing.md)
}
.closerCard()
.offset(isTop ? offset : .zero)
.rotationEffect(isTop ? .degrees(Double(offset.width / 20)) : .zero)
.gesture(isTop ? DragGesture()
.onChanged { value in offset = value.translation }
.onEnded { _ in
if abs(offset.width) > 120 {
// Let swipe handler in parent manage this
} else {
withAnimation(.spring()) { offset = .zero }
}
}
: nil)
}
private func iconForDate(_ idea: DateIdea) -> String {
switch idea.category {
case "romance": return "heart.fill"
case "fun": return "gamecontroller.fill"
case "creative": return "paintbrush.fill"
case "adventure": return "paperplane.fill"
default: return "star.fill"
}
}
}
// MARK: - Date Matches
struct DateMatchesView: View {
@State private var matches: [DateIdea] = []
@State private var isLoading = true
var body: some View {
List {
if isLoading {
ProgressView()
.frame(maxWidth: .infinity)
} else if matches.isEmpty {
EmptyStateView(
icon: "heart.slash",
title: "No Matches Yet",
message: "Swipe on date ideas to find mutual matches with your partner!",
action: (title: "Browse Ideas", handler: {})
)
.listRowBackground(Color.clear)
} else {
ForEach(matches, id: \.id) { match in
HStack(spacing: CloserSpacing.md) {
Image(systemName: "heart.fill")
.foregroundColor(.closerDanger)
VStack(alignment: .leading, spacing: 4) {
Text(match.title)
.font(CloserFont.body)
Text(match.description ?? "")
.font(CloserFont.caption)
.foregroundColor(.closerTextSecondary)
.lineLimit(2)
}
}
.padding(.vertical, 4)
}
}
}
.listStyle(.insetGrouped)
.background(Color.closerBackground)
.navigationTitle("Date Matches")
.navigationBarTitleDisplayMode(.inline)
.task {
try? await Task.sleep(nanoseconds: 500_000_000)
isLoading = false
}
}
}
// MARK: - Date Builder
struct DateBuilderView: View {
@State private var dateTitle = ""
@State private var dateDescription = ""
@State private var dateLocation = ""
@State private var selectedDate = Date()
@State private var isLoading = false
var body: some View {
Form {
Section("Date Details") {
TextField("Title", text: $dateTitle)
TextField("Description (optional)", text: $dateDescription, axis: .vertical)
.lineLimit(3)
TextField("Location (optional)", text: $dateLocation)
}
Section("Date & Time") {
DatePicker("Date", selection: $selectedDate, displayedComponents: [.date, .hourAndMinute])
}
Section {
Button(action: savePlan) {
if isLoading {
ProgressView()
} else {
Text("Save Date Plan")
}
}
.disabled(dateTitle.isEmpty || isLoading)
.frame(maxWidth: .infinity)
}
}
.background(Color.closerBackground)
.navigationTitle("Plan a Date")
.navigationBarTitleDisplayMode(.inline)
}
private func savePlan() {
isLoading = true
Task {
try? await Task.sleep(nanoseconds: 500_000_000)
isLoading = false
}
}
}
// MARK: - Bucket List
struct BucketListView: View {
@State private var items: [BucketListItem] = []
@State private var isLoading = true
@State private var showAdd = false
@State private var newItemTitle = ""
@State private var newItemDescription = ""
var body: some View {
List {
if isLoading {
ProgressView()
.frame(maxWidth: .infinity)
} else if items.isEmpty {
EmptyStateView(
icon: "list.bullet",
title: "Your Bucket List is Empty",
message: "Add things you want to do together as a couple!",
action: (title: "Add Item", handler: { showAdd = true })
)
.listRowBackground(Color.clear)
} else {
ForEach(items) { item in
HStack {
Image(systemName: item.completed ? "checkmark.circle.fill" : "circle")
.foregroundColor(item.completed ? .closerSuccess : .closerDivider)
.onTapGesture {
toggleItem(item)
}
VStack(alignment: .leading, spacing: 4) {
Text(item.title)
.font(CloserFont.body)
.strikethrough(item.completed)
.foregroundColor(item.completed ? .closerTextSecondary : .closerText)
if let desc = item.description {
Text(desc)
.font(CloserFont.caption)
.foregroundColor(.closerTextSecondary)
}
}
}
.padding(.vertical, 4)
}
}
}
.listStyle(.insetGrouped)
.background(Color.closerBackground)
.navigationTitle("Our Bucket List")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: { showAdd = true }) {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $showAdd) {
NavigationStack {
Form {
TextField("Title", text: $newItemTitle)
TextField("Description (optional)", text: $newItemDescription, axis: .vertical)
.lineLimit(3)
}
.navigationTitle("Add Item")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") { showAdd = false }
}
ToolbarItem(placement: .confirmationAction) {
Button("Add") { addItem() }
.disabled(newItemTitle.isEmpty)
}
}
}
}
.task {
try? await Task.sleep(nanoseconds: 500_000_000)
isLoading = false
}
}
private func addItem() {
let item = BucketListItem(
id: UUID().uuidString,
title: newItemTitle,
description: newItemDescription.isEmpty ? nil : newItemDescription,
createdBy: AuthService.shared.currentUserId ?? "",
completed: false,
completedAt: nil,
createdAt: Date()
)
items.append(item)
newItemTitle = ""
newItemDescription = ""
showAdd = false
}
private func toggleItem(_ item: BucketListItem) {
if let index = items.firstIndex(where: { $0.id == item.id }) {
items[index].completed.toggle()
items[index].completedAt = items[index].completed ? Date() : nil
}
}
}
// MARK: - Helper Views
struct TagView: View {
let text: String
var body: some View {
Text(text.capitalized)
.font(CloserFont.caption)
.foregroundColor(.closerTextSecondary)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.closerDivider.opacity(0.5))
.cornerRadius(CloserRadius.full)
}
}