diff --git a/app/src/main/java/com/example/pap_teste/FeaturedRestaurantAdapter.java b/app/src/main/java/com/example/pap_teste/FeaturedRestaurantAdapter.java new file mode 100644 index 0000000..4082357 --- /dev/null +++ b/app/src/main/java/com/example/pap_teste/FeaturedRestaurantAdapter.java @@ -0,0 +1,134 @@ +package com.example.pap_teste; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.example.pap_teste.models.Restaurant; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.ValueEventListener; + +import java.util.List; + +public class FeaturedRestaurantAdapter extends RecyclerView.Adapter { + private List restaurants; + private RestaurantAdapter.OnRestaurantClickListener listener; + + public FeaturedRestaurantAdapter(List restaurants, RestaurantAdapter.OnRestaurantClickListener listener) { + this.restaurants = restaurants; + this.listener = listener; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_restaurant_featured, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + Restaurant restaurant = restaurants.get(position); + holder.txtName.setText(restaurant.getName()); + holder.txtCategory.setText("• " + restaurant.getCategory()); + + if (holder.txtRating != null) { + if (restaurant.getRatingAvg() != null && restaurant.getRatingAvg() > 0) { + holder.txtRating.setText(String.format(java.util.Locale.getDefault(), "%.1f", restaurant.getRatingAvg())); + } else { + holder.txtRating.setText("Novo"); + } + } + + if (restaurant.getLogoUrl() != null && !restaurant.getLogoUrl().isEmpty()) { + com.bumptech.glide.Glide.with(holder.itemView.getContext()) + .load(restaurant.getLogoUrl()) + .centerCrop() + .into(holder.imgCover); + } else { + holder.imgCover.setImageResource(R.mipmap.ic_launcher); + } + + holder.itemView.setOnClickListener(v -> { + if (listener != null) { + listener.onRestaurantClick(restaurant); + } + }); + + if (holder.btnFavorite != null) { + FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser(); + if (user != null && user.getEmail() != null && restaurant.getEmail() != null) { + String encodedUserEmail = user.getEmail().replace(".", "_").replace("@", "_at_"); + String encodedRestEmail = restaurant.getEmail().replace(".", "_").replace("@", "_at_"); + DatabaseReference favRef = FirebaseDatabase.getInstance().getReference("users") + .child(encodedUserEmail).child("favorites").child(encodedRestEmail); + + // Read initial state + favRef.addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(@NonNull DataSnapshot snapshot) { + holder.btnFavorite.setTag(snapshot.exists()); + if (snapshot.exists()) { + holder.btnFavorite.setImageResource(android.R.drawable.btn_star_big_on); + holder.btnFavorite.setColorFilter(androidx.core.content.ContextCompat.getColor(holder.itemView.getContext(), R.color.colorPrimary)); + } else { + holder.btnFavorite.setImageResource(android.R.drawable.btn_star_big_off); + holder.btnFavorite.clearColorFilter(); + } + } + + @Override + public void onCancelled(@NonNull DatabaseError error) { } + }); + + holder.btnFavorite.setOnClickListener(v -> { + // Optimistic UI update + boolean isFav = holder.btnFavorite.getTag() != null && (Boolean) holder.btnFavorite.getTag(); + boolean newFavState = !isFav; + holder.btnFavorite.setTag(newFavState); + + if (newFavState) { + holder.btnFavorite.setImageResource(android.R.drawable.btn_star_big_on); + holder.btnFavorite.setColorFilter(androidx.core.content.ContextCompat.getColor(holder.itemView.getContext(), R.color.colorPrimary)); + favRef.setValue(restaurant); + com.google.android.material.snackbar.Snackbar.make(v, "Adicionado aos Favoritos!", com.google.android.material.snackbar.Snackbar.LENGTH_SHORT).show(); + } else { + holder.btnFavorite.setImageResource(android.R.drawable.btn_star_big_off); + holder.btnFavorite.clearColorFilter(); + favRef.removeValue(); + com.google.android.material.snackbar.Snackbar.make(v, "Removido dos Favoritos.", com.google.android.material.snackbar.Snackbar.LENGTH_SHORT).show(); + } + }); + } + } + } + + @Override + public int getItemCount() { + return restaurants.size(); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + TextView txtName, txtCategory, txtRating; + ImageView imgCover; + android.widget.ImageButton btnFavorite; + public ViewHolder(@NonNull View itemView) { + super(itemView); + txtName = itemView.findViewById(R.id.txtFeaturedName); + txtCategory = itemView.findViewById(R.id.txtFeaturedCategory); + txtRating = itemView.findViewById(R.id.txtFeaturedRating); + imgCover = itemView.findViewById(R.id.imgFeaturedCover); + btnFavorite = itemView.findViewById(R.id.btnFavorite); + } + } +} diff --git a/app/src/main/java/com/example/pap_teste/ReviewAdapter.java b/app/src/main/java/com/example/pap_teste/ReviewAdapter.java new file mode 100644 index 0000000..fb98f66 --- /dev/null +++ b/app/src/main/java/com/example/pap_teste/ReviewAdapter.java @@ -0,0 +1,75 @@ +package com.example.pap_teste; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.example.pap_teste.models.Review; + +import java.util.List; + +public class ReviewAdapter extends RecyclerView.Adapter { + private List reviews; + private String currentUserEmail; + private OnReviewDeleteListener deleteListener; + + public interface OnReviewDeleteListener { + void onDeleteClick(Review review); + } + + public ReviewAdapter(List reviews, String currentUserEmail, OnReviewDeleteListener deleteListener) { + this.reviews = reviews; + this.currentUserEmail = currentUserEmail; + this.deleteListener = deleteListener; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_review, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + Review review = reviews.get(position); + + holder.txtAuthor.setText(review.getAuthor() != null ? review.getAuthor() : "Anónimo"); + holder.txtText.setText(review.getText() != null ? review.getText() : ""); + holder.txtRating.setText(String.valueOf(review.getRating())); + + if (currentUserEmail != null && currentUserEmail.equals(review.getUserEmail())) { + holder.btnDelete.setVisibility(View.VISIBLE); + holder.btnDelete.setOnClickListener(v -> { + if (deleteListener != null) { + deleteListener.onDeleteClick(review); + } + }); + } else { + holder.btnDelete.setVisibility(View.GONE); + } + } + + @Override + public int getItemCount() { + return reviews.size(); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + TextView txtAuthor, txtText, txtRating; + ImageButton btnDelete; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + txtAuthor = itemView.findViewById(R.id.txtReviewAuthor); + txtText = itemView.findViewById(R.id.txtReviewText); + txtRating = itemView.findViewById(R.id.txtReviewRating); + btnDelete = itemView.findViewById(R.id.btnDeleteReview); + } + } +} diff --git a/app/src/main/java/com/example/pap_teste/components/InteractiveRatingBar.java b/app/src/main/java/com/example/pap_teste/components/InteractiveRatingBar.java new file mode 100644 index 0000000..6b0bc44 --- /dev/null +++ b/app/src/main/java/com/example/pap_teste/components/InteractiveRatingBar.java @@ -0,0 +1,167 @@ +package com.example.pap_teste.components; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import androidx.annotation.Nullable; + +import com.example.pap_teste.R; + +import java.util.ArrayList; +import java.util.List; + +public class InteractiveRatingBar extends LinearLayout { + + public interface OnRatingChangeListener { + void onRatingChanged(double rating); + } + + private OnRatingChangeListener listener; + private int numStars = 5; + private double currentRating = 5.0; + private List stars = new ArrayList<>(); + + // Size settings + private int starSizeDp = 40; + private int starPaddingDp = 4; + + public InteractiveRatingBar(Context context) { + super(context); + init(context); + } + + public InteractiveRatingBar(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public InteractiveRatingBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + private void init(Context context) { + setOrientation(HORIZONTAL); + setGravity(android.view.Gravity.CENTER); + + int sizePx = (int) (starSizeDp * context.getResources().getDisplayMetrics().density); + int paddingPx = (int) (starPaddingDp * context.getResources().getDisplayMetrics().density); + + for (int i = 0; i < numStars; i++) { + ImageView star = new ImageView(context); + LayoutParams params = new LayoutParams(sizePx, sizePx); + params.setMargins(paddingPx, paddingPx, paddingPx, paddingPx); + star.setLayoutParams(params); + + // Set scale type to fit center + star.setScaleType(ImageView.ScaleType.FIT_CENTER); + + addView(star); + stars.add(star); + } + + updateStars(currentRating, false); + } + + public void setOnRatingChangeListener(OnRatingChangeListener listener) { + this.listener = listener; + } + + public double getRating() { + return currentRating; + } + + public void setRating(double rating) { + this.currentRating = Math.max(0.0, Math.min(rating, numStars)); + updateStars(currentRating, false); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) { + float x = event.getX(); + calculateRatingFromTouch(x); + return true; + } + return super.onTouchEvent(event); + } + + private void calculateRatingFromTouch(float x) { + if (stars.isEmpty()) return; + + double newRating = 0; + + for (int i = 0; i < numStars; i++) { + ImageView star = stars.get(i); + float starLeft = star.getLeft(); + float starRight = star.getRight(); + float starCenter = starLeft + (star.getWidth() / 2f); + + if (x >= starRight) { + newRating = i + 1.0; + } else if (x >= starCenter && x < starRight) { + newRating = i + 1.0; + break; + } else if (x >= starLeft && x < starCenter) { + newRating = i + 0.5; + break; + } else if (x < starLeft && i == 0) { + newRating = 0.5; // Minimum 0.5 when touched + break; + } + } + + // Clamp values + newRating = Math.max(0.5, Math.min(newRating, numStars)); + + if (newRating != currentRating) { + currentRating = newRating; + updateStars(currentRating, true); + if (listener != null) { + listener.onRatingChanged(currentRating); + } + } + } + + private void updateStars(double rating, boolean animate) { + for (int i = 0; i < numStars; i++) { + ImageView star = stars.get(i); + int previousResId = (Integer) (star.getTag() != null ? star.getTag() : 0); + int newResId; + + double starVal = i + 1; + if (rating >= starVal) { + newResId = R.drawable.ic_star_full; + } else if (rating >= starVal - 0.5) { + newResId = R.drawable.ic_star_half; + } else { + newResId = R.drawable.ic_star_empty; + } + + if (previousResId != newResId) { + star.setImageResource(newResId); + star.setTag(newResId); + + if (animate) { + // Micro-animation (Bounce scale) + star.animate() + .scaleX(1.2f) + .scaleY(1.2f) + .setDuration(100) + .withEndAction(() -> { + star.animate() + .scaleX(1.0f) + .scaleY(1.0f) + .setDuration(100) + .start(); + }) + .start(); + } + } + } + } +} diff --git a/app/src/main/java/com/example/pap_teste/models/Review.java b/app/src/main/java/com/example/pap_teste/models/Review.java new file mode 100644 index 0000000..eef3b2a --- /dev/null +++ b/app/src/main/java/com/example/pap_teste/models/Review.java @@ -0,0 +1,34 @@ +package com.example.pap_teste.models; + +public class Review { + private String key; + private String author; + private String text; + private float rating; + private String userEmail; + + public Review() {} + + public Review(String key, String author, String text, float rating, String userEmail) { + this.key = key; + this.author = author; + this.text = text; + this.rating = rating; + this.userEmail = userEmail; + } + + public String getKey() { return key; } + public void setKey(String key) { this.key = key; } + + public String getAuthor() { return author; } + public void setAuthor(String author) { this.author = author; } + + public String getText() { return text; } + public void setText(String text) { this.text = text; } + + public float getRating() { return rating; } + public void setRating(float rating) { this.rating = rating; } + + public String getUserEmail() { return userEmail; } + public void setUserEmail(String userEmail) { this.userEmail = userEmail; } +} diff --git a/app/src/main/res/drawable/bg_circle_white.xml b/app/src/main/res/drawable/bg_circle_white.xml new file mode 100644 index 0000000..28fca31 --- /dev/null +++ b/app/src/main/res/drawable/bg_circle_white.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/bg_input_modern.xml b/app/src/main/res/drawable/bg_input_modern.xml new file mode 100644 index 0000000..2a8ce8f --- /dev/null +++ b/app/src/main/res/drawable/bg_input_modern.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/app/src/main/res/drawable/bg_rounded_card.xml b/app/src/main/res/drawable/bg_rounded_card.xml new file mode 100644 index 0000000..dd6a94b --- /dev/null +++ b/app/src/main/res/drawable/bg_rounded_card.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/gradient_shadow_up.xml b/app/src/main/res/drawable/gradient_shadow_up.xml new file mode 100644 index 0000000..abdfb49 --- /dev/null +++ b/app/src/main/res/drawable/gradient_shadow_up.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/drawable/ic_star_empty.xml b/app/src/main/res/drawable/ic_star_empty.xml new file mode 100644 index 0000000..6822ac3 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_empty.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_full.xml b/app/src/main/res/drawable/ic_star_full.xml new file mode 100644 index 0000000..9da8176 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_full.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_half.xml b/app/src/main/res/drawable/ic_star_half.xml new file mode 100644 index 0000000..03fb4ee --- /dev/null +++ b/app/src/main/res/drawable/ic_star_half.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/layout/dialog_leave_review.xml b/app/src/main/res/layout/dialog_leave_review.xml new file mode 100644 index 0000000..5d09b3a --- /dev/null +++ b/app/src/main/res/layout/dialog_leave_review.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_reviews_list.xml b/app/src/main/res/layout/dialog_reviews_list.xml new file mode 100644 index 0000000..6dd3a31 --- /dev/null +++ b/app/src/main/res/layout/dialog_reviews_list.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_restaurant_featured.xml b/app/src/main/res/layout/item_restaurant_featured.xml new file mode 100644 index 0000000..f46e7ed --- /dev/null +++ b/app/src/main/res/layout/item_restaurant_featured.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_review.xml b/app/src/main/res/layout/item_review.xml new file mode 100644 index 0000000..6a6bb52 --- /dev/null +++ b/app/src/main/res/layout/item_review.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml new file mode 100644 index 0000000..34484ae --- /dev/null +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -0,0 +1,15 @@ + + + + + +