Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions ProductivityApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
36A3171C2F5FB97C00070F06 /* LRStreakKit in Frameworks */ = {isa = PBXBuildFile; productRef = 36A3171B2F5FB97C00070F06 /* LRStreakKit */; };
84F314692F286D2000D83064 /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = 84F314682F286D2000D83064 /* GoogleSignIn */; };
84F3146B2F286D2000D83064 /* GoogleSignInSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 84F3146A2F286D2000D83064 /* GoogleSignInSwift */; };
84FEE9EB2F3AA45600DBB49F /* AuthenticationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F314602F286C6800D83064 /* AuthenticationManager.swift */; };
Expand All @@ -21,7 +22,6 @@
84FEEA122F3AE63100DBB49F /* DashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FEEA112F3AE63100DBB49F /* DashboardView.swift */; };
84FEEA142F3AE63300DBB49F /* ShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FEEA132F3AE63300DBB49F /* ShareView.swift */; };
84FEEA262F460D3900DBB49F /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FEEA252F460D3900DBB49F /* SettingsView.swift */; };
84FEEA322F460E8D00DBB49F /* DashboardView 2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FEEA312F460E8D00DBB49F /* DashboardView 2.swift */; };
84FEEA3C2F46110900DBB49F /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FEEA3B2F46110900DBB49F /* RootView.swift */; };
84FEEA3E2F46114200DBB49F /* EmailSignInView 2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FEEA3D2F46114200DBB49F /* EmailSignInView 2.swift */; };
84FEEA402F46114500DBB49F /* EmailSignUpView 2.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FEEA3F2F46114500DBB49F /* EmailSignUpView 2.swift */; };
Expand All @@ -42,7 +42,6 @@
84FEEA112F3AE63100DBB49F /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = "<group>"; };
84FEEA132F3AE63300DBB49F /* ShareView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareView.swift; sourceTree = "<group>"; };
84FEEA252F460D3900DBB49F /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
84FEEA312F460E8D00DBB49F /* DashboardView 2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DashboardView 2.swift"; sourceTree = "<group>"; };
84FEEA3B2F46110900DBB49F /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
84FEEA3D2F46114200DBB49F /* EmailSignInView 2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmailSignInView 2.swift"; sourceTree = "<group>"; };
84FEEA3F2F46114500DBB49F /* EmailSignUpView 2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmailSignUpView 2.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -77,6 +76,7 @@
files = (
84F314692F286D2000D83064 /* GoogleSignIn in Frameworks */,
84F3146B2F286D2000D83064 /* GoogleSignInSwift in Frameworks */,
36A3171C2F5FB97C00070F06 /* LRStreakKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -131,7 +131,6 @@
84FEEA112F3AE63100DBB49F /* DashboardView.swift */,
84FEEA132F3AE63300DBB49F /* ShareView.swift */,
84FEEA252F460D3900DBB49F /* SettingsView.swift */,
84FEEA312F460E8D00DBB49F /* DashboardView 2.swift */,
84FEEA3B2F46110900DBB49F /* RootView.swift */,
84FEEA3D2F46114200DBB49F /* EmailSignInView 2.swift */,
84FEEA3F2F46114500DBB49F /* EmailSignUpView 2.swift */,
Expand Down Expand Up @@ -162,6 +161,7 @@
packageProductDependencies = (
84F314682F286D2000D83064 /* GoogleSignIn */,
84F3146A2F286D2000D83064 /* GoogleSignInSwift */,
36A3171B2F5FB97C00070F06 /* LRStreakKit */,
);
productName = ProductivityApp;
productReference = 364323802F21D77600D6F8A5 /* ProductivityApp.app */;
Expand Down Expand Up @@ -193,6 +193,7 @@
minimizedProjectReferenceProxies = 1;
packageReferences = (
84F314672F286D2000D83064 /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */,
36A3171A2F5FB97C00070F06 /* XCRemoteSwiftPackageReference "LRStreakKit" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 364323812F21D77600D6F8A5 /* Products */;
Expand Down Expand Up @@ -223,7 +224,6 @@
84FEEA122F3AE63100DBB49F /* DashboardView.swift in Sources */,
84FEE9EC2F3AA46C00DBB49F /* User.swift in Sources */,
84FEE9EB2F3AA45600DBB49F /* AuthenticationManager.swift in Sources */,
84FEEA322F460E8D00DBB49F /* DashboardView 2.swift in Sources */,
84FEE9EE2F3AA47D00DBB49F /* EmailSignInView.swift in Sources */,
84FEE9F02F3AA48600DBB49F /* ProfileView.swift in Sources */,
84FEEA4A2F46134400DBB49F /* ShareManager.swift in Sources */,
Expand Down Expand Up @@ -449,6 +449,14 @@
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
36A3171A2F5FB97C00070F06 /* XCRemoteSwiftPackageReference "LRStreakKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/lukerobertsapps/LRStreakKit";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.1.0;
};
};
84F314672F286D2000D83064 /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/google/GoogleSignIn-iOS";
Expand All @@ -460,6 +468,11 @@
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
36A3171B2F5FB97C00070F06 /* LRStreakKit */ = {
isa = XCSwiftPackageProductDependency;
package = 36A3171A2F5FB97C00070F06 /* XCRemoteSwiftPackageReference "LRStreakKit" */;
productName = LRStreakKit;
};
84F314682F286D2000D83064 /* GoogleSignIn */ = {
isa = XCSwiftPackageProductDependency;
package = 84F314672F286D2000D83064 /* XCRemoteSwiftPackageReference "GoogleSignIn-iOS" */;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>ProductivityApp.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>
12 changes: 2 additions & 10 deletions ProductivityApp/ContentView.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
//
// ContentView.swift
// ProductivityApp
//
// Created by Ava Kaplin on 1/21/26.
//

import SwiftUI

struct ContentView: View {
Expand All @@ -13,14 +6,13 @@ struct ContentView: View {

var body: some View {
Group {
// 2. 判斷目前是否有使用者登入
// 判斷目前是否有使用者登入
if authManager.currentUser != nil {
// 已登入:前往主畫面
ProfileView()
} else {
// 未登入:前往登入畫面
AuthenticationView()
}
}
}
}

227 changes: 227 additions & 0 deletions ProductivityApp/HomePage/DeadlineView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
//
// DeadlineView.swift
// ProductivityApp
//
//

import SwiftUI

private struct DeadlineItem: Identifiable {
let id = UUID()
let title: String
let dueDate: Date?
let color: Color
}

struct DeadlineView: View {
@State private var isExpanded = false
@State private var newTitle = ""
@State private var newDue = ""
@State private var deadlines: [DeadlineItem] = [
DeadlineItem(title: "Math Homework", dueDate: Calendar.current.date(byAdding: .day, value: 1, to: Date()), color: .orange),
DeadlineItem(title: "History Essay", dueDate: Self.nextWeekday(6), color: .pink), // Friday
DeadlineItem(title: "Chemistry Quiz", dueDate: Self.nextWeekday(2), color: .mint) // Monday
]

private var sortedDeadlines: [DeadlineItem] {
deadlines.sorted { lhs, rhs in
switch (lhs.dueDate, rhs.dueDate) {
case let (.some(l), .some(r)):
return l < r
case (.some, .none):
return true
case (.none, .some):
return false
case (.none, .none):
return lhs.title.localizedCaseInsensitiveCompare(rhs.title) == .orderedAscending
}
}
}

private var visibleDeadlines: [DeadlineItem] {
isExpanded ? sortedDeadlines : Array(sortedDeadlines.prefix(2))
}

var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Upcoming Deadlines")
.font(.title3.weight(.semibold))

Spacer()

Button {
withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
isExpanded.toggle()
}
} label: {
Image(systemName: isExpanded ? "xmark" : "plus")
.font(.system(size: 16, weight: .semibold))
.frame(width: 36, height: 36)
.background(Color(.systemTeal))
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
}
.buttonStyle(.plain)
}

ForEach(visibleDeadlines) { item in
HStack(spacing: 12) {
Circle()
.fill(item.color)
.frame(width: 10, height: 10)

VStack(alignment: .leading, spacing: 2) {
Text(item.title)
.font(.system(size: 16, weight: .semibold))
Text(Self.dueText(for: item.dueDate))
.font(.footnote)
.foregroundStyle(Color(.secondaryLabel))
}

Spacer()

Button {
deadlines.removeAll { $0.id == item.id }
} label: {
Image(systemName: "trash")
.font(.system(size: 14, weight: .medium))
.foregroundStyle(Color(.secondaryLabel))
}
.buttonStyle(.plain)
}
.padding(.vertical, 11)
.padding(.horizontal, 12)
.background(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(Color(red: 0.97, green: 0.95, blue: 0.90))
)
.shadow(color: .clear, radius: 0)
}

if isExpanded {
VStack(spacing: 10) {
TextField("Task name", text: $newTitle)
.textFieldStyle(.roundedBorder)

TextField("Due date", text: $newDue)
.textFieldStyle(.roundedBorder)

Button {
let title = newTitle.trimmingCharacters(in: .whitespacesAndNewlines)
let due = newDue.trimmingCharacters(in: .whitespacesAndNewlines)
guard !title.isEmpty else { return }
deadlines.insert(
DeadlineItem(
title: title,
dueDate: Self.parseDueDate(from: due),
color: .blue
)
, at: 0
)
newTitle = ""
newDue = ""
} label: {
Text("Add Deadline")
.font(.subheadline.weight(.semibold))
.frame(maxWidth: .infinity)
.padding(.vertical, 11)
.background(Color(.systemTeal))
.foregroundStyle(.white)
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
}
.buttonStyle(.plain)
}
.transition(.opacity.combined(with: .move(edge: .top)))
}
}
.padding(18)
.background(
RoundedRectangle(cornerRadius: 24, style: .continuous)
.fill(Color(red: 1.00, green: 0.99, blue: 0.96))
)
.overlay(
RoundedRectangle(cornerRadius: 24, style: .continuous)
.stroke(Color(.separator).opacity(0.5), lineWidth: 1)
)
}

