Untitled

Anonymous
plain_text
04/15/2026 3:26 PM
24.3 KB
13
Indexable
import SwiftUI

// ── Design Tokens ──────────────────────────────────
private extension Color {
    static let bg        = Color(hex: "#0E0E12")
    static let surface   = Color(hex: "#17171F")
    static let surface2  = Color(hex: "#1E1E28")
    static let border    = Color.white.opacity(0.07)
    static let accent    = Color(hex: "#C8F04A")
    static let accent2   = Color(hex: "#7C6FFF")
    static let textPrimary = Color(hex: "#F0EFE8")
    static let muted     = Color(hex: "#6E6E7E")
    static let danger    = Color(hex: "#FF6B6B")
    static let warn      = Color(hex: "#FFC947")
}

// ── Model ──────────────────────────────────────────
struct TaskItem: Identifiable {
    let id   = UUID()
    let name: String
    let subject: String
    let tag: String
    let tagStyle: TagStyle
    var isDone: Bool
    let date: Date

    enum TagStyle { case priority, done, study, late }
}

private extension TaskItem.TagStyle {
    var color: Color {
        switch self {
        case .priority: return .warn
        case .done:     return .muted
        case .study:    return .accent2
        case .late:     return .danger
        }
    }
    var bgColor: Color { color.opacity(0.12) }
}

// ── Main View ──────────────────────────────────────
struct FocusDashboardView: View {
    private let calendar = Calendar.current
    @State private var isShowingAddTask = false
    @State private var selectedDate     = Date()
    @State private var expandedTask: String? = nil
    @State private var taskEntries: [TaskEntry] = [
        TaskEntry(id: "t1", title: "Write essay introduction", subject: "English",
                  dueLabel: "Due today", dotColor: Color(hex: "#C8F04A"),
                  subtasks: [
                    SubTask(name: "Outline introduction", detail: "15 min", isDone: false),
                    SubTask(name: "Write body paragraphs", detail: "45 min", isDone: false),
                  ]),
        TaskEntry(id: "t2", title: "Read Chapter 4", subject: "Biology",
                  dueLabel: "Due today", dotColor: Color(hex: "#FF6B6B"),
                  subtasks: [
                    SubTask(name: "Read chapter",  detail: "20 min", isDone: true),
                    SubTask(name: "Take notes",    detail: "15 min", isDone: true),
                  ]),
    ]
    
    // completedCount — update to:
    private var completedCount: Int {
        taskEntries.filter(\.isDone).count
    }

    // focusStreak — update to:
    private var focusStreak: Int {
        var streak = 0
        var checkDate = calendar.startOfDay(for: Date())

        while true {
            let dayEntries = taskEntries.filter { calendar.isDate($0.date, inSameDayAs: checkDate) }
            guard dayEntries.contains(where: \.isDone) else { break }
            streak += 1
            checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate) ?? checkDate
        }
        return streak
    }
    private var titleText: String {
        calendar.isDateInToday(selectedDate) ? "Today's Tasks"
        : "Tasks for \(selectedDate.formatAsDayName())"
    }
    
    private var focusStreak: Int {
        var streak = 0
        var checkDate = calendar.startOfDay(for: Date())
        
        while true {
            let dayTasks = allTasks.filter { calendar.isDate($0.date, inSameDayAs: checkDate) }
            guard dayTasks.contains(where: \.isDone) else { break }
            streak += 1
            checkDate = calendar.date(byAdding: .day, value: -1, to: checkDate) ?? checkDate
        }
        return streak
    }
    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            Color.bg.ignoresSafeArea()

            ScrollView {
                VStack(alignment: .leading, spacing: 16) {
                    PageHeaderView()
                    WeekStripView(selectedDate: $selectedDate, tasks: allTasks)
                    streakCard
                    tasksCard
                    statsCard
                    Spacer(minLength: 32)
                }
                .padding(.horizontal, 16)
                .padding(.bottom, 100)
            }

            // FAB Add Task button
            Button {
                isShowingAddTask = true
            } label: {
                HStack(spacing: 6) {
                    Image(systemName: "plus")
                        .font(.system(size: 14, weight: .bold))
                    Text("Add Task")
                        .font(.system(size: 14, weight: .bold))
                }
                .foregroundStyle(Color(hex: "#0E0E12"))
                .padding(.horizontal, 20)
                .padding(.vertical, 14)
                .background(Color.accent)
                .clipShape(Capsule())
                .shadow(color: Color.accent.opacity(0.35), radius: 16, y: 6)
            }
            .padding(.trailing, 20)
            .padding(.bottom, 32)
        }
        .sheet(isPresented: $isShowingAddTask) {
            AddTaskView { newTask in allTasks.append(newTask) }
        }
    }

    // ── Streak Card ──
    private var streakCard: some View {
        HStack(spacing: 10) {
            Text("🔥").font(.title3)
            if focusStreak == 0 {
                Text("Complete a task today to start your streak!")
                    .foregroundStyle(Color.textPrimary)
                    .font(.subheadline)
            } else {
                (Text("You're on a ")
                    .foregroundStyle(Color.textPrimary)
                 + Text("\(focusStreak)-day").fontWeight(.bold).foregroundStyle(Color.accent)
                 + Text(focusStreak == 1 ? " focus streak. Keep going!" : " focus streak. Keep going!")
                    .foregroundStyle(Color.textPrimary))
                    .font(.subheadline)
            }
        }
        .padding(14)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(focusStreak > 0 ? Color.accent.opacity(0.06) : Color.gray.opacity(0.06))
        .overlay(
            RoundedRectangle(cornerRadius: 12, style: .continuous)
                .strokeBorder(
                    focusStreak > 0 ? Color.accent.opacity(0.15) : Color.gray.opacity(0.15),
                    lineWidth: 0.5
                )
        )
        .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
    }
    // ── Tasks Card ──
    private var tasksCard: some View {
        VStack(alignment: .leading, spacing: 14) {
            Text(titleText)
                .font(.system(size: 11, weight: .semibold))
                .tracking(1.0)
                .textCase(.uppercase)
                .foregroundStyle(Color.muted)

            if taskEntries.isEmpty {
                Text("No tasks for this day.")
                    .font(.callout)
                    .foregroundStyle(Color.muted)
                    .padding(.vertical, 10)
            } else {
                VStack(spacing: 10) {
                    ForEach($taskEntries) { $entry in
                        TaskItemView(task: $entry, expandedTask: $expandedTask)
                    }
                }
            }
        }
        .padding(18)
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color.surface)
        .overlay(
            RoundedRectangle(cornerRadius: 16, style: .continuous)
                .strokeBorder(Color.border, lineWidth: 0.5)
        )
        .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
    }

    // ── Stats Card ──
    private var statsCard: some View {
        HStack(spacing: 0) {
            StatCell(value: "\(filteredTasks.count)", label: "Tasks")
            Color.border.frame(width: 0.5, height: 40)
            StatCell(value: "\(completedCount)",                          label: "Completed")
            Color.border.frame(width: 0.5, height: 40)
            StatCell(value: "\(max(filteredTasks.count - completedCount, 0))", label: "Remaining")
        }
        .padding(18)
        .frame(maxWidth: .infinity)
        .background(Color.surface)
        .overlay(
            RoundedRectangle(cornerRadius: 16, style: .continuous)
                .strokeBorder(Color.border, lineWidth: 0.5)
        )
        .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
    }
}

