app atualizada para apresentação

This commit is contained in:
2026-06-16 14:19:41 +01:00
parent 541f7300bd
commit ae1bc0b3df
9 changed files with 379 additions and 99 deletions

View File

@@ -39,12 +39,18 @@ public class LoginActivity extends AppCompatActivity {
checkRememberMe = findViewById(R.id.checkRememberMe);
criarContaTextView = findViewById(R.id.txtRegister);
txtForgotPassword = findViewById(R.id.txtForgotPassword);
TextView txtGuestMode = findViewById(R.id.txtGuestMode);
mAuth = FirebaseAuth.getInstance();
btnLogin.setOnClickListener(v -> loginUser());
criarContaTextView.setOnClickListener(view -> criarConta());
txtForgotPassword.setOnClickListener(view -> recuperarPassword());
txtGuestMode.setOnClickListener(v -> {
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
startActivity(intent);
finish();
});
// Carregar credenciais guardadas
loadSavedCredentials();

View File

@@ -22,6 +22,12 @@ import androidx.appcompat.app.AppCompatActivity;
import com.example.vdcscore.databinding.ActivityMainBinding;
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 androidx.annotation.NonNull;
import java.io.InputStream;
import android.app.AlertDialog;
@@ -43,6 +49,14 @@ public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
private ActivityResultLauncher<String> mGetContent;
private java.util.Set<String> favoriteClubs = new java.util.HashSet<>();
private DatabaseReference favoritesRef;
private ValueEventListener favoritesListener;
private DatabaseReference senioresRef;
private ValueEventListener senioresListener;
private DatabaseReference junioresRef;
private ValueEventListener junioresListener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -87,6 +101,9 @@ public class MainActivity extends AppCompatActivity {
// Carregar dados do utilizador no menu lateral
loadUserDataInNavHeader();
// Inicializar o sistema de notificações para favoritos
setupNotificationSystem();
}
private void loadUserDataInNavHeader() {
@@ -212,4 +229,157 @@ public class MainActivity extends AppCompatActivity {
// Atualizar dados do utilizador quando voltar à activity
loadUserDataInNavHeader();
}
private void setupNotificationSystem() {
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
if (user == null) return;
createNotificationChannel();
// Listen to favorites
favoritesRef = FirebaseDatabase.getInstance().getReference("users")
.child(user.getUid())
.child("favorites");
favoritesListener = new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
favoriteClubs.clear();
for (DataSnapshot favSnap : snapshot.getChildren()) {
if (Boolean.TRUE.equals(favSnap.getValue(Boolean.class))) {
favoriteClubs.add(favSnap.getKey());
}
}
}
@Override
public void onCancelled(@NonNull DatabaseError error) {}
};
favoritesRef.addValueEventListener(favoritesListener);
// Listen to seniores matches
senioresRef = FirebaseDatabase.getInstance().getReference("jornadas").child("seniores");
senioresListener = new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
checkMatchesAndNotify(snapshot);
}
@Override
public void onCancelled(@NonNull DatabaseError error) {}
};
senioresRef.addValueEventListener(senioresListener);
// Listen to juniores matches
junioresRef = FirebaseDatabase.getInstance().getReference("jornadas").child("juniores");
junioresListener = new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
checkMatchesAndNotify(snapshot);
}
@Override
public void onCancelled(@NonNull DatabaseError error) {}
};
junioresRef.addValueEventListener(junioresListener);
}
private void checkMatchesAndNotify(DataSnapshot categorySnapshot) {
if (favoriteClubs.isEmpty()) return;
android.content.SharedPreferences prefs = getSharedPreferences("NotifiedMatchesPrefs", android.content.Context.MODE_PRIVATE);
android.content.SharedPreferences.Editor editor = prefs.edit();
for (DataSnapshot jornadaSnap : categorySnapshot.getChildren()) {
for (DataSnapshot matchSnap : jornadaSnap.getChildren()) {
com.example.vdcscore.ui.gallery.Match match = matchSnap.getValue(com.example.vdcscore.ui.gallery.Match.class);
if (match != null) {
match.setId(matchSnap.getKey());
if (match.isPlayed() && match.getId() != null) {
boolean involvesFavorite = false;
String favoriteName = "";
String homeSanitized = com.example.vdcscore.utils.FirebaseErrorUtils.sanitizeKey(match.getHomeName());
String awaySanitized = com.example.vdcscore.utils.FirebaseErrorUtils.sanitizeKey(match.getAwayName());
if (homeSanitized != null && favoriteClubs.contains(homeSanitized)) {
involvesFavorite = true;
favoriteName = match.getHomeName();
} else if (awaySanitized != null && favoriteClubs.contains(awaySanitized)) {
involvesFavorite = true;
favoriteName = match.getAwayName();
}
if (involvesFavorite) {
String matchPrefKey = "notified_" + match.getId();
if (!prefs.getBoolean(matchPrefKey, false)) {
// Trigger notification!
triggerLocalNotification(match, favoriteName);
// Mark as notified
editor.putBoolean(matchPrefKey, true);
}
}
}
}
}
}
editor.apply();
}
private void triggerLocalNotification(com.example.vdcscore.ui.gallery.Match match, String favoriteClub) {
String homeScoreStr = match.getHomeScore() != null ? String.valueOf(match.getHomeScore()) : "0";
String awayScoreStr = match.getAwayScore() != null ? String.valueOf(match.getAwayScore()) : "0";
String title = "Fim de Jogo - " + favoriteClub;
String message = match.getHomeName() + " " + homeScoreStr + " - " + awayScoreStr + " " + match.getAwayName();
androidx.core.app.NotificationCompat.Builder builder = new androidx.core.app.NotificationCompat.Builder(this, "vdcscore_notifications")
.setSmallIcon(R.drawable.ic_soccer)
.setContentTitle(title)
.setContentText(message)
.setPriority(androidx.core.app.NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true);
// Click action: open MainActivity
android.content.Intent intent = new android.content.Intent(this, MainActivity.class);
intent.setFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK | android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK);
android.app.PendingIntent pendingIntent = android.app.PendingIntent.getActivity(
this, 0, intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT | android.app.PendingIntent.FLAG_IMMUTABLE);
builder.setContentIntent(pendingIntent);
android.app.NotificationManager notificationManager = (android.app.NotificationManager) getSystemService(android.content.Context.NOTIFICATION_SERVICE);
if (notificationManager != null) {
int notificationId = match.getId().hashCode();
notificationManager.notify(notificationId, builder.build());
}
}
private void createNotificationChannel() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
CharSequence name = "Resultados dos Jogos";
String description = "Notificações de resultados dos jogos dos seus clubes favoritos";
int importance = android.app.NotificationManager.IMPORTANCE_DEFAULT;
android.app.NotificationChannel channel = new android.app.NotificationChannel("vdcscore_notifications", name, importance);
channel.setDescription(description);
android.app.NotificationManager notificationManager = getSystemService(android.app.NotificationManager.class);
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel);
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (favoritesRef != null && favoritesListener != null) {
favoritesRef.removeEventListener(favoritesListener);
}
if (senioresRef != null && senioresListener != null) {
senioresRef.removeEventListener(senioresListener);
}
if (junioresRef != null && junioresListener != null) {
junioresRef.removeEventListener(junioresListener);
}
}
}

