diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f51d844..5083ccb 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/CategoriesActivity.kt b/app/src/main/java/com/p_vacho/neat_calendar/activities/CategoriesActivity.kt
index b76dce8..11b8433 100644
--- a/app/src/main/java/com/p_vacho/neat_calendar/activities/CategoriesActivity.kt
+++ b/app/src/main/java/com/p_vacho/neat_calendar/activities/CategoriesActivity.kt
@@ -1,21 +1,133 @@
package com.p_vacho.neat_calendar.activities
+import android.content.Intent
import android.os.Bundle
+import android.view.View
+import android.widget.ImageButton
+import android.widget.TextView
+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 androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.p_vacho.neat_calendar.MyApplication
import com.p_vacho.neat_calendar.R
+import com.p_vacho.neat_calendar.adapters.CategoryAdapter
+import com.p_vacho.neat_calendar.api.RetrofitClient
+import com.p_vacho.neat_calendar.api.models.CategoryResponse
+import com.p_vacho.neat_calendar.api.models.EventResponse
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
class CategoriesActivity : AppCompatActivity() {
+ private lateinit var rvCategories: RecyclerView
+ private lateinit var btnBack: ImageButton
+ private lateinit var btnAddCategory: ImageButton
+ private lateinit var tvEmptyState: TextView
+
+ private lateinit var categories: MutableList
+
+ private val createActivityLauncher = registerForActivityResult(
+ androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ if (result.resultCode == RESULT_OK) {
+ @Suppress("DEPRECATION")
+ val newCategory: CategoryResponse? = result.data?.getParcelableExtra("newCategory")
+
+ newCategory?.let { categoryCreateReply(it) }
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_categories)
+
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
}
+
+ rvCategories = findViewById(R.id.rvCategories)
+ btnBack = findViewById(R.id.btnBack)
+ btnAddCategory = findViewById(R.id.btnAddCategory)
+ tvEmptyState = findViewById(R.id.tvEmptyState)
+
+ btnBack.setOnClickListener { finish() }
+ btnAddCategory.setOnClickListener { navigateToCreateCategory() }
+
+ rvCategories.layoutManager = LinearLayoutManager(this)
+
+ lifecycleScope.launch {
+ categories = fetchCategories().toMutableList()
+
+ val adapter = CategoryAdapter(categories, ::handleDeleteCategory)
+
+ rvCategories.adapter = adapter
+ updateEmptyState()
+ }
}
-}
\ No newline at end of file
+
+ /**
+ * Fetches all categories for the current user from the backend.
+ */
+ private suspend fun fetchCategories(): List {
+ val userId = (application as MyApplication).tokenManager.userId
+ ?: run {
+ finish()
+ return emptyList()
+ }
+
+ return withContext(Dispatchers.IO) {
+ RetrofitClient.categoryService.userCategories(userId)
+ }
+ }
+
+ /**
+ * A callback function triggered by the Category Adapter, when the user
+ * clicks on the delete button.
+ */
+ private fun handleDeleteCategory(category: CategoryResponse, position: Int) {
+ lifecycleScope.launch {
+ withContext(Dispatchers.IO) {
+ RetrofitClient.categoryService.deleteCategory(category.id)
+ }
+ // Remove category from the adapter and update the UI
+ (rvCategories.adapter as CategoryAdapter).removeCategoryAt(position)
+ Toast.makeText(this@CategoriesActivity, "Category deleted", Toast.LENGTH_SHORT).show()
+ updateEmptyState()
+ }
+ }
+
+ /**
+ * Navigates to the activity for adding a new category.
+ */
+ private fun navigateToCreateCategory() {
+ val intent = Intent(this, CreateCategoryActivity::class.java)
+ startActivity(intent)
+ }
+
+ /**
+ * Used as a callback, triggered when the CreateCategory Activity returns a result.
+ *
+ * The returned value (the new category data) is passed over as a parameter.
+ */
+ private fun categoryCreateReply(category: CategoryResponse) {
+ (rvCategories.adapter as CategoryAdapter).addCategory(category)
+ }
+
+ /**
+ * This is a helper function to toggle the visibility of the empty state message
+ * when no categories are available.
+ *
+ * This should be called whenever a category was removed or added.
+ */
+ private fun updateEmptyState() {
+ tvEmptyState.visibility = if (categories.isEmpty()) View.VISIBLE else View.GONE
+ }
+}
diff --git a/app/src/main/java/com/p_vacho/neat_calendar/activities/CreateCategoryActivity.kt b/app/src/main/java/com/p_vacho/neat_calendar/activities/CreateCategoryActivity.kt
new file mode 100644
index 0000000..9c55922
--- /dev/null
+++ b/app/src/main/java/com/p_vacho/neat_calendar/activities/CreateCategoryActivity.kt
@@ -0,0 +1,21 @@
+package com.p_vacho.neat_calendar.activities
+
+import android.os.Bundle
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import com.p_vacho.neat_calendar.R
+
+class CreateCategoryActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContentView(R.layout.activity_create_category)
+ 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
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/p_vacho/neat_calendar/adapters/CategoryAdapter.kt b/app/src/main/java/com/p_vacho/neat_calendar/adapters/CategoryAdapter.kt
new file mode 100644
index 0000000..6273888
--- /dev/null
+++ b/app/src/main/java/com/p_vacho/neat_calendar/adapters/CategoryAdapter.kt
@@ -0,0 +1,68 @@
+package com.p_vacho.neat_calendar.adapters
+
+import android.graphics.Color
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.p_vacho.neat_calendar.R
+import com.p_vacho.neat_calendar.api.models.CategoryResponse
+
+class CategoryAdapter(
+ private val categories: MutableList,
+ private val onDeleteClick: (CategoryResponse, Int) -> Unit
+) : RecyclerView.Adapter() {
+
+ inner class CategoryViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val colorIndicator: View = view.findViewById(R.id.colorIndicator)
+ val categoryName: TextView = view.findViewById(R.id.tvCategoryName)
+ val deleteButton: ImageButton = view.findViewById(R.id.btnDeleteCategory)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_category, parent, false)
+ return CategoryViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: CategoryViewHolder, position: Int) {
+ val category = categories[position]
+
+ // Bind category data to the views
+ holder.colorIndicator.setBackgroundColor(category.color.toArgb())
+ holder.categoryName.text = category.name
+
+ // Set click listener for the delete button
+ holder.deleteButton.setOnClickListener {
+ onDeleteClick(category, position)
+ }
+ }
+
+ override fun getItemCount(): Int = categories.size
+
+ /**
+ * Remove a category from the list and notify the adapter.
+ *
+ * Call this after the onDeleteClick callback deletes the category from the backend API.
+ */
+ fun removeCategoryAt(position: Int) {
+ categories.removeAt(position)
+ notifyItemRemoved(position)
+
+ // Annoyingly, we can't just use notifyItemRemoved for the single removed item,
+ // as all the items below it would now be using the wrong position that was
+ // already bound to the callbacks from the click listeners, so we need to refresh
+ // all of the categories below this one as well.
+ notifyItemRangeChanged(position, categories.size - position)
+ }
+
+ /**
+ * Add a new category to the end of the list and notify the adapter.
+ */
+ fun addCategory(category: CategoryResponse) {
+ categories.add(category)
+ notifyItemInserted(categories.size)
+ }
+}
diff --git a/app/src/main/java/com/p_vacho/neat_calendar/api/services/CategoryService.kt b/app/src/main/java/com/p_vacho/neat_calendar/api/services/CategoryService.kt
index 0d991b5..2b2374d 100644
--- a/app/src/main/java/com/p_vacho/neat_calendar/api/services/CategoryService.kt
+++ b/app/src/main/java/com/p_vacho/neat_calendar/api/services/CategoryService.kt
@@ -1,6 +1,7 @@
package com.p_vacho.neat_calendar.api.services
import com.p_vacho.neat_calendar.api.models.CategoryResponse
+import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Path
@@ -11,6 +12,9 @@ interface CategoryService {
@GET("/events/{event_id}/categories")
suspend fun eventCategories(@Path("event_id") eventId: String): List
- @GET("/category/{category_id}")
+ @GET("/categories/{category_id}")
suspend fun getCategory(@Path("category_id") categoryId: String): CategoryResponse
+
+ @DELETE("/categories/{category_id}")
+ suspend fun deleteCategory(@Path("category_id") categoryId: String): Unit
}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_categories.xml b/app/src/main/res/layout/activity_categories.xml
index d01abd6..4a94d76 100644
--- a/app/src/main/res/layout/activity_categories.xml
+++ b/app/src/main/res/layout/activity_categories.xml
@@ -1,5 +1,6 @@
-
-
\ No newline at end of file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_create_category.xml b/app/src/main/res/layout/activity_create_category.xml
new file mode 100644
index 0000000..f17dc48
--- /dev/null
+++ b/app/src/main/res/layout/activity_create_category.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_category.xml b/app/src/main/res/layout/item_category.xml
new file mode 100644
index 0000000..eb1aa79
--- /dev/null
+++ b/app/src/main/res/layout/item_category.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e078925..768f773 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -81,4 +81,7 @@
View Event
Swipe right on a notification to delete it.
You\'re all caught up! No notifications right now.
+ No categories found
+ Categories
+ Delete category
\ No newline at end of file