Skip to content
This repository was archived by the owner on Oct 12, 2025. It is now read-only.

Commit 398080e

Browse files
authored
Merge pull request #558 from HackIllinois/sneh/points-page
2025 Prize Shop
2 parents 0b0be2c + 78033d6 commit 398080e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+3132
-105
lines changed

app/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ dependencies {
129129

130130
// Swipe-to-refresh
131131
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
132+
133+
//Json Parsing
134+
implementation 'com.google.code.gson:gson:2.8.9'
132135
}
133136

134137
apply plugin: 'com.google.gms.google-services'

app/src/main/java/org/hackillinois/android/API.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.hackillinois.android
22

3+
import okhttp3.ResponseBody
34
import org.hackillinois.android.database.entity.*
45
import org.hackillinois.android.model.event.EventsList
56
import org.hackillinois.android.model.event.ShiftsList
@@ -16,6 +17,7 @@ import org.hackillinois.android.model.user.FavoritesResponse
1617
import org.hackillinois.android.model.version.Version
1718
import org.hackillinois.android.notifications.DeviceToken
1819
import retrofit2.Call
20+
import retrofit2.Response
1921
import retrofit2.http.*
2022

2123
interface API {
@@ -69,6 +71,18 @@ interface API {
6971
@POST("shop/item/buy/")
7072
suspend fun buyShopItem(@Body body: ItemInstance): ShopItem
7173

74+
@POST("shop/cart/{itemId}")
75+
suspend fun addItemCart(@Path("itemId") itemId: String): Response<ResponseBody>
76+
77+
@GET("shop/cart/")
78+
suspend fun getCart(): Cart
79+
80+
@GET("shop/cart/qr/")
81+
suspend fun getCartQRCode(): QRResponse
82+
83+
@DELETE("shop/cart/{itemId}")
84+
suspend fun removeItemCart(@Path("itemId") itemId: String): Response<ResponseBody>
85+
7286
@POST("shop/cart/redeem/")
7387
suspend fun redeemCart(@Body body: QRCode): Cart
7488

app/src/main/java/org/hackillinois/android/App.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ class App : Application() {
3030
return if (apiInitialized) apiInternal else getAPI("")
3131
}
3232

33-
Log.d("TOKEN", token)
33+
Log.d("APPTOKEN", token)
3434

3535
val interceptor = { chain: Interceptor.Chain ->
3636
val newRequest = chain.request().newBuilder()
37-
.addHeader("Authorization", token)
37+
.addHeader("Authorization", "Bearer $token")
38+
.addHeader("Accept", "application/json")
3839
.build()
3940
chain.proceed(newRequest)
4041
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package org.hackillinois.android.database.entity
2+
3+
data class QRResponse(val QRCode: String)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package org.hackillinois.android.view.shop
2+
3+
import android.content.Context
4+
import android.graphics.Rect
5+
import android.view.LayoutInflater
6+
import android.view.TouchDelegate
7+
import android.view.View
8+
import android.view.ViewGroup
9+
import android.widget.ImageView
10+
import android.widget.TextView
11+
import androidx.recyclerview.widget.RecyclerView
12+
import com.bumptech.glide.Glide
13+
import org.hackillinois.android.R
14+
import org.hackillinois.android.database.entity.ShopItem
15+
16+
class CartAdapter(
17+
private var cartItems: List<Pair<ShopItem, Int>>,
18+
private val listener: OnQuantityChangeListener
19+
) : RecyclerView.Adapter<CartAdapter.ViewHolder>() {
20+
21+
private lateinit var context: Context
22+
23+
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
24+
val textViewSticker: TextView = view.findViewById(R.id.text_view_sticker)
25+
val quantityTextView: TextView = view.findViewById(R.id.number_text)
26+
val shopItemImageView: ImageView = view.findViewById(R.id.image_view_sticker_symbol)
27+
val plusButton: TextView = view.findViewById(R.id.button_plus)
28+
val minusButton: TextView = view.findViewById(R.id.button_minus)
29+
}
30+
31+
private fun expandTouchArea(targetView: View, extraPadding: Int) {
32+
val parentView = targetView.parent as? ViewGroup ?: return
33+
parentView.post {
34+
val rect = Rect()
35+
targetView.getHitRect(rect)
36+
rect.top -= extraPadding
37+
rect.left -= extraPadding
38+
rect.bottom += extraPadding
39+
rect.right += extraPadding
40+
parentView.touchDelegate = TouchDelegate(rect, targetView)
41+
parentView.requestLayout() // Refresh layout so it applies
42+
}
43+
}
44+
45+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
46+
val view = LayoutInflater.from(parent.context)
47+
.inflate(R.layout.point_shop_cart_tile, parent, false)
48+
context = parent.context
49+
return ViewHolder(view)
50+
}
51+
52+
override fun getItemCount() = cartItems.size
53+
54+
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
55+
val (item, quantity) = cartItems[position]
56+
holder.textViewSticker.text = item.name
57+
holder.quantityTextView.text = quantity.toString()
58+
Glide.with(context).load(item.imageURL).into(holder.shopItemImageView)
59+
60+
// Increase quantity using plus button
61+
holder.plusButton.setOnClickListener {
62+
val newQuantity = quantity + 1
63+
listener.onIncreaseQuantity(item, newQuantity)
64+
}
65+
66+
expandTouchArea(holder.plusButton, 100)
67+
68+
// Decrease quantity using minus button (allowing 0)
69+
holder.minusButton.setOnClickListener {
70+
val newQuantity = quantity - 1
71+
listener.onDecreaseQuantity(item, newQuantity)
72+
}
73+
74+
expandTouchArea(holder.minusButton, 100)
75+
}
76+
77+
fun updateCart(newCartItems: List<Pair<ShopItem, Int>>) {
78+
this.cartItems = newCartItems
79+
notifyDataSetChanged()
80+
}
81+
82+
interface OnQuantityChangeListener {
83+
fun onIncreaseQuantity(item: ShopItem, newQuantity: Int)
84+
fun onDecreaseQuantity(item: ShopItem, newQuantity: Int)
85+
}
86+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package org.hackillinois.android.view.shop
2+
3+
import android.os.Bundle
4+
import android.util.Log
5+
import android.view.LayoutInflater
6+
import android.view.View
7+
import android.view.ViewGroup
8+
import androidx.fragment.app.Fragment
9+
import androidx.lifecycle.lifecycleScope
10+
import androidx.recyclerview.widget.GridLayoutManager
11+
import androidx.recyclerview.widget.RecyclerView
12+
import kotlinx.coroutines.launch
13+
import org.hackillinois.android.App
14+
import org.hackillinois.android.R
15+
import org.hackillinois.android.database.entity.Cart
16+
import org.hackillinois.android.database.entity.ShopItem
17+
18+
class CartFragment : Fragment(), CartAdapter.OnQuantityChangeListener {
19+
20+
private lateinit var recyclerView: RecyclerView
21+
private lateinit var cartAdapter: CartAdapter
22+
private var cartItems: List<Pair<ShopItem, Int>> = listOf()
23+
24+
override fun onCreateView(
25+
inflater: LayoutInflater,
26+
container: ViewGroup?,
27+
savedInstanceState: Bundle?
28+
): View? {
29+
val view = inflater.inflate(R.layout.fragment_point_shop_cart, container, false)
30+
31+
recyclerView = view.findViewById(R.id.recyclerview_point_shop)
32+
recyclerView.layoutManager = GridLayoutManager(context, 2)
33+
34+
cartAdapter = CartAdapter(cartItems, this)
35+
recyclerView.adapter = cartAdapter
36+
37+
fetchCartData()
38+
39+
val backButton: View = view.findViewById(R.id.backButton)
40+
backButton.bringToFront()
41+
backButton.setOnClickListener {
42+
requireActivity().supportFragmentManager.popBackStack() // Go back to previous fragment
43+
}
44+
45+
val redeemButton: View = view.findViewById(R.id.redeemButton)
46+
redeemButton.setOnClickListener {
47+
val redeemFragment = RedeemFragment()
48+
requireActivity().supportFragmentManager.beginTransaction()
49+
.replace(R.id.contentFrame, redeemFragment)
50+
.addToBackStack(null)
51+
.commit()
52+
}
53+
54+
return view
55+
}
56+
57+
private fun fetchCartData() {
58+
lifecycleScope.launch {
59+
try {
60+
val shopItems: List<ShopItem> = App.getAPI().shop()
61+
val cart: Cart = App.getAPI().getCart()
62+
val items = mutableListOf<Pair<ShopItem, Int>>()
63+
64+
for ((itemId, quantity) in cart.items) {
65+
val shopItem = shopItems.find { it.itemId == itemId }
66+
if (shopItem != null) {
67+
items.add(Pair(shopItem, quantity))
68+
} else {
69+
Log.e("CartFragment", "ShopItem not found for itemId: $itemId")
70+
}
71+
}
72+
cartItems = items
73+
cartAdapter.updateCart(cartItems)
74+
} catch (e: Exception) {
75+
Log.e("CartFragment", "Error fetching cart items", e)
76+
}
77+
}
78+
}
79+
80+
override fun onIncreaseQuantity(item: ShopItem, newQuantity: Int) {
81+
lifecycleScope.launch {
82+
try {
83+
val response = App.getAPI().addItemCart(item.itemId)
84+
if (response.isSuccessful) {
85+
Log.d("CartDebug", "Item added: ${response.body()}")
86+
fetchCartData() // Refresh cart
87+
} else {
88+
Log.e("CartDebug", "Failed to add item: ${response.code()}")
89+
}
90+
} catch (e: Exception) {
91+
Log.e("CartDebug", "Error adding item to cart", e)
92+
}
93+
}
94+
}
95+
96+
override fun onDecreaseQuantity(item: ShopItem, newQuantity: Int) {
97+
lifecycleScope.launch {
98+
try {
99+
val response = App.getAPI().removeItemCart(item.itemId)
100+
if (response.isSuccessful) {
101+
Log.d("CartDebug", "Item removed: ${response.body()}")
102+
if (newQuantity == 0) {
103+
// If quantity is zero, remove the item from UI
104+
fetchCartData()
105+
} else {
106+
// Just update the UI without full fetch
107+
cartItems = cartItems.map {
108+
if (it.first.itemId == item.itemId) Pair(it.first, newQuantity) else it
109+
}
110+
cartAdapter.updateCart(cartItems)
111+
}
112+
} else {
113+
Log.e("CartDebug", "Failed to remove item: ${response.code()}")
114+
}
115+
} catch (e: Exception) {
116+
Log.e("CartDebug", "Error removing item from cart", e)
117+
}
118+
}
119+
}
120+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package org.hackillinois.android.view.shop
2+
3+
import RedeemViewModel
4+
import android.graphics.Bitmap
5+
import android.graphics.Color
6+
import android.os.Bundle
7+
import android.util.Log
8+
import android.view.LayoutInflater
9+
import android.view.View
10+
import android.view.ViewGroup
11+
import android.widget.ImageView
12+
import android.widget.Toast
13+
import androidx.fragment.app.Fragment
14+
import androidx.lifecycle.Observer
15+
import androidx.lifecycle.ViewModelProvider
16+
import com.google.zxing.BarcodeFormat
17+
import com.google.zxing.EncodeHintType
18+
import com.google.zxing.MultiFormatWriter
19+
import com.google.zxing.WriterException
20+
import org.hackillinois.android.R
21+
import java.util.EnumMap
22+
23+
class RedeemFragment : Fragment() {
24+
25+
private lateinit var qrCodeImage: ImageView
26+
private lateinit var redeemViewModel: RedeemViewModel
27+
28+
override fun onCreateView(
29+
inflater: LayoutInflater,
30+
container: ViewGroup?,
31+
savedInstanceState: Bundle?
32+
): View? {
33+
val view = inflater.inflate(R.layout.fragment_point_shop_redeem, container, false)
34+
35+
// Handle Back Button Click
36+
val backButton: View = view.findViewById(R.id.title_textview_back)
37+
backButton.bringToFront()
38+
backButton.setOnClickListener {
39+
requireActivity().supportFragmentManager.popBackStack() // Go back to previous fragment
40+
}
41+
42+
qrCodeImage = view.findViewById(R.id.qr_code_placeholder)
43+
44+
// Initialize and observe the ViewModel
45+
redeemViewModel = ViewModelProvider(this).get(RedeemViewModel::class.java)
46+
redeemViewModel.qrCodeLiveData.observe(
47+
viewLifecycleOwner,
48+
Observer { qrString ->
49+
Log.d("RedeemFragment", "Updated QR Code: $qrString")
50+
updateQRView(qrString)
51+
}
52+
)
53+
54+
redeemViewModel.errorLiveData.observe(viewLifecycleOwner) { errorMessage ->
55+
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
56+
}
57+
58+
return view
59+
}
60+
61+
private fun updateQRView(qrString: String) {
62+
if (qrCodeImage.width > 0 && qrCodeImage.height > 0) {
63+
Log.d("RedeemFragment", "Generating QR with text: $qrString")
64+
val bitmap = generateQR(qrString)
65+
qrCodeImage.setImageBitmap(bitmap)
66+
}
67+
}
68+
69+
private fun generateQR(text: String): Bitmap {
70+
val width = qrCodeImage.width
71+
val height = qrCodeImage.height
72+
73+
val pixels = IntArray(width * height)
74+
val multiFormatWriter = MultiFormatWriter()
75+
val hints = EnumMap<EncodeHintType, Any>(EncodeHintType::class.java)
76+
hints[EncodeHintType.MARGIN] = 0
77+
78+
try {
79+
val bitMatrix = multiFormatWriter.encode(text, BarcodeFormat.QR_CODE, width, height, hints)
80+
val clear = Color.TRANSPARENT
81+
val solid = Color.BLACK
82+
for (x in 0 until width) {
83+
for (y in 0 until height) {
84+
pixels[y * width + x] = if (bitMatrix.get(x, y)) solid else clear
85+
}
86+
}
87+
} catch (e: WriterException) {
88+
e.printStackTrace()
89+
}
90+
return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888)
91+
}
92+
93+
override fun onResume() {
94+
super.onResume()
95+
redeemViewModel.startAutoRefresh() // Resume QR refresh when fragment is visible
96+
}
97+
98+
override fun onPause() {
99+
super.onPause()
100+
redeemViewModel.stopAutoRefresh() // Pause QR refresh when fragment is hidden
101+
}
102+
}

0 commit comments

Comments
 (0)