Skip to content
Merged
75 changes: 74 additions & 1 deletion components/bounty/bounty-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client"

import { useMemo, useState } from "react"
import { RatingModal } from "../rating/rating-modal"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import type { Bounty } from "@/types/bounty"
Expand All @@ -20,8 +21,9 @@
const [loading, setLoading] = useState(false)
// const router = useRouter()

// Mock user ID for now - in real app this comes from auth context
// Mock user ID and maintainer check for now - in real app this comes from auth context
const CURRENT_USER_ID = "mock-user-123"
const IS_MAINTAINER = true // TODO: Replace with real maintainer check

// const isClaimable = bounty.status === "open"

Expand Down Expand Up @@ -75,7 +77,61 @@
}
}

// Rating modal state
const [showRating, setShowRating] = useState(false)
const [completed, setCompleted] = useState(false)
const [lastRating, setLastRating] = useState<number | null>(null)
const [reputationGain, setReputationGain] = useState<number | null>(null)
const [hasRated, setHasRated] = useState(false)

const handleMarkCompleted = async () => {
if (!IS_MAINTAINER) {
alert('Only maintainers can mark as completed.');
return;
}
setLoading(true)
// Simulate completion API call
setTimeout(() => {
setLoading(false)
setCompleted(true)
setShowRating(true)
}, 1000)
}

