diff --git a/t29-project-1/package-lock.json b/t29-project-1/package-lock.json index 997e31d..cdafae6 100644 --- a/t29-project-1/package-lock.json +++ b/t29-project-1/package-lock.json @@ -13,7 +13,8 @@ "dotenv": "^17.4.2", "express": "^5.2.1", "react": "^19.2.8", - "react-dom": "^19.2.8" + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.4" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -3758,6 +3759,57 @@ "react": "^19.2.8" } }, + "node_modules/react-router": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.4.tgz", + "integrity": "sha512-PUPQcMhMGRAslLcvtlPz/kmzBEWPhLdgLFrL7pLNepBL6dX0lWj4WD2cUYVgYCuT3jxvghYFg81cDTj44DhetQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.4.tgz", + "integrity": "sha512-yrfmJHIpDG7taCpqKjT1G5B6q3O2K+RN8/fgNf0lTjCwiPbQ0ei6vXX9ZjQR+7ld8Tr7Z5xmyMnZ8YJrphWQUw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3898,6 +3950,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", diff --git a/t29-project-1/package.json b/t29-project-1/package.json index 19bf385..36cd6ac 100644 --- a/t29-project-1/package.json +++ b/t29-project-1/package.json @@ -30,7 +30,8 @@ "dotenv": "^17.4.2", "express": "^5.2.1", "react": "^19.2.8", - "react-dom": "^19.2.8" + "react-dom": "^19.2.8", + "react-router-dom": "^7.18.4" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/t29-project-1/src/App.css b/t29-project-1/src/App.css index cd03c39..77f6b53 100644 --- a/t29-project-1/src/App.css +++ b/t29-project-1/src/App.css @@ -40,6 +40,12 @@ flex-shrink: 0; } +.favorite-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; +} + /* Mobile Responsive Adjustments */ @media (max-width: 600px) { .nav-layout { diff --git a/t29-project-1/src/App.tsx b/t29-project-1/src/App.tsx index d9b3191..0fe05bb 100644 --- a/t29-project-1/src/App.tsx +++ b/t29-project-1/src/App.tsx @@ -1,6 +1,8 @@ import { useCatNavigation } from './hooks/useCatNavigation'; +import { useFavoriteCat } from './hooks/useFavoriteCat'; import { NavPrevious, NavNext } from './components/ActionButtons'; import { CatCard } from './components/CatCard'; +import { Link } from 'react-router-dom'; import './App.css'; export default function App() { @@ -12,16 +14,23 @@ export default function App() { handleNext, handlePrevious, } = useCatNavigation(); - + const { isFavorite, toggleFavorite } = useFavoriteCat(currentCat); return (
+ Favorite diff --git a/t29-project-1/src/components/Favorites.css b/t29-project-1/src/components/Favorites.css index cf958f5..5073468 100644 --- a/t29-project-1/src/components/Favorites.css +++ b/t29-project-1/src/components/Favorites.css @@ -34,6 +34,11 @@ stroke 0.15s ease; } +.star-svg.is-active { + fill: #7d5d75; + stroke: #fff4c8; +} + /* Invert colors on BOTH hover and keyboard focus */ .star-btn:hover .star-svg, .star-btn:focus-visible .star-svg { diff --git a/t29-project-1/src/components/Favorites.tsx b/t29-project-1/src/components/Favorites.tsx index c97bcb6..e6c7f48 100644 --- a/t29-project-1/src/components/Favorites.tsx +++ b/t29-project-1/src/components/Favorites.tsx @@ -2,7 +2,7 @@ import React from 'react'; import './Favorites.css'; interface FavoritesProps { - onToggleFavorites: () => void; + onToggleFavorites?: () => void; isViewingFavorites?: boolean; } @@ -16,7 +16,7 @@ export const Favorites: React.FC = ({ className="star-btn" onClick={onToggleFavorites} aria-label={ - isViewingFavorites ? 'Return to cat browsing' : 'Go to favorites' + isViewingFavorites ? 'Remove from favorites' : 'Add to favorites' } aria-pressed={isViewingFavorites} > diff --git a/t29-project-1/src/hooks/useFavoriteCat.ts b/t29-project-1/src/hooks/useFavoriteCat.ts new file mode 100644 index 0000000..ad94baa --- /dev/null +++ b/t29-project-1/src/hooks/useFavoriteCat.ts @@ -0,0 +1,29 @@ +import { useState } from 'react'; +import type { CatData } from '../api/catApi'; +import { + addFavoriteCat, + getFavoriteCats, + removeFavoriteCat, +} from '../utils/favorites'; + +export function useFavoriteCat(currentCat: CatData | null) { + const [favoriteCats, setFavoriteCats] = useState(getFavoriteCats); + const isFavorite = Boolean( + currentCat?.id && + favoriteCats.some((favorite) => favorite.id === currentCat.id) + ); + + const toggleFavorite = () => { + if (!currentCat?.id) return; + + if (isFavorite) { + removeFavoriteCat(currentCat.id); + } else { + addFavoriteCat(currentCat); + } + + setFavoriteCats(getFavoriteCats()); + }; + + return { isFavorite, toggleFavorite }; +} diff --git a/t29-project-1/src/main.tsx b/t29-project-1/src/main.tsx index 8aff03e..d919520 100644 --- a/t29-project-1/src/main.tsx +++ b/t29-project-1/src/main.tsx @@ -1,8 +1,10 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; import './index.css'; import App from './App.tsx'; +import FavoritePage from './pages/FavoritePage.tsx'; const queryClient = new QueryClient({ defaultOptions: { @@ -16,7 +18,12 @@ const queryClient = new QueryClient({ createRoot(document.getElementById('root')!).render( - + + + } /> + } /> + + ); diff --git a/t29-project-1/src/pages/FavoritePage.tsx b/t29-project-1/src/pages/FavoritePage.tsx new file mode 100644 index 0000000..edf8218 --- /dev/null +++ b/t29-project-1/src/pages/FavoritePage.tsx @@ -0,0 +1,37 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { CatCard } from '../components/CatCard'; +import { getFavoriteCats, removeFavoriteCat } from '../utils/favorites'; +import '../App.css'; + +export default function FavoritePage() { + const [favoriteCats, setFavoriteCats] = useState(getFavoriteCats); + + const handleRemove = (catId?: string) => { + if (!catId) return; + + removeFavoriteCat(catId); + setFavoriteCats(getFavoriteCats()); + }; + + return ( +
+ Back to cats +

Favorites

+ {favoriteCats.length === 0 ? ( +

No favorite cats yet.

+ ) : ( +
+ {favoriteCats.map((cat) => ( + handleRemove(cat.id)} + /> + ))} +
+ )} +
+ ); +}