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
+})