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