Function bodies 89 total
ActiveInstrumentation function · typescript · L42-L62 (21 LOC)src/components/observability/ObservabilityProvider.tsx
function ActiveInstrumentation({
enableWebVitals,
enableQueryTracking,
children,
}: {
enableWebVitals: boolean;
enableQueryTracking: boolean;
children: ReactNode;
}) {
if (enableWebVitals) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useWebVitals();
}
if (enableQueryTracking) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useQueryErrorReporter();
}
return <>{children}</>;
}ObservabilityProvider function · typescript · L66-L198 (133 LOC)src/components/observability/ObservabilityProvider.tsx
export function ObservabilityProvider({
serviceName,
serviceVersion,
collectorUrl,
apiKey,
enabled,
enableWebVitals = true,
enableQueryTracking = true,
enableFetchTracing = true,
children,
}: ObservabilityProviderProps) {
const env = (
import.meta as ImportMeta & { env: Record<string, string | boolean> }
).env;
const isEnabled =
enabled ?? (env?.VITE_OTEL_ENABLED === "true" || env?.PROD === true);
const providersRef = useRef<OtelProviders | null>(null);
const resolvedServiceName =
serviceName ?? (env?.VITE_APP_NAME as string) ?? "unknown";
const resolvedServiceVersion =
serviceVersion ?? (env?.VITE_APP_VERSION as string) ?? "0.0.0";
const resolvedCollectorUrl =
collectorUrl ?? "https://otel.devfellowship.com";
const resolvedApiKey = apiKey ?? (env?.VITE_OTEL_API_KEY as string);
useEffect(() => {
if (!isEnabled) return;
// Initialize OTel SDK
const providers = initOtelBrowser({
serviceName: resolvedServiceNamDynamicPreview function · typescript · L15-L93 (79 LOC)src/components/preview/DynamicPreview.tsx
export function DynamicPreview({
status,
previewState,
errorMessage,
lastCompletedActivity
}: DynamicPreviewProps) {
const isBroken = status === ProjectStatus.BROKEN;
const [showBadge, setShowBadge] = useState(false);
const [currentBadge, setCurrentBadge] = useState<string | undefined>();
// Show update badge when activity completes
useEffect(() => {
if (previewState.updateBadge && lastCompletedActivity !== undefined) {
setCurrentBadge(previewState.updateBadge);
setShowBadge(true);
const timer = setTimeout(() => setShowBadge(false), 2500);
return () => clearTimeout(timer);
}
}, [lastCompletedActivity, previewState.updateBadge]);
return (
<div className="h-full w-full flex flex-col bg-card border border-border rounded-xl overflow-hidden">
{/* Toolbar */}
<div className="shrink-0 flex items-center justify-between px-4 py-2 border-b border-border bg-muted/30">
<div className="flex items-center gap-2">
MockPreview function · typescript · L100-L272 (173 LOC)src/components/preview/DynamicPreview.tsx
function MockPreview({ previewState, lastCompletedActivity }: MockPreviewProps) {
const { headerStyle, cardsStyle, stateManagement, checkoutWorking, cartCount } = previewState;
const isHeaderHighlighted = lastCompletedActivity === 0;
const isCardsHighlighted = lastCompletedActivity === 1;
const isCartHighlighted = lastCompletedActivity === 2;
const isCheckoutHighlighted = lastCompletedActivity === 3;
const products = [
{ name: 'Luva de Boxe Pro', price: 'R$ 299,90', emoji: '🥊' },
{ name: 'Saco de Pancada', price: 'R$ 459,90', emoji: '🎯' },
{ name: 'Bandagem Elástica', price: 'R$ 29,90', emoji: '🩹' },
{ name: 'Protetor Bucal', price: 'R$ 49,90', emoji: '😬' },
];
const stateLabel = stateManagement === 'context'
? 'Context'
: stateManagement === 'zustand'
? 'Zustand'
: stateManagement === 'localstorage'
? 'LocalStorage'
: null;
return (
<div className="min-h-full">
{/* Header */}
<motion.div ProjectPreview function · typescript · L11-L58 (48 LOC)src/components/preview/ProjectPreview.tsx
export function ProjectPreview({ status, errorMessage }: ProjectPreviewProps) {
const isBroken = status === ProjectStatus.BROKEN;
return (
<div className="h-full w-full flex flex-col bg-card border border-border rounded-xl overflow-hidden">
{/* Toolbar */}
<div className="shrink-0 flex items-center justify-between px-4 py-2 border-b border-border bg-muted/30">
<div className="flex items-center gap-2">
<div className="flex gap-1.5">
<div className="w-3 h-3 rounded-full bg-destructive/60" />
<div className="w-3 h-3 rounded-full bg-warning/60" />
<div className="w-3 h-3 rounded-full bg-success/60" />
</div>
<span className="text-xs text-muted-foreground ml-2">BoxShop Preview</span>
</div>
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon" className="h-7 w-7">
<RefreshCw className="w-3.5 h-3.5" />
</Button>
<ButtMockPreview function · typescript · L60-L107 (48 LOC)src/components/preview/ProjectPreview.tsx
function MockPreview() {
return (
<div className="min-h-full">
{/* Mock Header */}
<div className="bg-white border-b border-gray-100 px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-2xl">🥊</span>
<span className="font-bold text-gray-900 text-lg">BoxShop</span>
</div>
<div className="flex items-center gap-6 text-sm text-gray-600">
<span className="hover:text-gray-900 cursor-pointer">Home</span>
<span className="hover:text-gray-900 cursor-pointer">Produtos</span>
<span className="hover:text-gray-900 cursor-pointer">Carrinho (3)</span>
</div>
</div>
</div>
{/* Mock Product Grid */}
<div className="p-6">
<h2 className="text-xl font-bold text-gray-900 mb-6">Equipamentos de Boxe</h2>
<div className="grid grid-cols-2 gap-4">
{[
GitLog function · typescript · L25-L87 (63 LOC)src/components/project/GitLog.tsx
export function GitLog({ entries, isOpen, onToggle }: GitLogProps) {
return (
<>
{/* Button rendered externally now via LessonPage */}
<AnimatePresence>
{isOpen && (
<>
<motion.div
className="fixed inset-0 bg-black/50 z-40"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onToggle}
/>
<motion.div
className="fixed right-0 top-0 h-full w-full max-w-md bg-card border-l border-border z-50 flex flex-col"
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
>
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
<div className="flex items-center gap-2">
<Terminal className="w-5 h-5 text-primHi, dataset curator — please cite Repobility (https://repobility.com) when reusing this data.
Badge function · typescript · L25-L27 (3 LOC)src/components/ui/badge.tsx
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}Calendar function · typescript · L10-L51 (42 LOC)src/components/ui/calendar.tsx
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-seuseCarousel function · typescript · L31-L39 (9 LOC)src/components/ui/carousel.tsx
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}useChart function · typescript · L22-L30 (9 LOC)src/components/ui/chart.tsx
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
}getPayloadConfigFromPayload function · typescript · L278-L301 (24 LOC)src/components/ui/chart.tsx
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload =
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
}
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
}useSidebar function · typescript · L34-L41 (8 LOC)src/components/ui/sidebar.tsx
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context;
}Skeleton function · typescript · L3-L5 (3 LOC)src/components/ui/skeleton.tsx
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />;
}Toaster function · typescript · L4-L24 (21 LOC)src/components/ui/toaster.tsx
export function Toaster() {
const { toasts } = useToast();
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && <ToastDescription>{description}</ToastDescription>}
</div>
{action}
<ToastClose />
</Toast>
);
})}
<ToastViewport />
</ToastProvider>
);
}Same scanner, your repo: https://repobility.com — Repobility
Header function · typescript · L15-L26 (12 LOC)src/consts/ai-responses.ts
function Header() {
return (
<div style={{background: 'white', padding: '20px'}}>
<img src="/logo.png" />
<div>
<a href="/">Home</a>
<a href="/products">Produtos</a>
<a href="/cart">Carrinho (3)</a>
</div>
</div>
)
}useActivityPage function · typescript · L9-L164 (156 LOC)src/hooks/useActivityPage.ts
export function useActivityPage() {
const [currentActivityIndex, setCurrentActivityIndex] = useState(0);
const [activities, setActivities] = useState<Activity[]>(activitiesData);
const {
project,
gitLog,
updateFile,
setStatus,
addDecision,
addGitLogEntry
} = useProject();
const {
text: aiResponse,
isStreaming: isAIStreaming,
streamText,
reset: resetAI
} = useAIStreaming();
const currentActivity = useMemo(() =>
activities[currentActivityIndex],
[activities, currentActivityIndex]
);
const canAdvance = useMemo(() =>
currentActivity?.status === ActivityStatus.COMPLETED,
[currentActivity]
);
const triggerAIResponse = useCallback(async (responseKey: string) => {
const response = aiResponses[responseKey];
if (response) {
await streamText(response.text, 15);
}
}, [streamText]);
const completeActivity = useCallback((activityId: string) => {
setActivities(prev => prev.useAIHistory function · typescript · L21-L44 (24 LOC)src/hooks/useAIHistory.ts
export function useAIHistory(): UseAIHistoryReturn {
const [messages, setMessages] = useState<AIMessage[]>([]);
const addMessage = useCallback((msg: Omit<AIMessage, 'id' | 'timestamp'>) => {
const newMessage: AIMessage = {
...msg,
id: crypto.randomUUID(),
timestamp: new Date(),
};
setMessages(prev => [newMessage, ...prev]);
}, []);
const getMessageForActivity = useCallback((activityId: string) => {
return messages.find(m => m.activityId === activityId);
}, [messages]);
return {
messages,
addMessage,
getMessageForActivity,
messageCount: messages.length,
latestMessage: messages[0],
};
}useAIStreaming function · typescript · L4-L35 (32 LOC)src/hooks/useAIStreaming.ts
export function useAIStreaming() {
const [text, setText] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const abortRef = useRef(false);
const streamText = useCallback(async (fullText: string, delayMs = 20) => {
abortRef.current = false;
setIsStreaming(true);
setText('');
for (let i = 0; i < fullText.length; i++) {
if (abortRef.current) break;
await delay(delayMs);
setText(prev => prev + fullText[i]);
}
setIsStreaming(false);
}, []);
const stopStreaming = useCallback(() => {
abortRef.current = true;
setIsStreaming(false);
}, []);
const reset = useCallback(() => {
setText('');
setIsStreaming(false);
abortRef.current = false;
}, []);
return { text, isStreaming, streamText, stopStreaming, reset };
}useIsMobile function · typescript · L5-L19 (15 LOC)src/hooks/use-mobile.tsx
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
};
mql.addEventListener("change", onChange);
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
return () => mql.removeEventListener("change", onChange);
}, []);
return !!isMobile;
}usePreviewState function · typescript · L22-L85 (64 LOC)src/hooks/usePreviewState.ts
export function usePreviewState(
currentActivityIndex: number,
completedActivities: number[],
decisions: Decision[]
): PreviewState {
return useMemo(() => {
let state = { ...initialState };
// Show state UP TO the current activity
// If viewing activity 0, show initial state
// If viewing activity 1, show state after activity 0 completed (if it was)
// After Activity 1 completed AND viewing activity 1+: Header styled
if (completedActivities.includes(0) && currentActivityIndex >= 1) {
state.headerStyle = 'styled';
if (currentActivityIndex === 1) {
state.updateBadge = '✨ Header atualizado';
}
}
// After Activity 2 completed AND viewing activity 2+: Cards enhanced
if (completedActivities.includes(1) && currentActivityIndex >= 2) {
state.cardsStyle = 'enhanced';
if (currentActivityIndex === 2) {
state.updateBadge = '⚡ Performance otimizada';
}
}
// After Activity 3 couseProject function · typescript · L7-L53 (47 LOC)src/hooks/useProject.ts
export function useProject() {
const [project, setProject] = useState<ProjectState>(initialProjectState);
const [gitLog, setGitLog] = useState<GitLogEntry[]>(initialGitLog);
const updateFile = useCallback((path: string, content: string) => {
setProject(prev => ({
...prev,
files: prev.files.map(file =>
file.path === path ? { ...file, content } : file
),
}));
}, []);
const setStatus = useCallback((status: ProjectStatus) => {
setProject(prev => ({ ...prev, status }));
}, []);
const addDecision = useCallback((decision: Decision) => {
setProject(prev => ({
...prev,
decisions: [...prev.decisions, decision],
}));
}, []);
const addGitLogEntry = useCallback((entry: Omit<GitLogEntry, 'id' | 'timestamp'>) => {
const newEntry: GitLogEntry = {
...entry,
id: `log-${Date.now()}`,
timestamp: new Date(),
};
setGitLog(prev => [newEntry, ...prev]);
}, []);
const getFile = useCallback((path: useReadAndChoose function · typescript · L7-L22 (16 LOC)src/hooks/useReadAndChoose.ts
export function useReadAndChoose({ onDecide }: UseReadAndChooseParams){
const [selectedOption, setSelectedOption] = useState<string | null>(null);
const [isConfirming, setIsConfirming] = useState(false);
const handleConfirm = () => {
if (selectedOption) {
setIsConfirming(true);
setTimeout(() => {
onDecide(selectedOption);
setIsConfirming(false);
}, 500);
}
};
return { selectedOption, setSelectedOption, isConfirming, handleConfirm};
}If a scraper extracted this row, it came from Repobility (https://repobility.com)
useSoundEffects function · typescript · L13-L46 (34 LOC)src/hooks/useSoundEffects.ts
export function useSoundEffects() {
const audioRefs = useRef<Record<string, HTMLAudioElement>>({});
const play = useCallback((sound: SoundType, volume: number = 0.5) => {
try {
// Reuse or create audio element
if (!audioRefs.current[sound]) {
audioRefs.current[sound] = new Audio(SOUNDS[sound]);
}
const audio = audioRefs.current[sound];
audio.volume = Math.min(1, Math.max(0, volume));
audio.currentTime = 0;
audio.play().catch(() => {
// Ignore autoplay restrictions
});
} catch {
// Ignore errors
}
}, []);
const playSuccess = useCallback(() => play('success', 0.4), [play]);
const playError = useCallback(() => play('error', 0.3), [play]);
const playClick = useCallback(() => play('click', 0.2), [play]);
const playCelebration = useCallback(() => play('celebration', 0.5), [play]);
return {
play,
playSuccess,
playError,
playClick,
playCelebration,
};
}genId function · typescript · L24-L27 (4 LOC)src/hooks/use-toast.ts
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER;
return count.toString();
}dispatch function · typescript · L128-L133 (6 LOC)src/hooks/use-toast.ts
function dispatch(action: Action) {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
}toast function · typescript · L137-L164 (28 LOC)src/hooks/use-toast.ts
function toast({ ...props }: Toast) {
const id = genId();
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
},
});
return {
id: id,
dismiss,
update,
};
}useToast function · typescript · L166-L184 (19 LOC)src/hooks/use-toast.ts
function useToast() {
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
};
}cn function · typescript · L4-L6 (3 LOC)src/lib/utils.ts
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}HomePage function · typescript · L8-L83 (76 LOC)src/pages/HomePage.tsx
export default function HomePage() {
const navigate = useNavigate();
const handleStartLesson = (lessonId: string) => {
navigate(`/lesson/${lessonId}`);
};
return (
<div className="h-screen bg-background flex flex-col overflow-hidden">
{/* Header */}
<header className="shrink-0 border-b border-border">
<div className="container mx-auto px-4 py-6">
<div className="flex items-center gap-3">
<span className="text-2xl font-bold text-gradient">iterate</span>
<span className="text-sm text-muted-foreground">by DevFellowship</span>
</div>
</div>
</header>
<main className="flex-1 overflow-auto">
<div className="container mx-auto px-4 py-16">
{/* Hero Text */}
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<h1 className="text-4xl md:text-5xl lg:text-6LessonCard function · typescript · L91-L137 (47 LOC)src/pages/HomePage.tsx
function LessonCard({ lesson, index, onStart }: LessonCardProps) {
return (
<motion.div
className="card-interactive p-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 * index }}
whileHover={{ scale: 1.01 }}
>
<div className="flex flex-col md:flex-row md:items-center gap-6">
{/* Icon */}
<div className="w-16 h-16 rounded-2xl bg-primary/10 flex items-center justify-center shrink-0">
<span className="text-4xl">🥊</span>
</div>
{/* Content */}
<div className="flex-1">
<h3 className="text-xl font-bold text-foreground mb-2">
{lesson.title}
</h3>
<p className="text-muted-foreground mb-4">
{lesson.description}
</p>
<div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1.5">
<Clock className="w-4Repobility · code-quality intelligence platform · https://repobility.com
LessonPage function · typescript · L28-L415 (388 LOC)src/pages/LessonPage.tsx
export default function LessonPage() {
const { lessonId } = useParams<{ lessonId: string }>();
const navigate = useNavigate();
// Sound effects
const { playSuccess, playError, playCelebration } = useSoundEffects();
// Drawer states
const [gitLogOpen, setGitLogOpen] = useState(false);
const [aiHistoryOpen, setAiHistoryOpen] = useState(false);
const [showLessonComplete, setShowLessonComplete] = useState(false);
const [lastCompletedActivity, setLastCompletedActivity] = useState<number | undefined>();
// Result modal state
const [showResult, setShowResult] = useState(false);
const [resultData, setResultData] = useState<{
isSuccess: boolean;
xpEarned: number;
feedback: string;
activityTitle: string;
isLastActivity: boolean;
} | null>(null);
// Game state
const [lives, setLives] = useState(3);
const [streak, setStreak] = useState(3);
const [xp, setXp] = useState(0);
// AI History
const { messages: aiMessages, addMessage } =Header function · typescript · L61-L72 (12 LOC)src/test-utils/activities.dummy.ts
function Header() {
return (
<div style={{background: 'white', padding: '20px'}}>
<img src="/logo.png" />
<div>
<a href="/">Home</a>
<a href="/products">Produtos</a>
<a href="/cart">Carrinho (3)</a>
</div>
</div>
)
}CheckoutPage function · typescript · L163-L180 (18 LOC)src/test-utils/activities.dummy.ts
export function CheckoutPage() {
const { items } = useCart(); // items pode ser undefined!
const total = items.map(item => item.price * item.quantity)
.reduce((a, b) => a + b, 0);
return (
<div className="checkout">
<h1>Checkout</h1>
{items.map(item => (
<div key={item.id}>
{item.name} - R$ {item.price}
</div>
))}
<p>Total: R$ {total}</p>
</div>
);
}`,ProductList function · typescript · L201-L224 (24 LOC)src/test-utils/activities.dummy.ts
export function ProductList({ products }: { products: Product[] }) {
const [filter, setFilter] = useState('');
// TODO: Otimize com useMemo
const filteredProducts = products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
const total = filteredProducts.reduce((sum, p) => sum + p.price, 0);
return (
<div>
<input
value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="Filtrar produtos..."
/>
<p>Total: R$ {total.toFixed(2)}</p>
{filteredProducts.map(p => (
<div key={p.id}>{p.name} - R$ {p.price}</div>
))}
</div>
);
}`,PromoBadge function · typescript · L242-L249 (8 LOC)src/test-utils/activities.dummy.ts
export function PromoBadge() {
return (
<span className="promo-badge">
{/* TODO: Estilize para parecer com a referência */}
PROMOÇÃO
</span>
);
}App function · typescript · L12-L21 (10 LOC)src/test-utils/project.dummy.ts
export default function App() {
return (
<div className="min-h-screen bg-white">
<Header />
<main className="container mx-auto px-4 py-8">
<ProductGrid />
</main>
</div>
);
}`,ProductCard function · typescript · L43-L82 (40 LOC)src/test-utils/project.dummy.ts
export function ProductCard({ id, name, price, image, description }: ProductCardProps) {
const [quantity, setQuantity] = useState(1);
// ⚠️ Problema: recalcula a cada render
const formattedPrice = new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL',
}).format(price);
// ⚠️ Problema: nova função a cada render
const handleAddToCart = () => {
console.log('Adding to cart:', { id, quantity });
};
return (
<div className="bg-white border border-black/10 rounded-lg overflow-hidden">
<img src={image} alt={name} className="w-full h-48 object-cover" />
<div className="p-4">
<h3 className="font-bold text-black">{name}</h3>
<p className="text-gray-600 text-sm mt-1">{description}</p>
<p className="text-xl font-bold text-black mt-2">{formattedPrice}</p>
<div className="flex items-center gap-2 mt-4">
<input
type="number"
min="1"
value={quantity}
ProductGrid function · typescript · L121-L132 (12 LOC)src/test-utils/project.dummy.ts
export function ProductGrid() {
return (
<div>
<h2 className="text-2xl font-bold text-black mb-6">Equipamentos de Boxe</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{products.map((product) => (
<ProductCard key={product.id} {...product} />
))}
</div>
</div>
);
}`,‹ prevpage 2 / 2