398 lines
15 KiB
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)
|
|
}
|
|
} |