View File

@@ -26,6 +26,11 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.widget.ImageView;
import android.widget.TextView;
import android.content.Intent;
import android.app.AlertDialog;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.example.vdcscore.LoginActivity;
public class ClubDetailFragment extends Fragment {
@@ -33,6 +38,10 @@ public class ClubDetailFragment extends Fragment {
private DatabaseReference mDatabase;
private String clubId;
private String escalao;
private String currentClubName;
private boolean isFavorite = false;
private ValueEventListener favoriteListener;
private DatabaseReference favoriteRef;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -96,6 +105,8 @@ public class ClubDetailFragment extends Fragment {
private void populateStats(Team team) {
binding.textDetailClubName.setText(team.getNome());
currentClubName = team.getNome();
setupFavoriteButton();
binding.labelStats.setVisibility(View.VISIBLE);
binding.containerStats.setVisibility(View.VISIBLE);
binding.btnCompare.setVisibility(View.VISIBLE);
@@ -131,6 +142,8 @@ public class ClubDetailFragment extends Fragment {
private void populateClubInfo(Club club) {
binding.textDetailClubName.setText(club.getName());
currentClubName = club.getName();
setupFavoriteButton();
// Bind new club info fields
binding.textDetailPresident.setText(club.getPresident() != null && !club.getPresident().isEmpty() ? club.getPresident() : "---");
@@ -325,9 +338,82 @@ public class ClubDetailFragment extends Fragment {
binding.textCompareG2.setTypeface(null, t2.getGolos_marcados() > t1.getGolos_marcados() ? android.graphics.Typeface.BOLD : android.graphics.Typeface.NORMAL);
}
private void setupFavoriteButton() {
if (currentClubName == null || binding == null) return;
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
// Remove previous listener if any
if (favoriteRef != null && favoriteListener != null) {
favoriteRef.removeEventListener(favoriteListener);
}
if (user != null) {
favoriteRef = FirebaseDatabase.getInstance().getReference("users")
.child(user.getUid())
.child("favorites")
.child(com.example.vdcscore.utils.FirebaseErrorUtils.sanitizeKey(currentClubName));
favoriteListener = new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
if (binding == null) return;
isFavorite = snapshot.exists() && Boolean.TRUE.equals(snapshot.getValue(Boolean.class));
updateFavoriteIcon();
}
@Override
public void onCancelled(@NonNull DatabaseError error) {}
};
favoriteRef.addValueEventListener(favoriteListener);
} else {
isFavorite = false;
updateFavoriteIcon();
}
binding.btnFavorite.setOnClickListener(v -> {
FirebaseUser u = FirebaseAuth.getInstance().getCurrentUser();
if (u == null) {
// Pedir para criar conta
showRegisterRequiredDialog();
} else {
// Alternar favorito
if (favoriteRef != null) {
favoriteRef.setValue(!isFavorite);
}
}
});
}
private void updateFavoriteIcon() {
if (binding == null || getContext() == null) return;
if (isFavorite) {
binding.btnFavorite.setImageResource(R.drawable.ic_star);
} else {
binding.btnFavorite.setImageResource(R.drawable.ic_star_border);
}
}
private void showRegisterRequiredDialog() {
if (getContext() == null) return;
new AlertDialog.Builder(requireContext())
.setTitle("Iniciar Sessão Necessário")
.setMessage("Para adicionar clubes aos seus favoritos, precisa de ter uma conta com sessão iniciada. Deseja criar conta ou fazer login agora?")
.setPositiveButton("Criar Conta / Login", (dialog, which) -> {
Intent intent = new Intent(requireContext(), LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
})
.setNegativeButton("Cancelar", null)
.show();
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (favoriteRef != null && favoriteListener != null) {
favoriteRef.removeEventListener(favoriteListener);
}
binding = null;
}
}

View File

@@ -4,7 +4,6 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
@@ -13,8 +12,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import com.example.vdcscore.R;
import com.example.vdcscore.databinding.FragmentCupBinding;
import com.example.vdcscore.ui.gallery.Match;
import com.example.vdcscore.ui.gallery.Matchday;
import com.example.vdcscore.ui.gallery.MatchesAdapter;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
@@ -23,26 +20,38 @@ import com.google.firebase.database.ValueEventListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CupFragment extends Fragment {
private FragmentCupBinding binding;
private MatchesAdapter adapter;
private CupPhasesAdapter adapter;
private DatabaseReference mDatabase;
private List<Matchday> phasesList = new ArrayList<>(); // Reusing Matchday as generalized 'Phase'
private int currentPhaseIndex = 0;
private List<CupPhase> phasesList = new ArrayList<>();
private String currentCategory = "seniores";
private ValueEventListener currentListener;
// Estrutura fixa das fases da Taça
private static final String[] CUP_PHASES_ORDER = {
"1ª Eliminatoria 1º Mão",
"2ª Eliminatoria 2º Mão",
"Quartos 1º Mão",
"Quartos 2º Mão",
"Meia Final 1º Mão",
"Meia Final 2º Mão",
"Final"
};
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
binding = FragmentCupBinding.inflate(inflater, container, false);
View root = binding.getRoot();
// Initialize RecyclerView with reused MatchesAdapter
adapter = new MatchesAdapter();
// Initialize RecyclerView
adapter = new CupPhasesAdapter();
binding.recyclerPhases.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerPhases.setAdapter(adapter);
@@ -58,21 +67,6 @@ public class CupFragment extends Fragment {
}
});
// Setup Navigation Buttons
binding.btnPrevPhase.setOnClickListener(v -> {
if (currentPhaseIndex > 0) {
currentPhaseIndex--;
updateUI();
}
});
binding.btnNextPhase.setOnClickListener(v -> {
if (currentPhaseIndex < phasesList.size() - 1) {
currentPhaseIndex++;
updateUI();
}
});
// Initial Fetch
fetchPhases();
@@ -92,44 +86,89 @@ public class CupFragment extends Fragment {
public void onDataChange(@NonNull DataSnapshot snapshot) {
if (binding == null) return;
phasesList.clear();
// Obter todas as fases do Firebase
List<String> allPhases = new ArrayList<>();
for (DataSnapshot phaseSnap : snapshot.getChildren()) {
String name = phaseSnap.getKey();
List<Match> matches = new ArrayList<>();
for (DataSnapshot matchSnap : phaseSnap.getChildren()) {
Match match = matchSnap.getValue(Match.class);
if (match != null) {
match.setId(matchSnap.getKey());
matches.add(match);
}
}
if (!matches.isEmpty()) {
phasesList.add(new Matchday(name, matches));
if (name != null) {
allPhases.add(name);
}
}
// Sort alphabetically/chronologically as they are phrases like "1ª Fase", "Oitavos", etc.
Collections.sort(phasesList, (p1, p2) -> p1.getName().compareTo(p2.getName()));
if (allPhases.isEmpty()) {
binding.textPhaseName.setText("Sem fases");
adapter.setPhases(new ArrayList<>());
return;
}
// Tentar ordenar pela ordem fixa
phasesList.clear();
// Adicionar fases que estão na lista fixa (na ordem correta)
for (String fixedPhase : CUP_PHASES_ORDER) {
for (String firebasePhase : allPhases) {
if (firebasePhase.equals(fixedPhase)) {
List<Match> matches = getMatchesFromSnapshot(snapshot, firebasePhase);
if (!matches.isEmpty()) {
phasesList.add(new CupPhase(fixedPhase, matches));
}
break;
}
}
}
// Adicionar fases que não estão na lista fixa (ordenadas alfabeticamente)
List<String> remainingPhases = new ArrayList<>();
for (String firebasePhase : allPhases) {
boolean isFixedPhase = false;
for (String fixed : CUP_PHASES_ORDER) {
if (fixed.equals(firebasePhase)) {
isFixedPhase = true;
break;
}
}
if (!isFixedPhase) {
remainingPhases.add(firebasePhase);
}
}
Collections.sort(remainingPhases);
for (String remainingPhase : remainingPhases) {
List<Match> matches = getMatchesFromSnapshot(snapshot, remainingPhase);
if (!matches.isEmpty()) {
phasesList.add(new CupPhase(remainingPhase, matches));
}
}
if (phasesList.isEmpty()) {
binding.textPhaseName.setText("Sem fases");
adapter.setMatches(new ArrayList<>());
adapter.setPhases(new ArrayList<>());
} else {
if (currentPhaseIndex >= phasesList.size()) {
currentPhaseIndex = phasesList.size() - 1;
}
// In case initial load or filter swap, bound index.
if (currentPhaseIndex < 0) currentPhaseIndex = 0;
updateUI();
binding.textPhaseName.setText(phasesList.get(0).getName());
adapter.setPhases(phasesList);
}
}
private List<Match> getMatchesFromSnapshot(DataSnapshot snapshot, String phaseName) {
List<Match> matches = new ArrayList<>();
for (DataSnapshot phaseSnap : snapshot.getChildren()) {
if (phaseSnap.getKey() != null && phaseSnap.getKey().equals(phaseName)) {
for (DataSnapshot matchSnap : phaseSnap.getChildren()) {
Match match = matchSnap.getValue(Match.class);
if (match != null) {
match.setId(matchSnap.getKey());
matches.add(match);
}
}
break;
}
}
return matches;
}
@Override
public void onCancelled(@NonNull DatabaseError error) {
if (getContext() != null) {
Toast.makeText(getContext(), "Erro ao carregar taça: " + error.getMessage(), Toast.LENGTH_SHORT).show();
com.google.android.material.snackbar.Snackbar.make(binding.getRoot(), "Erro ao carregar taça: " + error.getMessage(), com.google.android.material.snackbar.Snackbar.LENGTH_SHORT).show();
}
}
};
@@ -137,22 +176,6 @@ public class CupFragment extends Fragment {
mDatabase.addValueEventListener(currentListener);
}
private void updateUI() {
if (phasesList.isEmpty()) return;
Matchday currentPhase = phasesList.get(currentPhaseIndex);
binding.textPhaseName.setText(currentPhase.getName());
adapter.setMatches(currentPhase.getMatches());
// Enable/Disable navigation buttons
binding.btnPrevPhase.setEnabled(currentPhaseIndex > 0);
binding.btnNextPhase.setEnabled(currentPhaseIndex < phasesList.size() - 1);
// Visual feedback
binding.btnPrevPhase.setAlpha(currentPhaseIndex > 0 ? 1.0f : 0.3f);
binding.btnNextPhase.setAlpha(currentPhaseIndex < phasesList.size() - 1 ? 1.0f : 0.3f);
}
@Override
public void onDestroyView() {
super.onDestroyView();

View File

@@ -12,8 +12,6 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.example.vdcscore.R;
import android.content.Intent;
import android.net.Uri;
import java.util.ArrayList;
import java.util.List;
@@ -90,17 +88,7 @@ public class MatchesAdapter extends RecyclerView.Adapter<MatchesAdapter.ViewHold
String stadium = match.getStadium();
holder.textStadium.setText(isValid(stadium) ? "Campo: " + stadium : "Campo a definir");
// Match Report Button
if (isValid(match.getMatchReportUrl())) {
holder.btnMatchReport.setVisibility(View.VISIBLE);
holder.btnMatchReport.setOnClickListener(v -> {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(match.getMatchReportUrl()));
holder.itemView.getContext().startActivity(browserIntent);
});
} else {
holder.btnMatchReport.setVisibility(View.GONE);
holder.btnMatchReport.setOnClickListener(null);
}
// Botão de Ficha de Jogo removido
}
private boolean isValid(String text) {
@@ -121,8 +109,6 @@ public class MatchesAdapter extends RecyclerView.Adapter<MatchesAdapter.ViewHold
public final TextView textStadium;
public final ImageView imgHomeLogo;
public final ImageView imgAwayLogo;
public final com.google.android.material.button.MaterialButton btnMatchReport;
public ViewHolder(View view) {
super(view);
textHomeTeam = view.findViewById(R.id.text_home_team);
@@ -133,7 +119,6 @@ public class MatchesAdapter extends RecyclerView.Adapter<MatchesAdapter.ViewHold
textStadium = view.findViewById(R.id.text_match_stadium);
imgHomeLogo = view.findViewById(R.id.img_home_logo);
imgAwayLogo = view.findViewById(R.id.img_away_logo);
btnMatchReport = view.findViewById(R.id.btn_match_report);
}
}
}

View File

@@ -204,6 +204,21 @@
android:focusable="true"/>
</LinearLayout>
<!-- Usar sem conta -->
<TextView
android:id="@+id/txtGuestMode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="24dp"
android:fontFamily="@font/font_ibm_plex_sans"
android:text="Usar a app sem conta"
android:textColor="@color/text_2"
android:textStyle="bold"
android:textSize="13sp"
android:clickable="true"
android:focusable="true"/>
</LinearLayout>
</ScrollView>

View File

@@ -36,6 +36,20 @@
tools:src="@mipmap/ic_launcher_round"/>
</com.google.android.material.card.MaterialCardView>
<!-- Botão de Favorito -->
<ImageButton
android:id="@+id/btn_favorite"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Favorito"
android:src="@drawable/ic_star_border"
app:tint="@color/brand"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- CLUBE Label -->
<TextView
android:id="@+id/label_club"

View File

@@ -167,26 +167,6 @@
</LinearLayout>
<!-- Match Report Button (Footer style) -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_match_report"
android:layout_width="match_parent"
android:layout_height="38dp"
android:layout_marginHorizontal="14dp"
android:layout_marginBottom="14dp"
android:backgroundTint="@color/bg_elevated"
app:strokeColor="@color/border"
app:strokeWidth="1dp"
android:fontFamily="@font/font_ibm_plex_sans"
android:text="Ficha de Jogo"
android:textColor="@color/text_2"
android:textSize="11sp"
android:textStyle="bold"
android:insetTop="0dp"
android:insetBottom="0dp"
app:cornerRadius="6dp"
android:visibility="gone" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@@ -23,7 +23,8 @@
<item
android:id="@+id/nav_live_games"
android:icon="@drawable/ic_nav_live"
android:title="@string/menu_live_games" />
android:title="@string/menu_live_games"
android:visible="false" />
<item
android:id="@+id/nav_clubs"
android:icon="@drawable/ic_nav_clubs"