// ── Page Header ───────────────────────────────────
struct PageHeaderView: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text("Track your progress")
                .font(.system(size: 12))
                .tracking(0.5)
                .foregroundStyle(Color.muted)

            HStack(spacing: 4) {
                Text("Task")
                    .font(.system(size: 28, weight: .bold))
                    .foregroundStyle(Color.textPrimary)
                Text("calendar")
                    .font(.system(size: 28, weight: .light))
                    .foregroundStyle(Color.accent)
            }
        }
        .padding(.top, 16)
    }
}

// ── Week Strip ────────────────────────────────────
struct WeekStripView: View {
    @Binding var selectedDate: Date
    let tasks: [TaskItem]

    private let calendar   = Calendar.current
    private let today      = Date()
    private let daysRange  = -3...6

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 8) {
                ForEach(Array(daysRange), id: \.self) { offset in
                    let date = calendar.date(byAdding: .day, value: offset, to: today) ?? today
                    let isToday    = calendar.isDateInToday(date)
                    let isSelected = calendar.isDate(date, inSameDayAs: selectedDate)
                    let hasTask    = tasks.contains { calendar.isDate($0.date, inSameDayAs: date) }

                    DayChip(
                        dayNum: calendar.component(.day, from: date),
                        dayName: date.formatAsDayName(),
                        hasTask: hasTask,
                        isToday: isToday,
                        isSelected: isSelected
                    )
                    .onTapGesture { selectedDate = date }
                }
            }
            .padding(.vertical, 4)
        }
    }
}

struct DayChip: View {
    let dayNum: Int
    let dayName: String
    let hasTask: Bool
    let isToday: Bool
    let isSelected: Bool

    var body: some View {
        VStack(spacing: 5) {
            Text("\(dayNum)")
                .font(.system(size: 16, weight: .bold))
            Text(dayName)
                .font(.system(size: 10))
                .tracking(0.5)
                .textCase(.uppercase)
            Circle()
                .fill(hasTask
                      ? (isToday ? Color(hex: "#0E0E12") : Color.accent2)
                      : Color.clear)
                .frame(width: 4, height: 4)
        }
        .frame(width: 48, height: 72)
        .foregroundStyle(isToday ? Color(hex: "#0E0E12") : (isSelected ? Color.textPrimary : Color.muted))
        .background(isToday ? Color.accent : (isSelected ? Color.surface2 : Color.surface2))
        .overlay(
            RoundedRectangle(cornerRadius: 10, style: .continuous)
                .strokeBorder(
                    isSelected && !isToday ? Color.accent.opacity(0.4) : Color.border,
                    lineWidth: 0.5
                )
        )
        .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
    }
}

// ── Task Row ──────────────────────────────────────
struct TaskRow: View {
    let task: TaskItem
    var onToggle: () -> Void = {}