const handleSubmitRating = async (rating: number, _feedback: string) => {

Check warning on line 101 in components/bounty/bounty-sidebar.tsx

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

'_feedback' is defined but never used
if (hasRated) {
alert('You have already rated this contributor.');
return;
}
if (!IS_MAINTAINER) {
alert('Only maintainers can rate contributors.');
return;
}
if (!completed) {
alert('Bounty must be marked as completed before rating.');
return;
}
Comment on lines +114 to +126
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "bounty-sidebar.tsx" -type f

Repository: boundlessfi/bounties

Length of output: 102


🏁 Script executed:

find . -name "*rating*modal*" -o -name "*RatingModal*" | grep -E "\.(tsx?|jsx?)$"

Repository: boundlessfi/bounties

Length of output: 100


🏁 Script executed:

find . -type f -name "*.tsx" | xargs grep -l "RatingModal" | head -20

Repository: boundlessfi/bounties

Length of output: 139


🏁 Script executed:

wc -l ./components/bounty/bounty-sidebar.tsx

Repository: boundlessfi/bounties

Length of output: 106


🏁 Script executed:

cat -n ./components/bounty/bounty-sidebar.tsx | grep -A 50 "handleSubmitRating"

Repository: boundlessfi/bounties

Length of output: 4539


🏁 Script executed:

wc -l ./components/rating/rating-modal.tsx

Repository: boundlessfi/bounties

Length of output: 103


🏁 Script executed:

cat -n ./components/rating/rating-modal.tsx

Repository: boundlessfi/bounties

Length of output: 2888


Guard failures should reject so the modal doesn't show success.

RatingModal treats any resolved onSubmit as success. Returning early after alert() still resolves the Promise and will show the success view. Reject/throw to keep the modal in an error state.

🛠️ Suggested fix
   if (hasRated) {
     alert('You have already rated this contributor.');
-    return;
+    throw new Error('You have already rated this contributor.');
   }
   if (!IS_MAINTAINER) {
     alert('Only maintainers can rate contributors.');
-    return;
+    throw new Error('Only maintainers can rate contributors.');
   }
   if (!completed) {
     alert('Bounty must be marked as completed before rating.');
-    return;
+    throw new Error('Bounty must be marked as completed before rating.');
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleSubmitRating = async (rating: number, feedback: string) => {
if (hasRated) {
alert('You have already rated this contributor.');
return;
}
if (!IS_MAINTAINER) {
alert('Only maintainers can rate contributors.');
return;
}
if (!completed) {
alert('Bounty must be marked as completed before rating.');
return;
}
const handleSubmitRating = async (rating: number, feedback: string) => {
if (hasRated) {
alert('You have already rated this contributor.');
throw new Error('You have already rated this contributor.');
}
if (!IS_MAINTAINER) {
alert('Only maintainers can rate contributors.');
throw new Error('Only maintainers can rate contributors.');
}
if (!completed) {
alert('Bounty must be marked as completed before rating.');
throw new Error('Bounty must be marked as completed before rating.');
}
🤖 Prompt for AI Agents
In `@components/bounty/bounty-sidebar.tsx` around lines 101 - 113, The
early-return guards in handleSubmitRating (checks for hasRated, IS_MAINTAINER,
completed) currently call alert() then return, which resolves the RatingModal
onSubmit Promise and shows success; change each guard to instead throw an Error
or return a rejected Promise with a clear message (e.g., "Already rated", "Only
maintainers can rate", "Bounty not completed") so the modal treats the outcome
as a failure and stays in the error state; keep the existing alert if you want
UI feedback but ensure the function subsequently throws/rejects using those same
messages.

// Simulate API call to reputation endpoint and calculate points
await new Promise((res) => setTimeout(res, 1000))
setLastRating(rating)
setReputationGain(rating * 10)
setHasRated(true)
setShowRating(false)
// Notify contributor (mock)
toast.success(`You have been rated ${rating} star${rating > 1 ? 's' : ''} and gained +${rating * 10} reputation!`, {
description: 'Congratulations on your contribution!'
})
}

const renderActionButton = () => {
if (bounty.status === 'claimed' && IS_MAINTAINER && !completed) {
return (
<Button onClick={handleMarkCompleted} disabled={loading} className="w-full gap-2 bg-green-600 text-white hover:bg-green-700">
{loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Check className="mr-2 h-4 w-4" />}
Mark as Completed
</Button>
)
}
Comment on lines 141 to +149
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Allow reopening the rating flow after completion.

Once the modal is dismissed, there’s no way to rate later because the “Mark as Completed” button disappears and no “Rate Contributor” action is shown. This blocks a core flow if the maintainer closes the modal accidentally.

🛠️ Suggested fix (add a “Rate Contributor” action)
   const renderActionButton = () => {
     if (bounty.status === 'claimed' && IS_MAINTAINER && !completed) {
       return (
         <Button onClick={handleMarkCompleted} disabled={loading} className="w-full gap-2 bg-green-600 text-white hover:bg-green-700">
           {loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Check className="mr-2 h-4 w-4" />}
           Mark as Completed
         </Button>
       )
     }
+    if (bounty.status === 'claimed' && IS_MAINTAINER && completed && !hasRated) {
+      return (
+        <Button onClick={() => setShowRating(true)} className="w-full gap-2 bg-primary text-primary-foreground hover:bg-primary/90">
+          Rate Contributor
+        </Button>
+      )
+    }
🤖 Prompt for AI Agents
In `@components/bounty/bounty-sidebar.tsx` around lines 126 - 134, The current
renderActionButton hides the maintainer rating flow after completion; update
renderActionButton to show a persistent "Rate Contributor" action when the
bounty is completed and the viewer is a maintainer by adding a new button
rendered for the case bounty.status === 'claimed' && IS_MAINTAINER && completed;
wire its onClick to open the rating modal (create or reuse a handler like
handleOpenRating or openRatingModal) and ensure the rating modal state/handler
(e.g., isRatingOpen, setIsRatingOpen, handleSubmitRating) exists and is used so
the maintainer can re-open the rating UI after dismissing it; keep the existing
handleMarkCompleted behavior unchanged for marking completion.

if (bounty.status !== 'open') {
const labels: Record<string, string> = {
claimed: 'Already Claimed',
Expand Down Expand Up @@ -155,6 +211,23 @@

return (
<div className="sticky top-4 rounded-xl border border-gray-800 bg-background-card p-6 space-y-4">
{/* Sidebar UI */}
{showRating && !hasRated && (
<RatingModal
contributor={{ id: bounty.claimedBy || '', name: 'Contributor', reputation: 100 + (reputationGain || 0) }}
bounty={{ id: bounty.id, title: bounty.issueTitle }}
onSubmit={handleSubmitRating}
onClose={() => setShowRating(false)}
/>
)}

{/* Show rating and reputation gain after rating, visible to all users if available */}
{lastRating && reputationGain && (
<div className="p-4 mb-4 rounded bg-green-900/60 text-green-200 border border-green-700">
<div className="mb-1">{IS_MAINTAINER ? 'You rated the contributor:' : 'Contributor was rated:'} <b>{lastRating} / 5</b> stars</div>
<div>Reputation gained: <b>+{reputationGain}</b></div>
</div>
)}
<Button asChild className="w-full gap-2 bg-primary text-primary-foreground hover:bg-primary/90">
<a href={bounty.githubIssueUrl} target="_blank" rel="noopener noreferrer">
<Github className="size-4" />
Expand Down
82 changes: 82 additions & 0 deletions components/rating/rating-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React, { useState } from 'react';
import { RatingStars } from './rating-stars';

interface RatingModalProps {
contributor: {
id: string;
name: string;
reputation: number;
};
bounty: {
id: string;
title: string;
};
onSubmit: (rating: number, feedback: string) => Promise<void>;
onClose: () => void;
}

export const RatingModal: React.FC<RatingModalProps> = ({ contributor, bounty, onSubmit, onClose }) => {
const [rating, setRating] = useState(0);
const [feedback, setFeedback] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);

const handleSubmit = async () => {
if (rating < 1 || rating > 5) {
setError('Please select a rating between 1 and 5.');
return;
}
setLoading(true);
setError(null);
try {
await onSubmit(rating, feedback);
setSuccess(true);
} catch (err) {
console.error(err)
setError('Failed to submit rating. Please try again.');
} finally {
setLoading(false);
}
};

if (success) {
return (
<div className="modal">
<h2>Success!</h2>
<p>Rating submitted. Contributor reputation updated.</p>
<button onClick={onClose}>Close</button>
</div>
);
}

return (
<div className="modal">
<h2>Rate Contributor</h2>
<div>
<strong>Bounty:</strong> {bounty.title}
</div>
<div>
<strong>Contributor:</strong> {contributor.name}
</div>
<div>
<strong>Current Reputation:</strong> {contributor.reputation}
</div>
<div style={{ margin: '16px 0' }}>
<RatingStars value={rating} onChange={setRating} />
</div>
<textarea
placeholder="Optional feedback"
value={feedback}
onChange={e => setFeedback(e.target.value)}
rows={3}
style={{ width: '100%', marginBottom: 8 }}
/>
{error && <div style={{ color: 'red', marginBottom: 8 }}>{error}</div>}
<button onClick={handleSubmit} disabled={loading}>
{loading ? 'Submitting...' : 'Submit'}
</button>
<button onClick={onClose} style={{ marginLeft: 8 }}>Cancel</button>
</div>
);
};
49 changes: 49 additions & 0 deletions components/rating/rating-stars.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { useState } from 'react';

interface RatingStarsProps {
value: number;
onChange?: (value: number) => void;
disabled?: boolean;
displayOnly?: boolean;
}

export const RatingStars: React.FC<RatingStarsProps> = ({ value, onChange, disabled, displayOnly }) => {
const [hovered, setHovered] = useState<number | null>(null);

const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (!onChange || disabled || displayOnly) return;
if (e.key === 'ArrowLeft' && value > 1) onChange(value - 1);
if (e.key === 'ArrowRight' && value < 5) onChange(value + 1);
};

return (
<div
tabIndex={displayOnly ? -1 : 0}
role={displayOnly ? 'img' : 'slider'}
aria-valuenow={value}
aria-valuemin={1}
aria-valuemax={5}
onKeyDown={handleKeyDown}
style={{ display: 'flex', gap: 4, outline: 'none', cursor: displayOnly ? 'default' : 'pointer' }}
>
{[1, 2, 3, 4, 5].map((star) => (
<span
key={star}
onMouseEnter={() => !displayOnly && setHovered(star)}
onMouseLeave={() => !displayOnly && setHovered(null)}
onClick={() => onChange && !disabled && !displayOnly && onChange(star)}
style={{
color: (hovered ?? value) >= star ? '#FFD700' : '#CCC',
fontSize: 28,
transition: 'color 0.2s',
pointerEvents: displayOnly ? 'none' : 'auto',
userSelect: 'none',
}}
Comment on lines +10 to +41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "rating-stars.tsx" | head -20

Repository: boundlessfi/bounties

Length of output: 100


🏁 Script executed:

cat -n ./components/rating/rating-stars.tsx

Repository: boundlessfi/bounties

Length of output: 1975


Ensure disabled state prevents all interactions and align ARIA props with role.

When disabled=true but displayOnly=false, hover effects still fire because the handlers check !displayOnly only (lines 32-33, 39). Additionally, ARIA slider attributes (aria-valuenow, aria-valuemin, aria-valuemax) are always present even when role="img", which is semantically incorrect. Consolidate interaction state logic and conditionally apply ARIA slider attributes only when appropriate.

Suggested fix
 export const RatingStars: React.FC<RatingStarsProps> = ({ value, onChange, disabled, displayOnly }) => {
   const [hovered, setHovered] = useState<number | null>(null);
+  const isDisplayOnly = displayOnly || !onChange;
+  const isInteractive = !isDisplayOnly && !disabled;
 
   const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
-    if (!onChange || disabled || displayOnly) return;
+    if (!isInteractive) return;
     if (e.key === 'ArrowLeft' && value > 1) onChange(value - 1);
     if (e.key === 'ArrowRight' && value < 5) onChange(value + 1);
   };
 
   return (
     <div
-      tabIndex={displayOnly ? -1 : 0}
-      role={displayOnly ? 'img' : 'slider'}
-      aria-valuenow={value}
-      aria-valuemin={1}
-      aria-valuemax={5}
+      tabIndex={isInteractive ? 0 : -1}
+      role={isDisplayOnly ? 'img' : 'slider'}
+      aria-label={isDisplayOnly ? `${value} out of 5 stars` : 'Rating'}
+      aria-valuenow={isDisplayOnly ? undefined : value}
+      aria-valuemin={isDisplayOnly ? undefined : 0}
+      aria-valuemax={isDisplayOnly ? undefined : 5}
+      aria-disabled={isDisplayOnly ? undefined : disabled}
       onKeyDown={handleKeyDown}
-      style={{ display: 'flex', gap: 4, outline: 'none', cursor: displayOnly ? 'default' : 'pointer' }}
+      style={{ display: 'flex', gap: 4, outline: 'none', cursor: isInteractive ? 'pointer' : 'default' }}
     >
       {[1, 2, 3, 4, 5].map((star) => (
         <span
           key={star}
-          onMouseEnter={() => !displayOnly && setHovered(star)}
-          onMouseLeave={() => !displayOnly && setHovered(null)}
-          onClick={() => onChange && !disabled && !displayOnly && onChange(star)}
+          onMouseEnter={() => isInteractive && setHovered(star)}
+          onMouseLeave={() => isInteractive && setHovered(null)}
+          onClick={() => isInteractive && onChange?.(star)}
           style={{
             color: (hovered ?? value) >= star ? '#FFD700' : '#CCC',
             fontSize: 28,
             transition: 'color 0.2s',
-            pointerEvents: displayOnly ? 'none' : 'auto',
+            pointerEvents: isInteractive ? 'auto' : 'none',
             userSelect: 'none',
           }}
           aria-label={star + ' star'}
🤖 Prompt for AI Agents
In `@components/rating/rating-stars.tsx` around lines 10 - 41, The component
allows interactions when disabled because mouse handlers only check displayOnly;
update all interaction checks to consider disabled as well (e.g.,
onMouseEnter/onMouseLeave/onClick and handleKeyDown should early-return if
disabled || displayOnly || !onChange) and use the disabled flag to set
pointerEvents and cursor; also only apply slider ARIA attributes (aria-valuenow,
aria-valuemin, aria-valuemax and role="slider") when not displayOnly (i.e., when
role is slider) and when not disabled ensure tabIndex is 0 otherwise -1 so the
DOM reflects the true interactive state; touch up references in RatingStars,
handleKeyDown, hovered/setHovered, and the span handlers to implement these
conditional checks and ARIA changes.

aria-label={star + ' star'}
>
</span>
))}
</div>
);
};
5 changes: 3 additions & 2 deletions components/ui/resizable-navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,13 @@ interface MobileNavMenuProps {
export const Navbar = ({ children, className }: NavbarProps) => {
const ref = useRef<HTMLDivElement>(null);
const { scrollY } = useScroll({
target: ref,
// Cast ref to a nullable Element ref to satisfy motion's typings at build time
target: ref as unknown as React.RefObject<Element | null>,
offset: ["start start", "end start"],
});
const [visible, setVisible] = useState<boolean>(false);

useMotionValueEvent(scrollY, "change", (latest) => {
useMotionValueEvent(scrollY, "change", (latest: number) => {
if (latest > 100) {
setVisible(true);
} else {
Expand Down
24 changes: 11 additions & 13 deletions hooks/use-local-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,22 @@ export function useLocalStorage<T>(
// ... persists the new value to localStorage.
const setValue = React.useCallback(
(value: T | ((val: T) => T)) => {
try {
// Allow value to be a function so we have same API as useState
// Use functional update to get the latest value
setStoredValue((currentValue) => {
const valueToStore =
value instanceof Function ? value(currentValue) : value
// Use functional update to get the latest value
setStoredValue((currentValue) => {
const valueToStore =
value instanceof Function ? value(currentValue) : value

// Save to local storage
// Save to local storage (catch errors here so they don't escape React internals)
try {
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore))
}
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error)
}

return valueToStore
})
} catch (error) {
// A more advanced implementation would handle the error case
console.warn(`Error setting localStorage key "${key}":`, error)
}
return valueToStore
})
},
[key]
)
Expand Down
12 changes: 10 additions & 2 deletions hooks/use-media-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,16 @@ export function useMediaQuery(query: string): boolean {
// Update the state with the current value
setMatches(media.matches)

// Listener callback
const listener = (e: MediaQueryListEvent) => setMatches(e.matches)
// Listener callback (supports event or direct call fallback)
const listener = (e?: MediaQueryListEvent) => {
if (e && typeof e.matches === 'boolean') {
setMatches(e.matches)
} else {
// Fallback: read current value from matchMedia in case tests call the
// handler directly and replaced the underlying matchMedia implementation.
setMatches(window.matchMedia(query).matches)
}
}

// Register listener with fallback for older browsers
if (media.addEventListener) {
Expand Down
27 changes: 18 additions & 9 deletions hooks/use-throttle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,24 @@ export function useThrottle<T>(value: T, limit: number): T {


React.useEffect(() => {
const handler = setTimeout(
() => {
if (Date.now() - lastRan.current >= limit) {
setThrottledValue(value)
lastRan.current = Date.now()
}
},
limit - (Date.now() - lastRan.current)
)
const now = Date.now()

if (lastRan.current === 0) {
lastRan.current = now
}

if (now - lastRan.current >= limit) {
setThrottledValue(value)
lastRan.current = now
return
}

const remaining = Math.max(0, limit - (now - lastRan.current))

const handler = setTimeout(() => {
setThrottledValue(value)
lastRan.current = Date.now()
}, remaining)

return () => {
clearTimeout(handler)
Expand Down
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const createJestConfig = nextJest({
const customJestConfig = {
testEnvironment: 'jest-environment-jsdom',
testMatch: ['**/__tests__/**/*.test.ts'],
setupFiles: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/$1',
},
Expand Down
16 changes: 16 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Ensure localStorage methods are configurable and writable so tests can spyOn/set mocks
const createLocalStorageMock = () => {
let store = {}
return {
getItem: (key) => (Object.prototype.hasOwnProperty.call(store, key) ? store[key] : null),
setItem: (key, value) => { store[String(key)] = String(value) },
removeItem: (key) => { delete store[String(key)] },
clear: () => { store = {} },
}
}

Object.defineProperty(window, 'localStorage', {
configurable: true,
writable: true,
value: createLocalStorageMock(),
})
4 changes: 2 additions & 2 deletions lib/mock-bounty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ Add a dark mode toggle to the application settings page that persists user prefe
tags: ["ui", "theme", "settings", "dark-mode"],
status: "claimed",
claimedBy: "dev_user_123",
claimedAt: "2024-01-01T00:00:00Z",
claimExpiresAt: "2024-01-15T00:00:00Z", // Expired
claimedAt: "2026-01-01T00:00:00Z",
claimExpiresAt: "2026-01-15T00:00:00Z",
createdAt: "2025-01-10T08:00:00Z",
updatedAt: "2025-01-17T11:00:00Z",
},
Expand Down
20 changes: 20 additions & 0 deletions types/shims.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
declare module "@tabler/icons-react" {
import * as React from 'react'
export const IconMenu2: React.FC<React.SVGProps<SVGSVGElement>>
export const IconX: React.FC<React.SVGProps<SVGSVGElement>>
}

declare module "motion/react" {
import { RefObject } from 'react'

export const motion: unknown
export const AnimatePresence: unknown

export function useScroll(options?: { target?: RefObject<Element | null>; offset?: string[] }): { scrollY: { get(): number } }

export function useMotionValueEvent<T = number>(
motionValue: { get(): T },
eventName: string,
handler: (latest: T) => void
): void
}
Comment on lines +1 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the file exists and show its content
echo "=== Checking types/shims.d.ts ===" 
if [ -f "types/shims.d.ts" ]; then
  echo "File exists, content:"
  cat -n types/shims.d.ts
else
  echo "File not found at types/shims.d.ts"
  # Search for it
  find . -name "shims.d.ts" -type f 2>/dev/null
fi

echo ""
echo "=== Looking for package.json in root ==="
if [ -f "package.json" ]; then
  echo "Found package.json, checking dependencies:"
  jq '.dependencies // {} | keys' package.json 2>/dev/null || cat package.json | head -30
fi

echo ""
echo "=== Checking for tsconfig.json ==="
if [ -f "tsconfig.json" ]; then
  echo "Found tsconfig.json:"
  cat tsconfig.json
fi

Repository: boundlessfi/bounties

Length of output: 2679


🏁 Script executed:

#!/bin/bash
# Search for references to `@tabler/icons-react` and motion/react in the codebase
echo "=== References to `@tabler/icons-react` ==="
rg "@tabler/icons-react" --type ts --type tsx --type js --type jsx -B 2 -A 2

echo ""
echo "=== References to motion/react ==="
rg "motion/react" --type ts --type tsx --type js --type jsx -B 2 -A 2

echo ""
echo "=== References to framer-motion ==="
rg "framer-motion" --type ts --type tsx --type js --type jsx -B 2 -A 2

Repository: boundlessfi/bounties

Length of output: 279


🏁 Script executed:

#!/bin/bash
# Try to find build or lint configuration files that might show errors
echo "=== Looking for build configuration ==="
find . -maxdepth 2 -type f \( -name ".eslintrc*" -o -name "eslint.config.*" -o -name "next.config.*" -o -name "tsconfig.*.json" -o -name ".github" \) 2>/dev/null | head -20

echo ""
echo "=== Checking for CI configuration ==="
if [ -d ".github/workflows" ]; then
  echo "GitHub workflows found:"
  ls -la .github/workflows/
  echo ""
  echo "Build/lint workflow content:"
  cat .github/workflows/*.yml 2>/dev/null | grep -A 20 "typescript\|build\|lint\|tsc" || echo "No explicit build commands found"
fi

echo ""
echo "=== Checking package.json for devDependencies with `@types` or TypeScript ==="
cat package.json | jq '.devDependencies // empty | to_entries[] | select(.key | contains("typescript") or contains("@types") or contains("eslint"))' 2>/dev/null | head -30

Repository: boundlessfi/bounties

Length of output: 1647


🌐 Web query:

@tabler/icons-react TypeScript types definitions built-in

💡 Result:

Yes — @tabler/icons-react ships TypeScript typings built-in (you don’t need a separate @types/... package).

  • The package’s package.json includes a types entry pointing to its bundled .d.ts file (e.g. ./dist/esm/tabler-icons-react.d.ts). (app.unpkg.com)
  • The Tabler Icons repo README also states that @tabler/icons-react exports its own type declarations for React + TypeScript. (github.com)

Citations:


🌐 Web query:

motion react framer-motion TypeScript types built-in v12

💡 Result:

In Motion/Framer Motion v12, the TypeScript types are built-in (the packages ship their own .d.ts files), so you generally do not install any @types/... package.

  • Install / import (Motion for React): install motion, then import from "motion/react". (motion.dev)
  • Built-in typings are published with the package: [email protected] exports a types entry (./dist/index.d.ts) and the React entrypoint also points at ../dist/react.d.ts. (app.unpkg.com)
  • Framer Motion v12 also ships its own types: [email protected] exports types (./dist/types/index.d.ts) via exports and types fields. (app.unpkg.com)

Common v12 React types you can import/use

(Names as exposed in the library’s docs/types)

  • MotionProps (base animation/gesture/layout props)
  • HTMLMotionProps<Tag> (props for motion.div, motion.button, etc.)
  • SVGMotionProps<T>
  • MotionValue<T> and related helpers

The motion namespace is typed so each intrinsic element is a ForwardRefExoticComponent<HTMLMotionProps<K>>, and motion.create returns a component typed as MotionProps & Props. (tessl.io)

Typical TypeScript patterns

import { motion, type MotionProps } from "motion/react";

type MyMotionDivProps = MotionProps & React.ComponentPropsWithoutRef<"div">;

export function MyMotionDiv(props: MyMotionDivProps) {
  return <motion.div {...props} />;
}
import { motion } from "motion/react";

type CardProps = { title: string };

const Card = (props: CardProps) => <div>{props.title}</div>;

export const MotionCard = motion.create(Card); // typed as MotionProps & CardProps

Citations:


Remove unnecessary any type shims — both packages include built-in TypeScript definitions.

The any types in this file conflict with your strict: true TypeScript configuration and are unnecessary. Both @tabler/icons-react and motion (v12+) ship with their own complete type definitions, so these ambient declarations should not be needed.

Either remove the shim file entirely and rely on the packages' built-in types, or if you must keep ambient declarations, re-export the actual package exports:

🛠️ Recommended fixes

Option 1: Remove the shim entirely (preferred if packages' types resolve correctly)

  • Delete types/shims.d.ts and ensure TypeScript can resolve the packages' built-in types.

Option 2: Re-export actual types from the packages

 declare module "@tabler/icons-react" {
-  import * as React from 'react'
-  export const IconMenu2: React.FC<any>
-  export const IconX: React.FC<any>
+  export * from "@tabler/icons-react";
 }

 declare module "motion/react" {
-  export const motion: any
-  export const AnimatePresence: any
-  export const useScroll: any
-  export const useMotionValueEvent: any
+  export * from "motion/react";
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
declare module "@tabler/icons-react" {
import * as React from 'react'
export const IconMenu2: React.FC<any>
export const IconX: React.FC<any>
}
declare module "motion/react" {
export const motion: any
export const AnimatePresence: any
export const useScroll: any
export const useMotionValueEvent: any
}
declare module "@tabler/icons-react" {
export * from "@tabler/icons-react";
}
declare module "motion/react" {
export * from "motion/react";
}
🧰 Tools
🪛 GitHub Check: build-and-lint (24.x)

[failure] 11-11:
Unexpected any. Specify a different type


[failure] 10-10:
Unexpected any. Specify a different type


[failure] 9-9:
Unexpected any. Specify a different type


[failure] 8-8:
Unexpected any. Specify a different type


[failure] 4-4:
Unexpected any. Specify a different type


[failure] 3-3:
Unexpected any. Specify a different type

🤖 Prompt for AI Agents
In `@types/shims.d.ts` around lines 1 - 12, The ambient shims declare overly-broad
any types for "@tabler/icons-react" (IconMenu2, IconX) and "motion/react"
(motion, AnimatePresence, useScroll, useMotionValueEvent); remove this shim file
entirely so TypeScript uses the packages' built-in types, or replace the
declarations by re-exporting the actual package types (e.g., export { IconMenu2,
IconX } from "@tabler/icons-react" and export { motion, AnimatePresence,
useScroll, useMotionValueEvent } from "motion/react") to eliminate the use of
any and satisfy strict:true.

Loading