Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
pull_request:
branches: [develop, main]

# Caller workflows that only invoke reusable jobs need an empty permissions block.
# The called workflow (quality.yml) declares its own token scope.
permissions: {}

jobs:
quality:
uses: ./.github/workflows/quality.yml
4 changes: 4 additions & 0 deletions .github/workflows/eas.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ on:
push:
branches: [main]

permissions:
contents: read

jobs:
quality:
permissions: {}
uses: ./.github/workflows/quality.yml

release-tag:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ name: Quality
on:
workflow_call:

permissions:
contents: read

jobs:
quality:
name: Quality
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/sentry-health.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ on:
- cron: '*/5 * * * *'
workflow_dispatch:

permissions:
contents: read

jobs:
edge-function-health:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion locales/es.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"ios": {
"CFBundleDisplayName": "jugaMUS",
"NSPhotoLibraryUsageDescription": "jugaMUS accede a tus fotos para que puedas elegir una imagen de perfil. La foto se sube a tu cuenta y la verán otros jugadores en las partidas y torneos en los que participes."
"NSPhotoLibraryUsageDescription": "jugaMUS accede a tus fotos para que puedas elegir una imagen de perfil. La foto se sube a tu cuenta y la verán otros jugadores en las partidas, torneos y ligas en los que participes."
}
}
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"react-hook-form": "^7.83.0",
"react-native": "0.81.5",
"react-native-chart-kit": "^6.12.2",
"react-native-confetti-cannon": "^1.5.2",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1",
Expand Down
27 changes: 1 addition & 26 deletions src/app/(admin)/analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ActivityIndicator, Dimensions, ScrollView, StyleSheet, Text, View } fro
import { BarChart, LineChart } from 'react-native-chart-kit'