private static func dueText(for dueDate: Date?) -> String {
guard let dueDate else { return "Due Soon" }
let cal = Calendar.current
if cal.isDateInToday(dueDate) { return "Due Today" }
if cal.isDateInTomorrow(dueDate) { return "Due Tomorrow" }

let startToday = cal.startOfDay(for: Date())
let startDue = cal.startOfDay(for: dueDate)
if let days = cal.dateComponents([.day], from: startToday, to: startDue).day,
days >= 2, days <= 6 {
let weekday = DateFormatter()
weekday.dateFormat = "EEEE"
return "Due \(weekday.string(from: dueDate))"
}

let formatter = DateFormatter()
formatter.dateStyle = .medium
return "Due \(formatter.string(from: dueDate))"
}

private static func parseDueDate(from input: String) -> Date? {
let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty { return nil }

let lower = trimmed.lowercased()
let cal = Calendar.current
let now = Date()

if lower == "today" { return now }
if lower == "tomorrow" { return cal.date(byAdding: .day, value: 1, to: now) }

let weekdayMap: [String: Int] = [
"sunday": 1, "monday": 2, "tuesday": 3, "wednesday": 4,
"thursday": 5, "friday": 6, "saturday": 7
]
if let weekday = weekdayMap[lower] {
return nextWeekday(weekday)
}

let formats = ["M/d/yyyy", "M/d/yy", "M/d", "MM/dd/yyyy", "MM/dd/yy", "MM/dd"]
for format in formats {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = format
if let parsed = formatter.date(from: trimmed) {
if format == "M/d" || format == "MM/dd" {
let comps = cal.dateComponents([.month, .day], from: parsed)
var candidate = cal.date(from: DateComponents(
year: cal.component(.year, from: now),
month: comps.month,
day: comps.day
))
if let candidateDate = candidate, candidateDate < cal.startOfDay(for: now) {
candidate = cal.date(byAdding: .year, value: 1, to: candidateDate)
}
return candidate
}
return parsed
}
}

return nil
}

private static func nextWeekday(_ weekday: Int) -> Date? {
let cal = Calendar.current
let today = Date()
return cal.nextDate(
after: today,
matching: DateComponents(weekday: weekday),
matchingPolicy: .nextTimePreservingSmallerComponents
)
}
}


#Preview {
DeadlineView()
}
Loading