Skip to content

Commit 92ef1bc

Browse files
authored
feat(recent-searches): add ability to remove individual recent searches (JhaSourav07#786)
## Description ## Pillar - [ ] 🎨 Pillar 1 — New Theme Design - [ ] 📐 Pillar 2 — Geometric SVG Improvement - [ ] 🕐 Pillar 3 — Timezone Logic Optimization - [ ] 🛠️ Other (Bug fix, refactoring, docs) ## Visual Preview ## Checklist before requesting a review: - [ ] I have read the `CONTRIBUTING.md` file. - [ ] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [ ] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise). - [ ] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). - [ ] I have updated `README.md` if I added a new theme or URL parameter. - [ ] I have started the repo. - [ ] I have made sure that i have only one commit to merge in this PR. - [ ] The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts). - [ ] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support.
1 parent 7d0b747 commit 92ef1bc

4 files changed

Lines changed: 71 additions & 11 deletions

File tree

app/page.test.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,24 @@ vi.mock('framer-motion', () => ({
5050
AnimatePresence: ({ children }: any) => <>{children}</>,
5151
}));
5252

53+
const mockRecentSearches = {
54+
searches: ['octocat', 'torvalds'] as string[],
55+
addSearch: vi.fn(),
56+
clearSearches: vi.fn(),
57+
removeSearch: vi.fn(),
58+
};
59+
5360
vi.mock('@/hooks/useRecentSearches', () => ({
54-
useRecentSearches: () => ({
55-
searches: ['octocat', 'torvalds'],
56-
addSearch: vi.fn(),
57-
clearSearches: vi.fn(),
58-
}),
61+
useRecentSearches: () => mockRecentSearches,
5962
}));
6063

6164
describe('LandingPage', () => {
6265
beforeEach(() => {
6366
vi.clearAllMocks();
67+
mockRecentSearches.searches = ['octocat', 'torvalds'];
68+
mockRecentSearches.addSearch = vi.fn();
69+
mockRecentSearches.clearSearches = vi.fn();
70+
mockRecentSearches.removeSearch = vi.fn();
6471

6572
// Mock fetch so the SVG preview useEffect resolves without a real network call.
6673
// Returns a minimal valid SVG so dangerouslySetInnerHTML has something to render.
@@ -233,4 +240,21 @@ describe('LandingPage', () => {
233240

234241
expect(screen.queryByLabelText('Clear input')).toBeNull();
235242
});
243+
244+
it('renders recent searches and handles individual deletion', () => {
245+
mockRecentSearches.searches = ['octocat', 'jhasourav07'];
246+
render(<LandingPage />);
247+
248+
expect(screen.getByText('octocat')).toBeDefined();
249+
expect(screen.getByText('jhasourav07')).toBeDefined();
250+
251+
const deleteButtons = screen.getAllByLabelText(/Remove/);
252+
expect(deleteButtons.length).toBe(2);
253+
254+
fireEvent.click(deleteButtons[0]);
255+
expect(mockRecentSearches.removeSearch).toHaveBeenCalledWith('octocat');
256+
257+
// Cleanup
258+
mockRecentSearches.searches = [];
259+
});
236260
});

app/page.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export default function LandingPage() {
7272
const [svgContent, setSvgContent] = useState<string | null>(null);
7373
const [svgState, setSvgState] = useState<'idle' | 'loading' | 'loaded'>('idle');
7474
const guideRef = useRef<HTMLDivElement>(null);
75-
const { searches, addSearch, clearSearches } = useRecentSearches();
75+
const { searches, addSearch, clearSearches, removeSearch } = useRecentSearches();
7676
const trimmedUsername = username.trim();
7777
const hasUsername = trimmedUsername.length > 0;
7878

@@ -281,13 +281,26 @@ export default function LandingPage() {
281281
<div className="flex flex-wrap items-center gap-2 mb-6 mt-3">
282282
<span className="text-xs text-[#A1A1AA]">Recent:</span>
283283
{searches.map((s) => (
284-
<button
284+
<span
285285
key={s}
286-
onClick={() => setUsername(s)}
287-
className="rounded-full border border-[rgba(255,255,255,0.08)] bg-[#111] px-3 py-1 text-xs text-white/70 transition-all hover:border-[rgba(255,255,255,0.2)] hover:text-white"
286+
className="inline-flex items-center gap-1.5 rounded-full border border-[rgba(255,255,255,0.08)] bg-[#111] pl-3 pr-2 py-1 text-xs text-white/70 transition-all hover:border-[rgba(255,255,255,0.2)] hover:text-white group/pill"
288287
>
289-
{s}
290-
</button>
288+
<button
289+
type="button"
290+
onClick={() => setUsername(s)}
291+
className="transition-colors hover:text-white"
292+
>
293+
{s}
294+
</button>
295+
<button
296+
type="button"
297+
onClick={() => removeSearch(s)}
298+
className="rounded-full p-0.5 text-white/40 hover:bg-white/10 hover:text-white transition-all flex items-center justify-center"
299+
aria-label={`Remove ${s} from recent searches`}
300+
>
301+
<X size={10} />
302+
</button>
303+
</span>
291304
))}
292305
<button
293306
onClick={clearSearches}

hooks/useRecentSearches.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,20 @@ describe('useRecentSearches', () => {
7979
expect(removeItemSpy).toHaveBeenCalledWith(STORAGE_KEY);
8080
});
8181

82+
it('removes an individual search', () => {
83+
const { result } = renderHook(() => useRecentSearches());
84+
act(() => {
85+
result.current.addSearch('torvalds');
86+
});
87+
act(() => {
88+
result.current.addSearch('gaearon');
89+
});
90+
act(() => {
91+
result.current.removeSearch('torvalds');
92+
});
93+
expect(result.current.searches).toEqual(['gaearon']);
94+
});
95+
8296
it('persists searches across remounts', () => {
8397
const { result, unmount } = renderHook(() => useRecentSearches());
8498
act(() => {

hooks/useRecentSearches.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,19 @@ export function useRecentSearches() {
8282
}));
8383
};
8484

85+
const removeSearch = (query: string): void => {
86+
setState((prev) => {
87+
const filtered = prev.searches.filter((s) => s !== query);
88+
writeStorage(filtered);
89+
return { ...prev, searches: filtered };
90+
});
91+
};
92+
8593
// Return empty searches until after hydration to prevent SSR/client mismatch.
8694
return {
8795
searches: state.mounted ? state.searches : [],
8896
addSearch,
8997
clearSearches,
98+
removeSearch,
9099
};
91100
}

0 commit comments

Comments
 (0)