import { AdminCloseBar } from '@/components/admin/AdminCloseBar'
import { StatCard } from '@/components/stats/StatCard'
import {
useAnalyticsSummary,
useMatchesByCity,
Expand All @@ -24,15 +25,6 @@ const chartConfig = {
propsForDots: { r: '4', strokeWidth: '2', stroke: Colors.primary },
}

function StatCard({ label, value }: { label: string; value: string }) {
return (
<View style={styles.statCard}>
<Text style={styles.statValue}>{value}</Text>
<Text style={styles.statLabel}>{label}</Text>
</View>
)
}

export default function AdminAnalyticsScreen() {
const { data: summary, isLoading: summaryLoading } = useAnalyticsSummary()
const { data: byWeek, isLoading: weekLoading } = useMatchesByWeek(12)
Expand Down Expand Up @@ -150,23 +142,6 @@ const styles = StyleSheet.create({
flexWrap: 'wrap',
gap: 12,
},
statCard: {
backgroundColor: Colors.surface,
borderRadius: 12,
padding: 14,
minWidth: '47%',
flexGrow: 1,
},
statValue: {
fontSize: 22,
fontFamily: Fonts.bold,
color: Colors.primary,
},
statLabel: {
fontSize: 12,
color: Colors.textSecondary,
marginTop: 2,
},
sectionTitle: {
fontSize: 16,
fontFamily: Fonts.semiBold,
Expand Down
6 changes: 6 additions & 0 deletions src/app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default function TabsLayout() {

return (
<Tabs
backBehavior="history"
screenOptions={{
headerShown: false,
lazy: true,
Expand Down Expand Up @@ -74,9 +75,14 @@ export default function TabsLayout() {
<Tabs.Screen name="tournaments/create" options={{ href: null }} />
<Tabs.Screen name="tournaments/[id]" options={{ href: null }} />
<Tabs.Screen name="tournaments/edit/[id]" options={{ href: null }} />
<Tabs.Screen name="leagues/create" options={{ href: null }} />
<Tabs.Screen name="leagues/[id]" options={{ href: null }} />
<Tabs.Screen name="leagues/edit/[id]" options={{ href: null }} />
<Tabs.Screen name="profile/index" options={{ title: 'Perfil', ...VISIBLE_TAB_OPTIONS }} />
<Tabs.Screen name="profile/[userId]" options={{ href: null }} />
<Tabs.Screen name="profile/edit" options={{ href: null }} />
<Tabs.Screen name="profile/stats/[userId]" options={{ href: null }} />
<Tabs.Screen name="leaderboard/index" options={{ href: null }} />
</Tabs>
)
}
Expand Down
126 changes: 111 additions & 15 deletions src/app/(tabs)/explore/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,25 @@ import {
parseIsoToDate,
} from '@/components/ui/dateTimePickerUtils'
import { MunicipalityPicker } from '@/components/ui/MunicipalityPicker'
import { MATCH_STATUS, TOURNAMENT_STATUS, type ExploreContentType } from '@/constants'
import { useInfinitePublicMatches, usePublicTournamentsExplore } from '@/hooks/useMatches'
import {
LEAGUE_STATUS,
MATCH_STATUS,
TOURNAMENT_STATUS,
type ExploreContentType,
} from '@/constants'
import {
useInfinitePublicMatches,
usePublicLeaguesExplore,
usePublicTournamentsExplore,
} from '@/hooks/useMatches'
import type {
PublicMatchExplorerRow,
PublicMatchesListFilters,
VisibilityFilter,
} from '@/services/matches.service'
import type { LeagueRow, PublicLeaguesListFilters } from '@/services/leagues.service'
import type { PublicTournamentsListFilters, TournamentRow } from '@/services/tournaments.service'
import { leagueFormatDisplay, leagueStatusDisplay } from '@/utils/leagueDisplay'
import { Colors } from '@/theme/colors'
import { Fonts } from '@/theme/typography'
import { screenTopPadding } from '@/theme/layout'
Expand Down Expand Up @@ -97,6 +108,13 @@ function tournamentStatusTone(tournament: TournamentRow): StatusDotTone {
return 'upcoming'
}

function leagueStatusTone(league: LeagueRow): StatusDotTone {
if (league.status === LEAGUE_STATUS.IN_PROGRESS) return 'active'
if (league.status === LEAGUE_STATUS.REGISTRATION) return 'upcoming'
if (league.status === LEAGUE_STATUS.FINISHED) return 'upcoming' // tono neutro (gris)
return 'upcoming'
}

function ExploreMatchRow({ row, onPress }: { row: PublicMatchExplorerRow; onPress: () => void }) {
const tone = matchStatusTone(row.status)
return (
Expand Down Expand Up @@ -158,6 +176,37 @@ function ExploreTournamentRow({ row, onPress }: { row: TournamentRow; onPress: (
)
}

function ExploreLeagueRow({ row, onPress }: { row: LeagueRow; onPress: () => void }) {
const tone = leagueStatusTone(row)
return (
<Pressable
style={styles.row}
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={`Liga: ${row.title}`}>
<StatusDot tone={tone} />
<View style={styles.rowBody}>
<Text style={styles.rowKind}>Liga · {leagueFormatDisplay(row.format)}</Text>
<Text style={styles.rowTitle} numberOfLines={2}>
{row.title}
</Text>
<Text style={styles.rowMeta}>
{formatCityAndPlace(row.city, row.place_defined, row.place_text)}
</Text>
</View>
<View style={styles.rowTrailing}>
<Text style={[styles.rowStatus, tone === 'active' && styles.rowStatusActive]}>
{leagueStatusDisplay(row).text}
</Text>
<Text style={styles.rowDate}>{formatDisplay(row.start_at)}</Text>
<Text style={styles.rowExtra}>
{row.status === LEAGUE_STATUS.REGISTRATION ? 'Sin iniciar' : 'Ver clasificación'}
</Text>
</View>
</Pressable>
)
}

export default function ExploreScreen() {
const router = useRouter()
const insets = useSafeAreaInsets()
Expand Down Expand Up @@ -213,10 +262,34 @@ export default function ExploreScreen() {
const {
data: tournaments,
isLoading: tournamentsLoading,
isError: tournamentsIsError,
isRefetching: tournamentsRefetching,
refetch: refetchTournaments,
} = usePublicTournamentsExplore(tournamentFilters)

const leagueFilters: PublicLeaguesListFilters = useMemo(
() => ({
search: filters.search,
city: filters.city,
status: filters.status,
hideCelebrated: filters.hideCelebrated,
startAfter: filters.startAfter,
startBefore: filters.startBefore,
minFreeSlots: filters.minFreeSlots,
contentType: filters.contentType,
visibility: filters.visibility ?? 'all',
}),
[filters]
)

const {
data: leagues,
isLoading: leaguesLoading,
isError: leaguesIsError,
isRefetching: leaguesRefetching,
refetch: refetchLeagues,
} = usePublicLeaguesExplore(leagueFilters)

const matchRows = useMemo(() => data?.pages.flatMap((p) => p.rows) ?? [], [data?.pages])

const exploreItems = useMemo((): ExploreItem[] => {
Expand All @@ -232,16 +305,23 @@ export default function ExploreScreen() {
start_at: row.start_at,
row,
}))
const merged = [...matchItems, ...tournamentItems].sort(
const leagueItems: ExploreItem[] = (leagues ?? []).map((row) => ({
kind: 'league',
id: row.id,
start_at: row.start_at,
row,
}))
const merged = [...matchItems, ...tournamentItems, ...leagueItems].sort(
(a, b) => new Date(a.start_at).getTime() - new Date(b.start_at).getTime()
)
return filterExploreItemsForCelebrated(merged, filters.hideCelebrated)
}, [matchRows, tournaments, filters.hideCelebrated])
}, [matchRows, tournaments, leagues, filters.hideCelebrated])

const refetchAll = useCallback(() => {
void refetch()
void refetchTournaments()
}, [refetch, refetchTournaments])
void refetchLeagues()
}, [refetch, refetchTournaments, refetchLeagues])

const openFilterModal = useCallback(() => {
setDraftCity(filters.city)
Expand Down Expand Up @@ -323,7 +403,7 @@ export default function ExploreScreen() {

const listHeader = (
<View style={styles.listHeader}>
<ScreenHeader title="Descubrir" subtitle="Partidas y torneos" />
<ScreenHeader title="Descubrir" subtitle="Partidas, torneos y ligas" />

<TextInput
style={styles.search}
Expand Down Expand Up @@ -351,8 +431,9 @@ export default function ExploreScreen() {
</View>
)

const showMatches = filters.contentType !== 'tournaments'
const showTournaments = filters.contentType !== 'matches'
const showMatches = filters.contentType !== 'tournaments' && filters.contentType !== 'leagues'
const showTournaments = filters.contentType !== 'matches' && filters.contentType !== 'leagues'
const showLeagues = filters.contentType !== 'matches' && filters.contentType !== 'tournaments'

const listFooter = isFetchingNextPage ? (
<View style={styles.footerLoad}>
Expand All @@ -368,17 +449,22 @@ export default function ExploreScreen() {

if (
(showMatches && isLoading && !data) ||
(showTournaments && tournamentsLoading && !tournaments)
(showTournaments && tournamentsLoading && !tournaments) ||
(showLeagues && leaguesLoading && !leagues)
) {
return (
<View style={[styles.centered, { paddingTop: screenTopPadding(insets.top, 24) }]}>
<ActivityIndicator size="large" color={Colors.primary} />
<Text style={styles.loadingHint}>Cargando partidas y torneos…</Text>
<Text style={styles.loadingHint}>Cargando partidas, torneos y ligas…</Text>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</View>
)
}

if (isError) {
if (
(showMatches && isError) ||
(showTournaments && tournamentsIsError) ||
(showLeagues && leaguesIsError)
) {
return (
<View
style={[
Expand All @@ -393,7 +479,7 @@ export default function ExploreScreen() {
Si acabas de actualizar la app, aplica la migración Supabase `009_list_public_matches` en
tu proyecto.
</Text>
<Button title="Reintentar" onPress={() => void refetch()} style={{ marginTop: 16 }} />
<Button title="Reintentar" onPress={() => void refetchAll()} style={{ marginTop: 16 }} />
</View>
)
}
Expand All @@ -409,19 +495,26 @@ export default function ExploreScreen() {
row={item.row}
onPress={() => router.push(`/(tabs)/matches/${item.id}`)}
/>
) : (
) : item.kind === 'tournament' ? (
<ExploreTournamentRow
row={item.row}
onPress={() => router.push(`/(tabs)/tournaments/${item.id}`)}
/>
) : (
<ExploreLeagueRow
row={item.row}
onPress={() => router.push(`/(tabs)/leagues/${item.id}`)}
/>
)
}
ListHeaderComponent={listHeader}
ListFooterComponent={listFooter}
contentContainerStyle={[styles.listContent, { paddingBottom: insets.bottom + 88 }]}
onEndReached={onEndReached}
onEndReachedThreshold={0.35}
refreshing={(isRefetching && !isFetchingNextPage) || tournamentsRefetching}
refreshing={
(isRefetching && !isFetchingNextPage) || tournamentsRefetching || leaguesRefetching
}
onRefresh={refetchAll}
ListEmptyComponent={
<View style={styles.emptyWrap}>
Expand All @@ -431,7 +524,9 @@ export default function ExploreScreen() {
? 'No hay partidas que coincidan con tu búsqueda y filtros.'
: filters.contentType === 'tournaments'
? 'No hay torneos que coincidan con tu búsqueda y filtros.'
: 'No hay partidas ni torneos que coincidan con tu búsqueda y filtros.'}{' '}
: filters.contentType === 'leagues'
? 'No hay ligas que coincidan con tu búsqueda y filtros.'
: 'No hay partidas, torneos ni ligas que coincidan con tu búsqueda y filtros.'}{' '}
Prueba a ampliar fechas o quitar filtros.
</Text>
<Button
Expand Down Expand Up @@ -472,6 +567,7 @@ export default function ExploreScreen() {
{ id: 'all', label: 'Todo', value: 'all' as ExploreContentType },
{ id: 'm', label: 'Partidas', value: 'matches' as ExploreContentType },
{ id: 't', label: 'Torneos', value: 'tournaments' as ExploreContentType },
{ id: 'l', label: 'Ligas', value: 'leagues' as ExploreContentType },
] as const
).map((opt) => {
const selected = draftContentType === opt.value
Expand Down
Loading