From 3df1faa17c7255386bcf00a50c4560261024607b Mon Sep 17 00:00:00 2001 From: Peter Vacho Date: Wed, 1 Jan 2025 18:07:04 +0100 Subject: [PATCH] feat: Add support for creating events --- app/src/main/AndroidManifest.xml | 3 + .../activities/CreateEventActivity.kt | 195 ++++++++++++++++++ .../activities/DayViewActivity.kt | 56 +++-- .../neat_calendar/api/models/EventModels.kt | 11 +- .../api/services/EventsService.kt | 6 + .../toggle_button_background_selector.xml | 5 + .../color/toggle_button_stroke_selector.xml | 5 + .../main/res/layout/activity_create_event.xml | 140 +++++++++++++ app/src/main/res/values/strings.xml | 7 + 9 files changed, 411 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/p_vacho/neat_calendar/activities/CreateEventActivity.kt create mode 100644 app/src/main/res/color/toggle_button_background_selector.xml create mode 100644 app/src/main/res/color/toggle_button_stroke_selector.xml create mode 100644 app/src/main/res/layout/activity_create_event.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b165bd5..3c11880 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,6 +16,9 @@ android:supportsRtl="true" android:theme="@style/Theme.NeatCalendar" tools:targetApi="31"> + diff --git a/app/src/main/java/com/p_vacho/neat_calendar/activities/CreateEventActivity.kt b/app/src/main/java/com/p_vacho/neat_calendar/activities/CreateEventActivity.kt new file mode 100644 index 0000000..b78b839 --- /dev/null +++ b/app/src/main/java/com/p_vacho/neat_calendar/activities/CreateEventActivity.kt @@ -0,0 +1,195 @@ +package com.p_vacho.neat_calendar.activities + +import android.app.DatePickerDialog +import android.app.TimePickerDialog +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import android.view.View +import android.widget.EditText +import android.widget.Toast +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.lifecycleScope +import com.google.android.material.button.MaterialButton +import com.google.android.material.button.MaterialButtonToggleGroup +import com.google.android.material.textfield.TextInputEditText +import com.p_vacho.neat_calendar.R +import com.p_vacho.neat_calendar.api.RetrofitClient +import com.p_vacho.neat_calendar.api.models.EventRequest +import com.p_vacho.neat_calendar.api.models.EventResponse +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import retrofit2.HttpException +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.ZoneId + +class CreateEventActivity : AppCompatActivity() { + private lateinit var etEventTitle: EditText + private lateinit var etEventDescription: EditText + private lateinit var eventTypeToggleGroup: MaterialButtonToggleGroup + private lateinit var btnInstantEvent: MaterialButton + private lateinit var btnDurationEvent: MaterialButton + private lateinit var txtStartTime: TextInputEditText + private lateinit var txtEndTime: TextInputEditText + private lateinit var btnCreateEvent: MaterialButton + + private lateinit var defaultDate: LocalDate + + private var instantEvent: Boolean = true + private var startDateTime: LocalDateTime? = null + private var endDateTime: LocalDateTime? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContentView(R.layout.activity_create_event) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + + + // The getParcelableExtra wants the class as a second argument to + // be more type-safe, but this is only supported since api 33, which is over + // our min api version, so we can ignore this deprecation for now. + @Suppress("DEPRECATION") + val dateString = intent.getStringExtra("date")!! + defaultDate = LocalDate.parse(dateString) + + initializeViews() + setupListeners() + } + + private fun initializeViews() { + etEventTitle = findViewById(R.id.etEventTitle) + etEventDescription = findViewById(R.id.etEventDescription) + eventTypeToggleGroup = findViewById(R.id.eventTypeToggleGroup) + btnInstantEvent = findViewById(R.id.btnInstantEvent) + btnDurationEvent = findViewById(R.id.btnDurationEvent) + txtStartTime = findViewById(R.id.txtStartTime) + txtEndTime = findViewById(R.id.txtEndTime) + btnCreateEvent = findViewById(R.id.btnCreateEvent) + } + + private fun setupListeners() { + eventTypeToggleGroup.addOnButtonCheckedListener { group, checkedId, isChecked -> + if (isChecked) { // Triggered only for the newly selected button + when (checkedId) { + R.id.btnInstantEvent -> { + txtEndTime.visibility = View.GONE + txtEndTime.setText("") + endDateTime = null + instantEvent = true + } + R.id.btnDurationEvent -> { + txtEndTime.visibility = View.VISIBLE + instantEvent = false + } + } + } + } + + txtStartTime.setOnClickListener { + val defaultDateTime = startDateTime ?: defaultDate.atTime(12, 0) + showDateTimePicker(defaultDateTime) { selectedDateTime -> + startDateTime = selectedDateTime + txtStartTime.setText(formatDateTime(selectedDateTime)) + } + } + + txtEndTime.setOnClickListener { + val defaultDateTime = endDateTime ?: startDateTime ?: defaultDate.atTime(12, 0) + showDateTimePicker(defaultDateTime) { selectedDateTime -> + if (!selectedDateTime.isAfter(startDateTime)) { + Toast.makeText(this, "End time must be after start time", Toast.LENGTH_SHORT).show() + } else { + endDateTime = selectedDateTime + txtEndTime.setText(formatDateTime(selectedDateTime)) + } + } + } + + btnCreateEvent.setOnClickListener { createEvent() } + } + + private fun createEvent() { + val title = etEventTitle.text.toString() + val description = etEventDescription.text.toString() + + if (title.isEmpty() || startDateTime == null) { + Toast.makeText(this, "Please provide a title and start time", Toast.LENGTH_SHORT).show() + return + } + + if (!instantEvent && endDateTime == null) { + Toast.makeText(this, "Please provide an end time or use an instant event", Toast.LENGTH_SHORT).show() + return + } + + val eventRequest = EventRequest( + title = title, + description = description, + category_ids = emptyList(), // Categories will be added later + start_time = startDateTime!!.atZone(ZoneId.systemDefault()).toOffsetDateTime(), + end_time = endDateTime?.atZone(ZoneId.systemDefault())?.toOffsetDateTime() + ?: startDateTime!!.atZone(ZoneId.systemDefault()).toOffsetDateTime(), + color = Color.valueOf(0xFF33AABB.toInt()) // Placeholder color + ) + + lifecycleScope.launch(Dispatchers.IO) { + try { + val createdEvent = RetrofitClient.eventsService.createEvent(eventRequest) + withContext(Dispatchers.Main) { handleEventCreated(createdEvent) } + } catch (e: HttpException) { + if (e.code() != 400) { + throw e + } + + withContext(Dispatchers.Main) { + Toast.makeText( + this@CreateEventActivity, + "Failed to create event", + Toast.LENGTH_SHORT + ).show() + } + } + } + } + + private fun handleEventCreated(createdEvent: EventResponse) { + Toast.makeText(this, "Event Created: ${createdEvent.title}", Toast.LENGTH_SHORT).show() + + val intent = Intent().apply { + putExtra("newEvent", createdEvent) + } + setResult(RESULT_OK, intent) // Pass the event back + + finish() // Close the activity and return + } + + private fun showDateTimePicker( + initialDateTime: LocalDateTime?, + onDateTimeSelected: (LocalDateTime) -> Unit + ) { + val now = initialDateTime ?: LocalDateTime.now() + + val datePicker = DatePickerDialog(this, { _, year, month, dayOfMonth -> + val timePicker = TimePickerDialog(this, { _, hourOfDay, minute -> + val selectedDateTime = LocalDateTime.of(year, month + 1, dayOfMonth, hourOfDay, minute) + onDateTimeSelected(selectedDateTime) + }, now.hour, now.minute, true) + timePicker.show() + }, now.year, now.monthValue - 1, now.dayOfMonth) + datePicker.show() + } + + private fun formatDateTime(dateTime: LocalDateTime?): String { + return dateTime?.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) ?: "Select Time" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/p_vacho/neat_calendar/activities/DayViewActivity.kt b/app/src/main/java/com/p_vacho/neat_calendar/activities/DayViewActivity.kt index bfe03b0..1bd2a43 100644 --- a/app/src/main/java/com/p_vacho/neat_calendar/activities/DayViewActivity.kt +++ b/app/src/main/java/com/p_vacho/neat_calendar/activities/DayViewActivity.kt @@ -1,5 +1,7 @@ package com.p_vacho.neat_calendar.activities +import android.annotation.SuppressLint +import android.content.Intent import android.os.Bundle import android.widget.ImageButton import android.widget.TextView @@ -11,6 +13,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.p_vacho.neat_calendar.R import com.p_vacho.neat_calendar.adapters.EventCardAdapter +import com.p_vacho.neat_calendar.api.models.EventResponse import com.p_vacho.neat_calendar.models.CalendarDay class DayViewActivity : AppCompatActivity() { @@ -19,6 +22,20 @@ class DayViewActivity : AppCompatActivity() { private lateinit var tvDate: TextView private lateinit var rvEvents: RecyclerView + private lateinit var calendarDay: CalendarDay + private lateinit var events: MutableList + + private val createEventLauncher = registerForActivityResult( + androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == RESULT_OK) { + @Suppress("DEPRECATION") + val newEvent: EventResponse? = result.data?.getParcelableExtra("newEvent") + + newEvent?.let { addNewEvent(it) } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() @@ -33,32 +50,39 @@ class DayViewActivity : AppCompatActivity() { // be more type-safe, but this is only supported since api 33, which is over // our min api version, so we can ignore this deprecation for now. @Suppress("DEPRECATION") - val calendarDay = intent.getParcelableExtra("calendarDay")!! + calendarDay = intent.getParcelableExtra("calendarDay")!! - // It's possible that events will be removed by the event card adapter, use a - // mutable list to keep track of them from here. We don't currently propagate - // these changes anywhere, going back to calendar activity will just trigger - // an api refetch, which is sufficient for now, but in the future, we could. - val events = calendarDay.events.toMutableList() + events = calendarDay.events.sortedBy { it.start_time }.toMutableList() - // Setup the UI + // Initialize Views tvDate = findViewById(R.id.tvDate) rvEvents = findViewById(R.id.rvEvents) btnBack = findViewById(R.id.btnBack) btnAddEvent = findViewById(R.id.btnAddEvent) - btnBack.setOnClickListener { - // Handle back navigation - finish() - } - - btnAddEvent.setOnClickListener { - TODO("Implement adding events") - } - + // Setup UI tvDate.text = calendarDay.date.toString() rvEvents.layoutManager = LinearLayoutManager(this) rvEvents.adapter = EventCardAdapter(events, this) + + btnBack.setOnClickListener { finish() } + btnAddEvent.setOnClickListener { navigateToCreateEventActivity() } + } + + fun navigateToCreateEventActivity() { + val intent = Intent(this, CreateEventActivity::class.java).apply { + putExtra("date", calendarDay.date.toString()) + } + createEventLauncher.launch(intent) + } + + private fun addNewEvent(newEvent: EventResponse) { + events.add(newEvent) + events.sortBy { it.start_time } + + // We can't be more specific, we don't know the id due to sorting + @Suppress("NotifyDataSetChanged") + rvEvents.adapter!!.notifyDataSetChanged() } } \ No newline at end of file diff --git a/app/src/main/java/com/p_vacho/neat_calendar/api/models/EventModels.kt b/app/src/main/java/com/p_vacho/neat_calendar/api/models/EventModels.kt index 3c6328d..8e71c67 100644 --- a/app/src/main/java/com/p_vacho/neat_calendar/api/models/EventModels.kt +++ b/app/src/main/java/com/p_vacho/neat_calendar/api/models/EventModels.kt @@ -20,4 +20,13 @@ data class EventResponse( val owner_user_id: String, val attendee_ids: List, val created_at: OffsetDateTime -): Parcelable \ No newline at end of file +): Parcelable + +data class EventRequest( + val title: String, + val description: String, + val category_ids: List, + val start_time: OffsetDateTime, + val end_time: OffsetDateTime, + val color: Color, +) \ No newline at end of file diff --git a/app/src/main/java/com/p_vacho/neat_calendar/api/services/EventsService.kt b/app/src/main/java/com/p_vacho/neat_calendar/api/services/EventsService.kt index 38531ee..7b8944d 100644 --- a/app/src/main/java/com/p_vacho/neat_calendar/api/services/EventsService.kt +++ b/app/src/main/java/com/p_vacho/neat_calendar/api/services/EventsService.kt @@ -1,8 +1,11 @@ package com.p_vacho.neat_calendar.api.services +import com.p_vacho.neat_calendar.api.models.EventRequest import com.p_vacho.neat_calendar.api.models.EventResponse +import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.Query import java.time.OffsetDateTime @@ -31,4 +34,7 @@ interface EventsService { @DELETE("events/{event_id}") suspend fun deleteEvent(@Path("event_id") eventId: String): Unit + + @POST("events") + suspend fun createEvent(@Body eventData: EventRequest): EventResponse } \ No newline at end of file diff --git a/app/src/main/res/color/toggle_button_background_selector.xml b/app/src/main/res/color/toggle_button_background_selector.xml new file mode 100644 index 0000000..8ff8d43 --- /dev/null +++ b/app/src/main/res/color/toggle_button_background_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/toggle_button_stroke_selector.xml b/app/src/main/res/color/toggle_button_stroke_selector.xml new file mode 100644 index 0000000..7abc020 --- /dev/null +++ b/app/src/main/res/color/toggle_button_stroke_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_create_event.xml b/app/src/main/res/layout/activity_create_event.xml new file mode 100644 index 0000000..855de25 --- /dev/null +++ b/app/src/main/res/layout/activity_create_event.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9cf2f12..d8dae1b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -33,4 +33,11 @@ Edit this event Add a new event Go to the previous page + Event Title + Event Description + Instant Event + Duration Event + Start Time + End Time + Create Event \ No newline at end of file