    var body: some View {
        HStack(spacing: 12) {
            // Check circle
            ZStack {
                Circle()
                    .strokeBorder(
                        task.isDone ? Color.accent : Color.border,
                        lineWidth: 1.5
                    )
                    .frame(width: 22, height: 22)
                if task.isDone {
                    Image(systemName: "checkmark")
                        .font(.system(size: 10, weight: .bold))
                        .foregroundStyle(Color(hex: "#0E0E12"))
                        .frame(width: 22, height: 22)
                        .background(Color.accent)
                        .clipShape(Circle())
                }
            }

            VStack(alignment: .leading, spacing: 2) {
                Text(task.name)
                    .font(.system(size: 14, weight: .medium))
                    .strikethrough(task.isDone, color: Color.muted)
                    .foregroundStyle(task.isDone ? Color.muted : Color.textPrimary)
                Text(task.subject)
                    .font(.system(size: 12))
                    .foregroundStyle(Color.muted)
            }

            Spacer()

            Text(task.tag)
                .font(.system(size: 10, weight: .medium))
                .padding(.horizontal, 8)
                .padding(.vertical, 4)
                .background(task.tagStyle.bgColor)
                .foregroundStyle(task.tagStyle.color)
                .clipShape(Capsule())
        }
        .padding(.vertical, 2)
        .contentShape(Rectangle())       // makes full row tappable
        .onTapGesture { onToggle() }
    }
}

// ── Stat Cell ─────────────────────────────────────
struct StatCell: View {
    let value: String
    let label: String

    var body: some View {
        VStack(alignment: .center, spacing: 4) {
            Text(value)
                .font(.system(size: 26, weight: .bold))
                .foregroundStyle(Color.accent)
            Text(label)
                .font(.system(size: 11))
                .foregroundStyle(Color.muted)
        }
        .frame(maxWidth: .infinity)
    }
}

// ── Add Task Sheet ─────────────────────────────────
struct AddTaskView: View {
    @Environment(\.dismiss) var dismiss
    @State private var taskName = ""
    @State private var taskDate = Date()
    var onAdd: (TaskItem) -> Void

    var body: some View {
        ZStack {
            Color.bg.ignoresSafeArea()

            VStack(alignment: .leading, spacing: 24) {
                // Handle
                RoundedRectangle(cornerRadius: 2)
                    .fill(Color.border)
                    .frame(width: 36, height: 4)
                    .frame(maxWidth: .infinity)
                    .padding(.top, 12)

                Text("New Task")
                    .font(.system(size: 22, weight: .bold))
                    .foregroundStyle(Color.textPrimary)
                    .padding(.horizontal, 20)

                VStack(spacing: 12) {
                    // Task name field
                    VStack(alignment: .leading, spacing: 6) {
                        Text("TASK NAME")
                            .font(.system(size: 10, weight: .semibold))
                            .tracking(1)
                            .foregroundStyle(Color.muted)
                        TextField("", text: $taskName)
                            .font(.system(size: 15))
                            .foregroundStyle(Color.textPrimary)
                            .tint(Color.accent)
                            .padding(14)
                            .background(Color.surface2)
                            .overlay(
                                RoundedRectangle(cornerRadius: 10, style: .continuous)
                                    .strokeBorder(Color.border, lineWidth: 0.5)
                            )
                            .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
                    }

                    // Date picker
                    VStack(alignment: .leading, spacing: 6) {
                        Text("DATE")
                            .font(.system(size: 10, weight: .semibold))
                            .tracking(1)
                            .foregroundStyle(Color.muted)
                        DatePicker("", selection: $taskDate, displayedComponents: .date)
                            .datePickerStyle(.compact)
                            .tint(Color.accent)
                            .padding(14)
                            .background(Color.surface2)
                            .overlay(
                                RoundedRectangle(cornerRadius: 10, style: .continuous)
                                    .strokeBorder(Color.border, lineWidth: 0.5)
                            )
                            .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
                            .colorScheme(.dark)
                    }
                }
                .padding(.horizontal, 20)

                Spacer()

                // Add button
                Button {
                    let newTask = TaskItem(
                        name: taskName, subject: "General",
                        tag: "New", tagStyle: .study,
                        isDone: false, date: taskDate
                    )
                    onAdd(newTask)
                    dismiss()
                } label: {
                    Text("Add Task")
                        .font(.system(size: 15, weight: .bold))
                        .foregroundStyle(Color(hex: "#0E0E12"))
                        .frame(maxWidth: .infinity)
                        .padding(.vertical, 16)
                        .background(taskName.isEmpty ? Color.accent.opacity(0.3) : Color.accent)
                        .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
                }
                .disabled(taskName.isEmpty)
                .padding(.horizontal, 20)
                .padding(.bottom, 40)
            }
        }
        .presentationDetents([.medium, .large])
        .presentationDragIndicator(.hidden)
    }
}

// ── Helpers ───────────────────────────────────────
private extension Date {
    func formatAsDayName() -> String {
        formatted(.dateTime.weekday(.abbreviated))
    }
}

#Preview {
    FocusDashboardView()
}
Editor is loading...
Leave a Comment