feat: Add support for creating events

This commit is contained in:
Peter Vacho 2025-01-01 18:07:04 +01:00
parent 82f300fdd9
commit 3df1faa17c
Signed by: school
GPG key ID: 8CFC3837052871B4
9 changed files with 411 additions and 17 deletions

View file

@ -16,6 +16,9 @@
android:supportsRtl="true"
android:theme="@style/Theme.NeatCalendar"
tools:targetApi="31">
<activity
android:name=".activities.CreateEventActivity"
android:exported="false" />
<activity
android:name=".activities.DayViewActivity"
android:exported="false" />

View file

@ -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"
}
}

View file

@ -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<EventResponse>
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")!!
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()
}
}

View file

@ -21,3 +21,12 @@ data class EventResponse(
val attendee_ids: List<String>,
val created_at: OffsetDateTime
): Parcelable
data class EventRequest(
val title: String,
val description: String,
val category_ids: List<String>,
val start_time: OffsetDateTime,
val end_time: OffsetDateTime,
val color: Color,
)

View file

@ -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
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorPrimary" android:state_checked="true" />
<item android:color="?attr/colorSecondary" />
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorOnPrimary" android:state_checked="true" />
<item android:color="?attr/colorOnSecondary" />
</selector>

View file

@ -0,0 +1,140 @@
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:padding="16dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:id="@+id/main"
android:layout_margin="15dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Event Title -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/event_title"
app:boxStrokeWidth="1dp"
app:boxStrokeColor="?attr/colorPrimary">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEventTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Event Description -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/event_description"
app:boxStrokeWidth="1dp"
app:boxStrokeColor="?attr/colorPrimary">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etEventDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:maxLines="3"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Event Type Toggle -->
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/eventTypeToggleGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_gravity="center"
app:singleSelection="true"
app:checkedButton="@id/btnInstantEvent">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnInstantEvent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/instant_event"
android:checkable="true"
app:backgroundTint="@color/toggle_button_background_selector"
app:strokeColor="@color/toggle_button_stroke_selector"
app:strokeWidth="2dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDurationEvent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/duration_event"
android:checkable="true"
app:backgroundTint="@color/toggle_button_background_selector"
app:strokeColor="@color/toggle_button_stroke_selector"
app:strokeWidth="2dp" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<!-- Start Time -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/start_time"
app:boxStrokeWidth="1dp"
app:boxStrokeColor="?attr/colorPrimary">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/txtStartTime"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="false"
android:clickable="true" />
</com.google.android.material.textfield.TextInputLayout>
<!-- End Time -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/end_time"
app:boxStrokeWidth="1dp"
app:boxStrokeColor="?attr/colorPrimary">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/txtEndTime"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="false"
android:clickable="true"
android:visibility="gone"
tools:visibility="visible" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Create Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnCreateEvent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/create_event"
app:cornerRadius="8dp" />
<!-- Close Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnClose"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:text="@string/close"
app:icon="@drawable/ic_arrow_back"
app:iconGravity="textStart"
app:iconPadding="8dp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -33,4 +33,11 @@
<string name="edit_event">Edit this event</string>
<string name="add_event">Add a new event</string>
<string name="back">Go to the previous page</string>
<string name="event_title">Event Title</string>
<string name="event_description">Event Description</string>
<string name="instant_event">Instant Event</string>
<string name="duration_event">Duration Event</string>
<string name="start_time">Start Time</string>
<string name="end_time">End Time</string>
<string name="create_event">Create Event</string>
</resources>