diff --git a/front/__tests__/detalheTreino.test.tsx b/front/__tests__/detalheTreino.test.tsx index 8c35448..487d967 100644 --- a/front/__tests__/detalheTreino.test.tsx +++ b/front/__tests__/detalheTreino.test.tsx @@ -5,6 +5,7 @@ import type { WorkoutPlan } from "@/domain/workout"; const mockBuscarDetalheTreino = jest.fn(); const mockUseLocalSearchParams = jest.fn(); +const mockPush = jest.fn(); jest.mock("@/services/api", () => ({ buscarDetalheTreino: (...args: any[]) => mockBuscarDetalheTreino(...args), @@ -12,7 +13,7 @@ jest.mock("@/services/api", () => ({ jest.mock("expo-router", () => ({ useLocalSearchParams: () => mockUseLocalSearchParams(), - useRouter: () => ({ back: jest.fn(), push: jest.fn() }), + useRouter: () => ({ back: jest.fn(), push: mockPush }), usePathname: () => '/workout/1', })); @@ -95,4 +96,19 @@ describe("Tela de detalhe do treino", () => { expect(screen.getByTestId("loading")).toBeOnTheScreen(); }); + + it("deve abrir a tela de orientacao de carga ao tocar no botao Carga", async () => { + setup(); + mockBuscarDetalheTreino.mockResolvedValue(planoMock); + + await render(); + + await waitFor(() => { + expect(screen.getByText("Carga")).toBeOnTheScreen(); + }); + + screen.getByText("Carga").props.onPress(); + + expect(mockPush).toHaveBeenCalledWith("/load-guide"); + }); }); diff --git a/front/__tests__/orientacaoCarga.test.tsx b/front/__tests__/orientacaoCarga.test.tsx new file mode 100644 index 0000000..6a479c7 --- /dev/null +++ b/front/__tests__/orientacaoCarga.test.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { fireEvent, render, screen } from "@testing-library/react-native"; + +const mockBack = jest.fn(); + +jest.mock("expo-router", () => ({ + useRouter: () => ({ back: mockBack }), + usePathname: () => "/load-guide", +})); + +jest.mock("react-native-safe-area-context", () => ({ + useSafeAreaInsets: () => ({ top: 0, bottom: 0, left: 0, right: 0 }), +})); + +import LoadGuideScreen from "@/app/load-guide"; + +describe("Tela de orientacao de carga", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("deve exibir as orientacoes para escolher a carga certa", async () => { + await render(); + + expect(screen.getByText("Como escolher a carga certa?")).toBeOnTheScreen(); + expect(screen.getByText("Exemplo")).toBeOnTheScreen(); + expect(screen.getByText(/Meta: 8-12 repeticoes/)).toBeOnTheScreen(); + expect(screen.getByText(/pelo menos 8 repeticoes/)).toBeOnTheScreen(); + expect(screen.getByText(/aumente a carga na proxima sessao/)).toBeOnTheScreen(); + }); + + it("deve permitir voltar para a tela anterior", async () => { + await render(); + + fireEvent.press(screen.getByText("Voltar")); + + expect(mockBack).toHaveBeenCalled(); + }); +}); diff --git a/front/src/app/_layout.tsx b/front/src/app/_layout.tsx index b79c046..6c8c804 100644 --- a/front/src/app/_layout.tsx +++ b/front/src/app/_layout.tsx @@ -45,6 +45,7 @@ export default function RootLayout() { + ) diff --git a/front/src/app/load-guide.tsx b/front/src/app/load-guide.tsx new file mode 100644 index 0000000..4302c05 --- /dev/null +++ b/front/src/app/load-guide.tsx @@ -0,0 +1,207 @@ +import React from "react"; +import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import { useRouter } from "expo-router"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +import BottomNav from "@/components/BottomNav"; +import { colors, radius } from "@/styles/theme"; + +export default function LoadGuideScreen() { + const router = useRouter(); + const insets = useSafeAreaInsets(); + + return ( + + + router.back()} + > + + + + TreinAI + + + + + + + Como escolher a carga certa? + + Uma estrategia pratica e usar uma faixa de repeticoes. + + + + Exemplo + + + + + + + [ + styles.primaryButton, + pressed && styles.primaryButtonPressed, + ]} + onPress={() => router.back()} + > + + Voltar + + + + + + + ); +} + +function GuideItem({ text, boldStart = false }: { text: string; boldStart?: boolean }) { + if (!boldStart) { + return ( + + + {text} + + ); + } + + const [label, rest] = text.split(": "); + + return ( + + + + {label}: {rest} + + + ); +} + +const styles = StyleSheet.create({ + screen: { flex: 1, backgroundColor: colors.bg }, + scroll: { flex: 1 }, + content: { + paddingHorizontal: 20, + gap: 16, + alignItems: "center", + }, + backLink: { + alignSelf: "flex-start", + flexDirection: "row", + alignItems: "center", + gap: 8, + paddingVertical: 6, + }, + brand: { + fontFamily: "Exo_900Black", + fontSize: 24, + letterSpacing: 2, + color: colors.accent, + }, + card: { + width: "100%", + maxWidth: 620, + backgroundColor: colors.surface, + borderColor: colors.border, + borderWidth: 1, + borderRadius: 18, + padding: 22, + gap: 16, + alignItems: "center", + }, + iconWrap: { + width: 56, + height: 56, + borderRadius: 28, + backgroundColor: colors.accent, + alignItems: "center", + justifyContent: "center", + }, + title: { + color: colors.text, + fontSize: 24, + fontWeight: "800", + lineHeight: 31, + textAlign: "center", + }, + intro: { + color: colors.textDim, + fontSize: 14, + lineHeight: 21, + textAlign: "center", + }, + exampleBox: { + width: "100%", + backgroundColor: colors.surface2, + borderColor: colors.border, + borderWidth: 1, + borderRadius: radius + 4, + padding: 16, + gap: 13, + }, + sectionTitle: { + color: colors.accent, + fontSize: 13, + fontWeight: "800", + letterSpacing: 0.8, + textTransform: "uppercase", + }, + item: { + flexDirection: "row", + alignItems: "flex-start", + gap: 10, + }, + bullet: { + width: 6, + height: 6, + borderRadius: 3, + backgroundColor: colors.accent, + marginTop: 8, + }, + itemText: { + flex: 1, + color: colors.text, + fontSize: 14, + lineHeight: 21, + }, + bold: { + color: colors.text, + fontWeight: "800", + }, + primaryButton: { + width: "100%", + minHeight: 48, + borderRadius: radius, + backgroundColor: colors.accent, + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + gap: 8, + }, + primaryButtonPressed: { + backgroundColor: colors.accentHover, + transform: [{ translateY: -1 }], + }, + primaryText: { + color: colors.bg, + fontSize: 13, + fontWeight: "800", + letterSpacing: 0.7, + textTransform: "uppercase", + }, +}); diff --git a/front/src/app/workout/[id].tsx b/front/src/app/workout/[id].tsx index 8140b31..7e6838a 100644 --- a/front/src/app/workout/[id].tsx +++ b/front/src/app/workout/[id].tsx @@ -103,7 +103,7 @@ export default function WorkoutDetailScreen() { - + diff --git a/front/src/components/WorkoutContent.tsx b/front/src/components/WorkoutContent.tsx index 660e0f6..d762f4e 100644 --- a/front/src/components/WorkoutContent.tsx +++ b/front/src/components/WorkoutContent.tsx @@ -1,4 +1,6 @@ -import { StyleSheet, Text, View, useWindowDimensions } from 'react-native' +import { Pressable, StyleSheet, Text, View, useWindowDimensions } from 'react-native' +import { Ionicons } from '@expo/vector-icons' +import { useRouter } from 'expo-router' import { colors, radius } from '@/styles/theme' import type { Exercise, WorkoutDay, WorkoutPlan } from '@/domain/workout' @@ -13,14 +15,21 @@ const LEVEL_LABEL: Record = { interface WorkoutContentProps { plan: WorkoutPlan + showLoadGuideButton?: boolean } -export default function WorkoutContent({ plan }: WorkoutContentProps) { +export default function WorkoutContent({ plan, showLoadGuideButton = false }: WorkoutContentProps) { + const router = useRouter() + return ( <> - + router.push('/load-guide')} + /> ) } @@ -93,7 +102,15 @@ function StatCard({ ) } -function DaysList({ days }: { days: WorkoutDay[] }) { +function DaysList({ + days, + showLoadGuideButton, + onOpenLoadGuide, +}: { + days: WorkoutDay[] + showLoadGuideButton: boolean + onOpenLoadGuide: () => void +}) { if (!days?.length) { return ( @@ -114,7 +131,12 @@ function DaysList({ days }: { days: WorkoutDay[] }) { {d.exercises.map((ex, i) => ( - + ))} @@ -123,7 +145,32 @@ function DaysList({ days }: { days: WorkoutDay[] }) { ) } -function ExerciseCard({ exercise, index }: { exercise: Exercise; index: number }) { +function LoadGuideButton({ onPress }: { onPress: () => void }) { + return ( + [ + styles.loadButton, + pressed && styles.loadButtonPressed, + ]} + onPress={onPress} + > + + Carga + + ) +} + +function ExerciseCard({ + exercise, + index, + onOpenLoadGuide, +}: { + exercise: Exercise + index: number + onOpenLoadGuide?: () => void +}) { const { width } = useWindowDimensions() const isMobile = width < 480 @@ -142,10 +189,14 @@ function ExerciseCard({ exercise, index }: { exercise: Exercise; index: number } - - - × - + + + + × + + + + {!!onOpenLoadGuide && } ) @@ -237,6 +288,30 @@ const styles = StyleSheet.create({ exercisesSection: { gap: 20, }, + loadButton: { + alignSelf: 'flex-end', + minHeight: 36, + borderRadius: radius, + backgroundColor: colors.accent, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 6, + paddingHorizontal: 12, + paddingVertical: 8, + flexShrink: 0, + }, + loadButtonPressed: { + backgroundColor: colors.accentHover, + transform: [{ translateY: -1 }], + }, + loadButtonText: { + color: colors.bg, + fontSize: 11, + fontWeight: '800', + letterSpacing: 0.7, + textTransform: 'uppercase', + }, dayBlock: { gap: 10, }, @@ -318,6 +393,14 @@ const styles = StyleSheet.create({ alignItems: 'flex-end', gap: 10, flexWrap: 'wrap', + flex: 1, + minWidth: 0, + }, + exerciseFooter: { + flexDirection: 'row', + alignItems: 'flex-end', + justifyContent: 'space-between', + gap: 12, }, metric: { flexDirection: 'row', @@ -351,4 +434,4 @@ const styles = StyleSheet.create({ fontWeight: '300', marginHorizontal: 4, }, -}) \ No newline at end of file +})