diff --git a/app/build.gradle b/app/build.gradle index a860c9b2..d8c7bad4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -129,6 +129,9 @@ dependencies { // Swipe-to-refresh implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' + + //Json Parsing + implementation 'com.google.code.gson:gson:2.8.9' } apply plugin: 'com.google.gms.google-services' diff --git a/app/src/main/java/org/hackillinois/android/API.kt b/app/src/main/java/org/hackillinois/android/API.kt index 1dc0418b..58ffd5fc 100644 --- a/app/src/main/java/org/hackillinois/android/API.kt +++ b/app/src/main/java/org/hackillinois/android/API.kt @@ -1,5 +1,6 @@ package org.hackillinois.android +import okhttp3.ResponseBody import org.hackillinois.android.database.entity.* import org.hackillinois.android.model.event.EventsList import org.hackillinois.android.model.event.ShiftsList @@ -16,6 +17,7 @@ import org.hackillinois.android.model.user.FavoritesResponse import org.hackillinois.android.model.version.Version import org.hackillinois.android.notifications.DeviceToken import retrofit2.Call +import retrofit2.Response import retrofit2.http.* interface API { @@ -69,6 +71,18 @@ interface API { @POST("shop/item/buy/") suspend fun buyShopItem(@Body body: ItemInstance): ShopItem + @POST("shop/cart/{itemId}") + suspend fun addItemCart(@Path("itemId") itemId: String): Response + + @GET("shop/cart/") + suspend fun getCart(): Cart + + @GET("shop/cart/qr/") + suspend fun getCartQRCode(): QRResponse + + @DELETE("shop/cart/{itemId}") + suspend fun removeItemCart(@Path("itemId") itemId: String): Response + @POST("shop/cart/redeem/") suspend fun redeemCart(@Body body: QRCode): Cart diff --git a/app/src/main/java/org/hackillinois/android/App.kt b/app/src/main/java/org/hackillinois/android/App.kt index fba00533..2c8d8849 100644 --- a/app/src/main/java/org/hackillinois/android/App.kt +++ b/app/src/main/java/org/hackillinois/android/App.kt @@ -30,11 +30,12 @@ class App : Application() { return if (apiInitialized) apiInternal else getAPI("") } - Log.d("TOKEN", token) + Log.d("APPTOKEN", token) val interceptor = { chain: Interceptor.Chain -> val newRequest = chain.request().newBuilder() - .addHeader("Authorization", token) + .addHeader("Authorization", "Bearer $token") + .addHeader("Accept", "application/json") .build() chain.proceed(newRequest) } diff --git a/app/src/main/java/org/hackillinois/android/database/entity/QRResponse.kt b/app/src/main/java/org/hackillinois/android/database/entity/QRResponse.kt new file mode 100644 index 00000000..553c7daa --- /dev/null +++ b/app/src/main/java/org/hackillinois/android/database/entity/QRResponse.kt @@ -0,0 +1,3 @@ +package org.hackillinois.android.database.entity + +data class QRResponse(val QRCode: String) diff --git a/app/src/main/java/org/hackillinois/android/view/shop/CartAdapter.kt b/app/src/main/java/org/hackillinois/android/view/shop/CartAdapter.kt new file mode 100644 index 00000000..bb836948 --- /dev/null +++ b/app/src/main/java/org/hackillinois/android/view/shop/CartAdapter.kt @@ -0,0 +1,86 @@ +package org.hackillinois.android.view.shop + +import android.content.Context +import android.graphics.Rect +import android.view.LayoutInflater +import android.view.TouchDelegate +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import org.hackillinois.android.R +import org.hackillinois.android.database.entity.ShopItem + +class CartAdapter( + private var cartItems: List>, + private val listener: OnQuantityChangeListener +) : RecyclerView.Adapter() { + + private lateinit var context: Context + + inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val textViewSticker: TextView = view.findViewById(R.id.text_view_sticker) + val quantityTextView: TextView = view.findViewById(R.id.number_text) + val shopItemImageView: ImageView = view.findViewById(R.id.image_view_sticker_symbol) + val plusButton: TextView = view.findViewById(R.id.button_plus) + val minusButton: TextView = view.findViewById(R.id.button_minus) + } + + private fun expandTouchArea(targetView: View, extraPadding: Int) { + val parentView = targetView.parent as? ViewGroup ?: return + parentView.post { + val rect = Rect() + targetView.getHitRect(rect) + rect.top -= extraPadding + rect.left -= extraPadding + rect.bottom += extraPadding + rect.right += extraPadding + parentView.touchDelegate = TouchDelegate(rect, targetView) + parentView.requestLayout() // Refresh layout so it applies + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.point_shop_cart_tile, parent, false) + context = parent.context + return ViewHolder(view) + } + + override fun getItemCount() = cartItems.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val (item, quantity) = cartItems[position] + holder.textViewSticker.text = item.name + holder.quantityTextView.text = quantity.toString() + Glide.with(context).load(item.imageURL).into(holder.shopItemImageView) + + // Increase quantity using plus button + holder.plusButton.setOnClickListener { + val newQuantity = quantity + 1 + listener.onIncreaseQuantity(item, newQuantity) + } + + expandTouchArea(holder.plusButton, 100) + + // Decrease quantity using minus button (allowing 0) + holder.minusButton.setOnClickListener { + val newQuantity = quantity - 1 + listener.onDecreaseQuantity(item, newQuantity) + } + + expandTouchArea(holder.minusButton, 100) + } + + fun updateCart(newCartItems: List>) { + this.cartItems = newCartItems + notifyDataSetChanged() + } + + interface OnQuantityChangeListener { + fun onIncreaseQuantity(item: ShopItem, newQuantity: Int) + fun onDecreaseQuantity(item: ShopItem, newQuantity: Int) + } +} diff --git a/app/src/main/java/org/hackillinois/android/view/shop/CartFragment.kt b/app/src/main/java/org/hackillinois/android/view/shop/CartFragment.kt new file mode 100644 index 00000000..4be20170 --- /dev/null +++ b/app/src/main/java/org/hackillinois/android/view/shop/CartFragment.kt @@ -0,0 +1,120 @@ +package org.hackillinois.android.view.shop + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.launch +import org.hackillinois.android.App +import org.hackillinois.android.R +import org.hackillinois.android.database.entity.Cart +import org.hackillinois.android.database.entity.ShopItem + +class CartFragment : Fragment(), CartAdapter.OnQuantityChangeListener { + + private lateinit var recyclerView: RecyclerView + private lateinit var cartAdapter: CartAdapter + private var cartItems: List> = listOf() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_point_shop_cart, container, false) + + recyclerView = view.findViewById(R.id.recyclerview_point_shop) + recyclerView.layoutManager = GridLayoutManager(context, 2) + + cartAdapter = CartAdapter(cartItems, this) + recyclerView.adapter = cartAdapter + + fetchCartData() + + val backButton: View = view.findViewById(R.id.backButton) + backButton.bringToFront() + backButton.setOnClickListener { + requireActivity().supportFragmentManager.popBackStack() // Go back to previous fragment + } + + val redeemButton: View = view.findViewById(R.id.redeemButton) + redeemButton.setOnClickListener { + val redeemFragment = RedeemFragment() + requireActivity().supportFragmentManager.beginTransaction() + .replace(R.id.contentFrame, redeemFragment) + .addToBackStack(null) + .commit() + } + + return view + } + + private fun fetchCartData() { + lifecycleScope.launch { + try { + val shopItems: List = App.getAPI().shop() + val cart: Cart = App.getAPI().getCart() + val items = mutableListOf>() + + for ((itemId, quantity) in cart.items) { + val shopItem = shopItems.find { it.itemId == itemId } + if (shopItem != null) { + items.add(Pair(shopItem, quantity)) + } else { + Log.e("CartFragment", "ShopItem not found for itemId: $itemId") + } + } + cartItems = items + cartAdapter.updateCart(cartItems) + } catch (e: Exception) { + Log.e("CartFragment", "Error fetching cart items", e) + } + } + } + + override fun onIncreaseQuantity(item: ShopItem, newQuantity: Int) { + lifecycleScope.launch { + try { + val response = App.getAPI().addItemCart(item.itemId) + if (response.isSuccessful) { + Log.d("CartDebug", "Item added: ${response.body()}") + fetchCartData() // Refresh cart + } else { + Log.e("CartDebug", "Failed to add item: ${response.code()}") + } + } catch (e: Exception) { + Log.e("CartDebug", "Error adding item to cart", e) + } + } + } + + override fun onDecreaseQuantity(item: ShopItem, newQuantity: Int) { + lifecycleScope.launch { + try { + val response = App.getAPI().removeItemCart(item.itemId) + if (response.isSuccessful) { + Log.d("CartDebug", "Item removed: ${response.body()}") + if (newQuantity == 0) { + // If quantity is zero, remove the item from UI + fetchCartData() + } else { + // Just update the UI without full fetch + cartItems = cartItems.map { + if (it.first.itemId == item.itemId) Pair(it.first, newQuantity) else it + } + cartAdapter.updateCart(cartItems) + } + } else { + Log.e("CartDebug", "Failed to remove item: ${response.code()}") + } + } catch (e: Exception) { + Log.e("CartDebug", "Error removing item from cart", e) + } + } + } +} diff --git a/app/src/main/java/org/hackillinois/android/view/shop/RedeemFragment.kt b/app/src/main/java/org/hackillinois/android/view/shop/RedeemFragment.kt new file mode 100644 index 00000000..a3baa0cb --- /dev/null +++ b/app/src/main/java/org/hackillinois/android/view/shop/RedeemFragment.kt @@ -0,0 +1,102 @@ +package org.hackillinois.android.view.shop + +import RedeemViewModel +import android.graphics.Bitmap +import android.graphics.Color +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import com.google.zxing.BarcodeFormat +import com.google.zxing.EncodeHintType +import com.google.zxing.MultiFormatWriter +import com.google.zxing.WriterException +import org.hackillinois.android.R +import java.util.EnumMap + +class RedeemFragment : Fragment() { + + private lateinit var qrCodeImage: ImageView + private lateinit var redeemViewModel: RedeemViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_point_shop_redeem, container, false) + + // Handle Back Button Click + val backButton: View = view.findViewById(R.id.title_textview_back) + backButton.bringToFront() + backButton.setOnClickListener { + requireActivity().supportFragmentManager.popBackStack() // Go back to previous fragment + } + + qrCodeImage = view.findViewById(R.id.qr_code_placeholder) + + // Initialize and observe the ViewModel + redeemViewModel = ViewModelProvider(this).get(RedeemViewModel::class.java) + redeemViewModel.qrCodeLiveData.observe( + viewLifecycleOwner, + Observer { qrString -> + Log.d("RedeemFragment", "Updated QR Code: $qrString") + updateQRView(qrString) + } + ) + + redeemViewModel.errorLiveData.observe(viewLifecycleOwner) { errorMessage -> + Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show() + } + + return view + } + + private fun updateQRView(qrString: String) { + if (qrCodeImage.width > 0 && qrCodeImage.height > 0) { + Log.d("RedeemFragment", "Generating QR with text: $qrString") + val bitmap = generateQR(qrString) + qrCodeImage.setImageBitmap(bitmap) + } + } + + private fun generateQR(text: String): Bitmap { + val width = qrCodeImage.width + val height = qrCodeImage.height + + val pixels = IntArray(width * height) + val multiFormatWriter = MultiFormatWriter() + val hints = EnumMap(EncodeHintType::class.java) + hints[EncodeHintType.MARGIN] = 0 + + try { + val bitMatrix = multiFormatWriter.encode(text, BarcodeFormat.QR_CODE, width, height, hints) + val clear = Color.TRANSPARENT + val solid = Color.BLACK + for (x in 0 until width) { + for (y in 0 until height) { + pixels[y * width + x] = if (bitMatrix.get(x, y)) solid else clear + } + } + } catch (e: WriterException) { + e.printStackTrace() + } + return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888) + } + + override fun onResume() { + super.onResume() + redeemViewModel.startAutoRefresh() // Resume QR refresh when fragment is visible + } + + override fun onPause() { + super.onPause() + redeemViewModel.stopAutoRefresh() // Pause QR refresh when fragment is hidden + } +} diff --git a/app/src/main/java/org/hackillinois/android/view/shop/ShopAdapter.kt b/app/src/main/java/org/hackillinois/android/view/shop/ShopAdapter.kt index 1ca447b2..8b4d5fc5 100644 --- a/app/src/main/java/org/hackillinois/android/view/shop/ShopAdapter.kt +++ b/app/src/main/java/org/hackillinois/android/view/shop/ShopAdapter.kt @@ -7,14 +7,13 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView -import android.widget.Toast import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import kotlinx.android.synthetic.main.shop_tile.view.* import org.hackillinois.android.R import org.hackillinois.android.database.entity.ShopItem -class ShopAdapter(private var itemList: List) : +class ShopAdapter(private var itemList: List, private val buyItemListener: OnBuyItemListener) : RecyclerView.Adapter() { private lateinit var context: Context inner class ViewHolder(parent: View) : RecyclerView.ViewHolder(parent) @@ -22,7 +21,7 @@ class ShopAdapter(private var itemList: List) : // onCreateViewHolder used to display scrollable list of items // implemented as part of RecyclerView's adapter, responsible for creating new ViewHolder objects override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val layoutResource = R.layout.shop_tile + val layoutResource = R.layout.point_shop_tile val view = LayoutInflater.from(parent.context).inflate(layoutResource, parent, false) val viewHolder = ViewHolder(view) context = parent.context @@ -41,21 +40,8 @@ class ShopAdapter(private var itemList: List) : private fun bind(item: ShopItem, itemView: View, position: Int) { itemView.apply { - // set on click listener on item price + quantity "button" section - shopItemListenerView.setOnClickListener { - Toast.makeText(itemView.context, R.string.shop_toast_text, Toast.LENGTH_SHORT).show() - } - - // set the top brown divider for the first item to be visible - if (position == 1) { - val topDivider: TextView = itemView.findViewById(R.id.brownDividerTop) - topDivider.visibility = View.VISIBLE - } else { - val topDivider: TextView = itemView.findViewById(R.id.brownDividerTop) - topDivider.visibility = View.GONE - } - - shopItemTextView.text = item.name + val textViewSticker: TextView = findViewById(R.id.text_view_sticker) + textViewSticker.text = item.name priceTextView.text = item.price.toString() val quantity = item.quantity @@ -65,17 +51,28 @@ class ShopAdapter(private var itemList: List) : quantityTextView.text = resources.getString(R.string.shopquantity, quantity) } - val shopItemImageView: ImageView = itemView.findViewById(R.id.shopItemImageView) + val shopItemImageView: ImageView = itemView.findViewById(R.id.image_view_sticker_symbol) try { Glide.with(context).load(item.imageURL).into(shopItemImageView) } catch (e: Exception) { Log.d("Shop Glide Error", e.message.toString()) } + val plusButton: ImageView = findViewById(R.id.plusButton) + + plusButton.setOnClickListener { + Log.d("CartDebug", "Plus button clicked!") + Log.d("Item ID: ", "" + item.itemId) + buyItemListener.onBuyItem(item) + } } } fun updateShop(shopItem: List) { - this.itemList = shopItem + this.itemList = shopItem.sortedBy { it.quantity == 0 } // Move out-of-stock items to the end notifyDataSetChanged() } + + interface OnBuyItemListener { + fun onBuyItem(item: ShopItem) + } } diff --git a/app/src/main/java/org/hackillinois/android/view/shop/ShopFragment.kt b/app/src/main/java/org/hackillinois/android/view/shop/ShopFragment.kt index dfd106a7..76d69f36 100644 --- a/app/src/main/java/org/hackillinois/android/view/shop/ShopFragment.kt +++ b/app/src/main/java/org/hackillinois/android/view/shop/ShopFragment.kt @@ -2,37 +2,51 @@ package org.hackillinois.android.view.shop import android.content.Context import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import android.widget.Toast import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.synthetic.main.fragment_point_shop.coin_total_textview +import com.bumptech.glide.Glide +import kotlinx.android.synthetic.main.fragment_point_shop.number_of_coins_textview import kotlinx.android.synthetic.main.fragment_point_shop.view.recyclerview_point_shop +import kotlinx.coroutines.launch +import org.hackillinois.android.App import org.hackillinois.android.R import org.hackillinois.android.common.JWTUtilities import org.hackillinois.android.database.entity.Profile import org.hackillinois.android.database.entity.ShopItem import org.hackillinois.android.viewmodel.ShopViewModel -class ShopFragment : Fragment() { +class ShopFragment : Fragment(), ShopAdapter.OnBuyItemListener { companion object { fun newInstance() = ShopFragment() } private lateinit var shopViewModel: ShopViewModel - private var shop: List = listOf() private lateinit var recyclerView: RecyclerView private lateinit var mLayoutManager: LinearLayoutManager private lateinit var mAdapter: ShopAdapter + private lateinit var sticker1ImageView: ImageView + private lateinit var sticker2ImageView: ImageView + private lateinit var sticker1TextView: TextView + private lateinit var sticker2TextView: TextView + private lateinit var priceTextView1: TextView + private lateinit var priceTextView2: TextView + private lateinit var quantityTextView1: TextView + private lateinit var quantityTextView2: TextView + private lateinit var merchButton: TextView private lateinit var raffleButton: TextView @@ -42,6 +56,9 @@ class ShopFragment : Fragment() { // Merch tab is default selected private var showingMerch: Boolean = true + private lateinit var miniTile1: View + private lateinit var miniTile2: View + override fun onPause() { super.onPause() shopViewModel.stopTimer() @@ -50,6 +67,7 @@ class ShopFragment : Fragment() { override fun onResume() { super.onResume() shopViewModel.startTimer() +// updateShopUI() } override fun onCreate(savedInstanceState: Bundle?) { @@ -65,25 +83,38 @@ class ShopFragment : Fragment() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { val view = inflater.inflate(R.layout.fragment_point_shop, container, false) + sticker1ImageView = view.findViewById(R.id.image_view_sticker1_symbol) + sticker2ImageView = view.findViewById(R.id.image_view_sticker2_symbol) + sticker1TextView = view.findViewById(R.id.text_view_sticker1) + sticker2TextView = view.findViewById(R.id.text_view_sticker2) + priceTextView1 = view.findViewById(R.id.priceTextView1) + priceTextView2 = view.findViewById(R.id.priceTextView2) + quantityTextView1 = view.findViewById(R.id.quantityTextView1) + quantityTextView2 = view.findViewById(R.id.quantityTextView2) + merchButton = view.findViewById(R.id.merchButton) raffleButton = view.findViewById(R.id.raffleButton) - mAdapter = ShopAdapter(shop) + recyclerView = view.recyclerview_point_shop - recyclerView = view.recyclerview_point_shop.apply { - mLayoutManager = LinearLayoutManager(context) - this.layoutManager = mLayoutManager - this.adapter = mAdapter - } + val plusButton1: ImageView = view.findViewById(R.id.plusButton1) + val plusButton2: ImageView = view.findViewById(R.id.plusButton2) +// miniTile1 = view.findViewById(R.id.mini_tile_1) +// miniTile2 = view.findViewById(R.id.mini_tile_2) shopViewModel.shopLiveData.observe( viewLifecycleOwner, - Observer { - // will split shop items into Merch or Raffle category - updateShopItems(it) + Observer { shopItems -> + // Split the shop items into Merch or Raffle category + updateShopUI() + updateShopItems(shopItems) }, ) @@ -92,8 +123,8 @@ class ShopFragment : Fragment() { if (hasLoggedIn() && isAttendee()) { // set coin views visible for attendee - val coinBg: TextView = view.findViewById(R.id.total_coin_view) - val coinText: TextView = view.findViewById(R.id.coin_total_textview) + val coinBg: TextView = view.findViewById(R.id.number_of_coins_background) + val coinText: TextView = view.findViewById(R.id.number_of_coins_textview) val coinImg: ImageView = view.findViewById(R.id.coin_imageview) coinBg.visibility = View.VISIBLE coinText.visibility = View.VISIBLE @@ -107,32 +138,124 @@ class ShopFragment : Fragment() { ) } + val cartTextView: TextView = view.findViewById(R.id.text_view_cart) + cartTextView.bringToFront() + + cartTextView.setOnClickListener { + Log.d("ShopFragment", "Cart image clicked") + val cartFragment = CartFragment() + val transaction = requireActivity().supportFragmentManager.beginTransaction() + transaction.replace(R.id.contentFrame, cartFragment) + transaction.addToBackStack(null) // Optional, for back navigation + transaction.commit() + } + + plusButton1.setOnClickListener { buyFirstItem() } + plusButton2.setOnClickListener { buySecondItem() } + return view } // Called in onCreateView within shopLiveData.observe private fun updateShopItems(newShop: List) { - // Split the shop items into Merch and Raffle items - merchItems = newShop.filter { !it.isRaffle } - raffleItems = newShop.filter { it.isRaffle } + // Split shop items into categories + merchItems = newShop.filter { !it.isRaffle }.sortedBy { it.quantity == 0 } + raffleItems = newShop.filter { it.isRaffle }.sortedBy { it.quantity == 0 } - // Update the UI based on the selected button - updateShopUI() + // **Update only the RecyclerView items** + val recyclerViewItems = if (showingMerch) { + if (merchItems.size > 2) merchItems.subList(2, merchItems.size) else listOf() + } else { + raffleItems + } + + // Update adapter + mAdapter.updateShop(recyclerViewItems) } private fun updateShopUI() { - // if showingMerch variable is True based on selected button, show merch items. else, show raffle - val itemsToShow = if (showingMerch) merchItems else raffleItems - mAdapter.updateShop(itemsToShow) + // Sort items so that out-of-stock items are pushed to the end + val sortedItems = if (showingMerch) { + merchItems.sortedBy { it.quantity == 0 } + } else { + raffleItems.sortedBy { it.quantity == 0 } + } + + // Ensure first two items are always displayed in the fixed sticker views + if (sortedItems.isNotEmpty()) { + val firstItem = sortedItems[0] + updateStickerView(firstItem, sticker1ImageView, sticker1TextView, priceTextView1, quantityTextView1) + } else { + clearStickerView(sticker1ImageView, sticker1TextView, priceTextView1, quantityTextView1) + } + + if (sortedItems.size >= 2) { + val secondItem = sortedItems[1] + updateStickerView(secondItem, sticker2ImageView, sticker2TextView, priceTextView2, quantityTextView2) + } else { + clearStickerView(sticker2ImageView, sticker2TextView, priceTextView2, quantityTextView2) + } + + // RecyclerView should only show items *after* the first two + val recyclerViewItems = + if (sortedItems.size > 2) sortedItems.subList(2, sortedItems.size) else listOf() + + recyclerView.apply { + mLayoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + layoutManager = mLayoutManager + mAdapter = ShopAdapter(recyclerViewItems, this@ShopFragment) + adapter = mAdapter + } + } + + // Helper function to update a sticker view + private fun updateStickerView( + item: ShopItem, + imageView: ImageView, + textView: TextView, + priceView: TextView, + quantityView: TextView + ) { + Glide.with(requireContext()).load(item.imageURL).into(imageView) + textView.text = item.name + priceView.text = item.price.toString() + quantityView.text = when { + item.isRaffle -> resources.getString(R.string.unlimited) + item.quantity == 0 -> resources.getString(R.string.out_of_stock) + else -> resources.getString(R.string.shopquantity, item.quantity) + } + } + + // Helper function to clear sticker views if not enough items are available + private fun clearStickerView( + imageView: ImageView, + textView: TextView, + priceView: TextView, + quantityView: TextView + ) { + imageView.setImageDrawable(null) + textView.text = "" + priceView.text = "" + quantityView.text = "" } // update merch ViewModel on click private val merchClickListener = View.OnClickListener { if (!merchButton.isSelected) { merchButton.isSelected = true - merchButton.background = this.context?.let { it1 -> ContextCompat.getDrawable(it1, R.drawable.shop_selected_tab) } + merchButton.background = this.context?.let { it1 -> + ContextCompat.getDrawable( + it1, + R.drawable.point_shop_selected_background + ) + } raffleButton.isSelected = false - raffleButton.background = this.context?.let { it1 -> ContextCompat.getDrawable(it1, R.drawable.shop_unselected_tab) } + raffleButton.background = this.context?.let { it1 -> + ContextCompat.getDrawable( + it1, + R.drawable.point_shop_unselected_background + ) + } showingMerch = true updateShopUI() } @@ -142,9 +265,19 @@ class ShopFragment : Fragment() { private val raffleClickListener = View.OnClickListener { if (!raffleButton.isSelected) { raffleButton.isSelected = true - raffleButton.background = this.context?.let { it1 -> ContextCompat.getDrawable(it1, R.drawable.shop_selected_tab) } + raffleButton.background = this.context?.let { it1 -> + ContextCompat.getDrawable( + it1, + R.drawable.point_shop_selected_background + ) + } merchButton.isSelected = false - merchButton.background = this.context?.let { it1 -> ContextCompat.getDrawable(it1, R.drawable.shop_unselected_tab) } + merchButton.background = this.context?.let { it1 -> + ContextCompat.getDrawable( + it1, + R.drawable.point_shop_unselected_background + ) + } showingMerch = false updateShopUI() } @@ -164,6 +297,44 @@ class ShopFragment : Fragment() { private fun isAttendee(): Boolean { val context = requireActivity().applicationContext val prefString = context.getString(R.string.authorization_pref_file_key) - return context.getSharedPreferences(prefString, Context.MODE_PRIVATE).getString("provider", "") ?: "" == "github" + return context.getSharedPreferences(prefString, Context.MODE_PRIVATE) + .getString("provider", "") ?: "" == "github" + } + + override fun onBuyItem(item: ShopItem) { + // Implement your buying logic here (e.g., make a network call) + lifecycleScope.launch { + try { + val response = App.getAPI().addItemCart(item.itemId) + if (response.isSuccessful) { + // Update UI or local data with the new cart state + Log.d("CartDebug", "Item added: ${response.body()}") + Toast.makeText(requireContext(), "${item.name} redeemed successfully!", Toast.LENGTH_SHORT).show() + } else { + Log.e("CartDebug", "Failed to add item: ${response.code()}") + Toast.makeText(requireContext(), "Failed to add item: ${response.code()}", Toast.LENGTH_SHORT).show() + } + } catch (e: Exception) { + Log.e("CartDebug", "Error adding item to cart", e) + Toast.makeText(requireContext(), "Failed to add item: ${e.message}", Toast.LENGTH_SHORT).show() + } + updateShopUI() + } + } + + private fun buyFirstItem() { + if (merchItems.isNotEmpty()) { + val firstItem = merchItems[0] + Log.d("ShopFragment", "Buying: ${firstItem.name}") + onBuyItem(firstItem) + } + } + + private fun buySecondItem() { + if (merchItems.size >= 2) { + val secondItem = merchItems[1] + Log.d("ShopFragment", "Buying: ${secondItem.name}") + onBuyItem(secondItem) + } } } diff --git a/app/src/main/java/org/hackillinois/android/viewmodel/RedeemViewModel.kt b/app/src/main/java/org/hackillinois/android/viewmodel/RedeemViewModel.kt new file mode 100644 index 00000000..354389e1 --- /dev/null +++ b/app/src/main/java/org/hackillinois/android/viewmodel/RedeemViewModel.kt @@ -0,0 +1,76 @@ +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.google.gson.JsonParser +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import okhttp3.ResponseBody +import org.hackillinois.android.App +import org.hackillinois.android.database.entity.QRResponse +import retrofit2.HttpException + +class RedeemViewModel : ViewModel() { + + private val _qrCodeLiveData = MutableLiveData() + val qrCodeLiveData: LiveData = _qrCodeLiveData + + private val _errorLiveData = MutableLiveData() + val errorLiveData: LiveData = _errorLiveData + + private var refreshJob: Job? = null + + private fun fetchQRCode() { + viewModelScope.launch(Dispatchers.IO) { + try { + val response: QRResponse = App.getAPI().getCartQRCode() + val extractedQRCode = extractQRString(response.QRCode) + _qrCodeLiveData.postValue(extractedQRCode) + } catch (e: HttpException) { + val errorMessage = extractErrorMessage(e.response()?.errorBody()) + _errorLiveData.postValue("Error: $errorMessage") + } catch (e: Exception) { + e.printStackTrace() + _errorLiveData.postValue("Unexpected error: ${e.message}") + } + } + } + + private fun extractQRString(qrCodeUrl: String): String { + return qrCodeUrl.substringAfter("qr=", "Invalid QR Code") + } + + // Extract the error message from the response body + private fun extractErrorMessage(errorBody: ResponseBody?): String { + return try { + val jsonString = errorBody?.string() ?: return "Unknown error" + val jsonObject = JsonParser.parseString(jsonString).asJsonObject + jsonObject["message"]?.asString ?: "Unknown error" + } catch (e: Exception) { + "Failed to parse error response" + } + } + + fun startAutoRefresh() { + if (refreshJob?.isActive == true) return // Prevent duplicate jobs + + refreshJob = viewModelScope.launch { + while (isActive) { + fetchQRCode() + delay(15000) // Refresh every 15 seconds + } + } + } + + fun stopAutoRefresh() { + refreshJob?.cancel() // Stop refreshing when fragment is not visible + } + + override fun onCleared() { + super.onCleared() + stopAutoRefresh() + } +} diff --git a/app/src/main/res/drawable/point_shop_back_bg.xml b/app/src/main/res/drawable/point_shop_back_bg.xml new file mode 100644 index 00000000..05221243 --- /dev/null +++ b/app/src/main/res/drawable/point_shop_back_bg.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/point_shop_back_circle.xml b/app/src/main/res/drawable/point_shop_back_circle.xml new file mode 100644 index 00000000..4eedd9d6 --- /dev/null +++ b/app/src/main/res/drawable/point_shop_back_circle.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/point_shop_back_symbol.png b/app/src/main/res/drawable/point_shop_back_symbol.png new file mode 100644 index 00000000..7e098418 Binary files /dev/null and b/app/src/main/res/drawable/point_shop_back_symbol.png differ diff --git a/app/src/main/res/drawable/point_shop_borders.xml b/app/src/main/res/drawable/point_shop_borders.xml new file mode 100644 index 00000000..29d50b07 --- /dev/null +++ b/app/src/main/res/drawable/point_shop_borders.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/point_shop_cart_background.xml b/app/src/main/res/drawable/point_shop_cart_background.xml new file mode 100644 index 00000000..1467b3f3 --- /dev/null +++ b/app/src/main/res/drawable/point_shop_cart_background.xml @@ -0,0 +1,350 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/point_shop_cart_bg.png b/app/src/main/res/drawable/point_shop_cart_bg.png new file mode 100644 index 00000000..b258f2e4 Binary files /dev/null and b/app/src/main/res/drawable/point_shop_cart_bg.png differ diff --git a/app/src/main/res/drawable/point_shop_cart_symbol.xml b/app/src/main/res/drawable/point_shop_cart_symbol.xml new file mode 100644 index 00000000..953aba5b --- /dev/null +++ b/app/src/main/res/drawable/point_shop_cart_symbol.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/drawable/point_shop_close.xml b/app/src/main/res/drawable/point_shop_close.xml new file mode 100644 index 00000000..40950ffb --- /dev/null +++ b/app/src/main/res/drawable/point_shop_close.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/point_shop_cloud_side.xml b/app/src/main/res/drawable/point_shop_cloud_side.xml new file mode 100644 index 00000000..9a94573c --- /dev/null +++ b/app/src/main/res/drawable/point_shop_cloud_side.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/point_shop_cloud_top.xml b/app/src/main/res/drawable/point_shop_cloud_top.xml new file mode 100644 index 00000000..11a2728a --- /dev/null +++ b/app/src/main/res/drawable/point_shop_cloud_top.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/point_shop_coin_background.xml b/app/src/main/res/drawable/point_shop_coin_background.xml new file mode 100644 index 00000000..2d2ce2e0 --- /dev/null +++ b/app/src/main/res/drawable/point_shop_coin_background.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/point_shop_counter.xml b/app/src/main/res/drawable/point_shop_counter.xml new file mode 100644 index 00000000..04ab76e4 --- /dev/null +++ b/app/src/main/res/drawable/point_shop_counter.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/point_shop_counter_bottom.png b/app/src/main/res/drawable/point_shop_counter_bottom.png new file mode 100644 index 00000000..60242967 Binary files /dev/null and b/app/src/main/res/drawable/point_shop_counter_bottom.png differ diff --git a/app/src/main/res/drawable/point_shop_counter_middle.png b/app/src/main/res/drawable/point_shop_counter_middle.png new file mode 100644 index 00000000..42a29764 Binary files /dev/null and b/app/src/main/res/drawable/point_shop_counter_middle.png differ diff --git a/app/src/main/res/drawable/point_shop_currency.xml b/app/src/main/res/drawable/point_shop_currency.xml new file mode 100644 index 00000000..6b7f63ad --- /dev/null +++ b/app/src/main/res/drawable/point_shop_currency.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/point_shop_girl.xml b/app/src/main/res/drawable/point_shop_girl.xml new file mode 100644 index 00000000..04cf2034 --- /dev/null +++ b/app/src/main/res/drawable/point_shop_girl.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/point_shop_gradient_background.xml b/app/src/main/res/drawable/point_shop_gradient_background.xml new file mode 100644 index 00000000..8f9e76f3 --- /dev/null +++ b/app/src/main/res/drawable/point_shop_gradient_background.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/point_shop_number_bar.xml b/app/src/main/res/drawable/point_shop_number_bar.xml new file mode 100644 index 00000000..92072875 --- /dev/null +++ b/app/src/main/res/drawable/point_shop_number_bar.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/point_shop_plus_button.xml b/app/src/main/res/drawable/point_shop_plus_button.xml new file mode 100644 index 00000000..cfb37f11 --- /dev/null +++ b/app/src/main/res/drawable/point_shop_plus_button.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/app/src/main/res/drawable/point_shop_redeem.xml b/app/src/main/res/drawable/point_shop_redeem.xml new file mode 100644 index 00000000..03673346 --- /dev/null +++ b/app/src/main/res/drawable/point_shop_redeem.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/drawable/point_shop_selected_background.xml b/app/src/main/res/drawable/point_shop_selected_background.xml new file mode 100644 index 00000000..fe41624a --- /dev/null +++ b/app/src/main/res/drawable/point_shop_selected_background.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/drawable/point_shop_sticker.xml b/app/src/main/res/drawable/point_shop_sticker.xml new file mode 100644 index 00000000..f5668dfd --- /dev/null +++ b/app/src/main/res/drawable/point_shop_sticker.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/point_shop_sticker_coin_bg.xml b/app/src/main/res/drawable/point_shop_sticker_coin_bg.xml new file mode 100644 index 00000000..0e64891d --- /dev/null +++ b/app/src/main/res/drawable/point_shop_sticker_coin_bg.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/point_shop_sticker_symbol.xml b/app/src/main/res/drawable/point_shop_sticker_symbol.xml new file mode 100644 index 00000000..b8e2a27e --- /dev/null +++ b/app/src/main/res/drawable/point_shop_sticker_symbol.xml @@ -0,0 +1,483 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/point_shop_unselected_background.xml b/app/src/main/res/drawable/point_shop_unselected_background.xml new file mode 100644 index 00000000..2f478ba9 --- /dev/null +++ b/app/src/main/res/drawable/point_shop_unselected_background.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index cadbfbfb..43cbec63 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -203,7 +203,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" - app:layout_constraintGuide_percent="0.055" /> + app:layout_constraintGuide_begin="40dp" /> + android:background="@drawable/point_shop_gradient_background"> + + + + + + + + + + + + + + + + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.636" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + app:layout_constraintGuide_begin="44dp" /> + app:layout_constraintTop_toTopOf="@id/number_of_coins_textview" + app:layout_constraintEnd_toEndOf="@id/number_of_coins_textview" + app:layout_constraintBottom_toBottomOf="@id/number_of_coins_textview" + android:visibility="invisible"/> + android:src="@drawable/point_shop_currency" + app:layout_constraintEnd_toStartOf="@id/number_of_coins_textview" + app:layout_constraintTop_toTopOf="@id/number_of_coins_textview" + app:layout_constraintBottom_toBottomOf="@id/number_of_coins_textview" + android:visibility="invisible"/> + android:text="@string/blank" + android:visibility="invisible"/> - + + + + + + + + + + + + + + + app:layout_constraintVertical_bias="0.80" /> + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_point_shop_redeem.xml b/app/src/main/res/layout/fragment_point_shop_redeem.xml new file mode 100644 index 00000000..ffee1af2 --- /dev/null +++ b/app/src/main/res/layout/fragment_point_shop_redeem.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/point_shop_cart_tile.xml b/app/src/main/res/layout/point_shop_cart_tile.xml new file mode 100644 index 00000000..6dcdee13 --- /dev/null +++ b/app/src/main/res/layout/point_shop_cart_tile.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/point_shop_tile.xml b/app/src/main/res/layout/point_shop_tile.xml new file mode 100644 index 00000000..db123567 --- /dev/null +++ b/app/src/main/res/layout/point_shop_tile.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 9c409947..abe117d9 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -60,6 +60,11 @@ #C5673F #903D2B #F9C126 + + + #741029 + #612547 + #48A4A4 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d9ad26a4..4f271ad4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -84,6 +84,16 @@ RAFFLE Unlimited Head to the Point Shop location to scan and buy items! + CART + Sticker + - + + + 1 + BACK + SCAN HERE TO \nCOMPLETE PURCHASE + OUT OF STOCK + + PROFILE