-
Gender: {character.gender}
-
Origin: {character.origin.name}
-
Species: {character.species}
-
Status: {character.status}
+
+
+ Gender: {character.gender}
+
+
+ Origin: {character.origin.name}
+
+
+ Species: {character.species}
+
+
+ Status: {character.status}
+
-
+
Episodes
{episodes.map((episode) => (
@@ -100,15 +123,24 @@ export default function CharacterDetailCard({ character, onClose, onPrevious, on
-
- )
-}
\ No newline at end of file
+ );
+}
diff --git a/src/components/Hero.css b/src/components/Hero.css
index 8124d7a..fdfe9f7 100644
--- a/src/components/Hero.css
+++ b/src/components/Hero.css
@@ -73,4 +73,4 @@
.hero-characters {
display: none;
}
-}
\ No newline at end of file
+}
diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx
index 29c7ce3..be3bfab 100644
--- a/src/components/Hero.tsx
+++ b/src/components/Hero.tsx
@@ -3,23 +3,35 @@ import logo from "../assets/logo.png";
import "./Hero.css";
function Hero() {
- return (
-
-
-

+ return (
+
+
+

-
- Explore every character in the TV universe Rick and Morty.
- Enter an item name or keyword in the search bar to instantly
- narrow down the results and find exactly what you're looking for.
-
+
+ Explore every character in the TV universe Rick and Morty. Enter an
+ item name or keyword in the search bar to instantly narrow down the
+ results and find exactly what you're looking for.
+
-
Read more
-
+
+ {" "}
+ Read more{" "}
+
+
-
-
- );
+

+
+ );
}
-export default Hero;
\ No newline at end of file
+export default Hero;
diff --git a/src/index.css b/src/index.css
index 895c604..d437676 100644
--- a/src/index.css
+++ b/src/index.css
@@ -11,8 +11,8 @@
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
- --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
- --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
+ --sans: system-ui, "Segoe UI", Roboto, sans-serif;
+ --heading: system-ui, "Segoe UI", Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
diff --git a/src/main.tsx b/src/main.tsx
index 5b4f9dc..31358ab 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,12 +1,12 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './index.css'
-import App from './App.tsx'
-import CharactersPage from './pages/CharactersPage.tsx'
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import "./index.css";
+import App from "./App.tsx";
+import CharactersPage from "./pages/CharactersPage.tsx";
-createRoot(document.getElementById('root')!).render(
+createRoot(document.getElementById("root")!).render(
,
-)
+);
diff --git a/src/pages/CharactersPage.css b/src/pages/CharactersPage.css
index 40ce9b7..0714fa1 100644
--- a/src/pages/CharactersPage.css
+++ b/src/pages/CharactersPage.css
@@ -1,95 +1,95 @@
-.content{
- display: flex;
- padding-top: 6rem;
+.content {
+ display: flex;
+ padding-top: 6rem;
}
-.search-area{
- max-width: 56rem;
+.search-area {
+ max-width: 56rem;
}
-.filter-area{
- display: none;
+.filter-area {
+ display: none;
}
@media (min-width: 600px) {
- .filter-area {
- display: block;
- width: 13rem;
- height: fit-content;
- margin-right: 1rem;
- border: 2px solid #3F3F3F;
-
- border-radius: 1rem;
- padding: 2rem;
- }
+ .filter-area {
+ display: block;
+ width: 13rem;
+ height: fit-content;
+ margin-right: 1rem;
+ border: 2px solid #3f3f3f;
+
+ border-radius: 1rem;
+ padding: 2rem;
+ }
}
-.character-cards{
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 2rem;
+.character-cards {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 2rem;
}
.searchbar {
- display: flex;
- width: min(95%, 80rem);
- margin-bottom: 2rem;
+ display: flex;
+ width: min(95%, 80rem);
+ margin-bottom: 2rem;
- overflow: hidden;
- border: 2px solid #3f3f3f;
- border-radius: 0.5rem;
+ overflow: hidden;
+ border: 2px solid #3f3f3f;
+ border-radius: 0.5rem;
}
.searchfield {
- flex: 1;
- min-width: 0;
- height: 2.5rem;
- padding: 0 1rem;
-
- border: 0;
- outline: 0;
- font: inherit;
+ flex: 1;
+ min-width: 0;
+ height: 2.5rem;
+ padding: 0 1rem;
+
+ border: 0;
+ outline: 0;
+ font: inherit;
}
.search-button {
- min-height: 2.5rem;
- padding: 0 1.25rem;
- display: flex;
- align-items: center;
- justify-content: center;
-
- background-color: #42b4ca;
- border: 0;
- border-left: 2px solid #3f3f3f;
-
- font: inherit;
- font-weight: 700;
- cursor: pointer;
+ min-height: 2.5rem;
+ padding: 0 1.25rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ background-color: #42b4ca;
+ border: 0;
+ border-left: 2px solid #3f3f3f;
+
+ font: inherit;
+ font-weight: 700;
+ cursor: pointer;
}
-.search-icon{
- filter: brightness(0) invert(1);
+.search-icon {
+ filter: brightness(0) invert(1);
}
.searchbar:focus-within {
- border-color: #42b4ca;
- box-shadow: 0 0 0 3px rgb(66 180 202 / 25%);
+ border-color: #42b4ca;
+ box-shadow: 0 0 0 3px rgb(66 180 202 / 25%);
}
.search-button:hover {
- background-color: #369bad;
+ background-color: #369bad;
}
.filter-group {
- border:none;
- margin-bottom: 1rem;
+ border: none;
+ margin-bottom: 1rem;
}
.checkbox-option {
- display: flex;
- align-items: center;
- gap: 0.5rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
- width: fit-content;
- cursor: pointer;
-}
\ No newline at end of file
+ width: fit-content;
+ cursor: pointer;
+}
diff --git a/src/pages/CharactersPage.tsx b/src/pages/CharactersPage.tsx
index 727c985..ac82c9e 100644
--- a/src/pages/CharactersPage.tsx
+++ b/src/pages/CharactersPage.tsx
@@ -1,189 +1,226 @@
-import './CharactersPage.css'
+import "./CharactersPage.css";
import { useEffect, useState } from "react";
import CharacterCardSmall from "../components/CharacterCardSmall";
import CharacterDetailCard from "../components/CharacterDetailCard";
-import SearchIcon from "../assets/search.svg"
+import SearchIcon from "../assets/search.svg";
import type { Character } from "../types/Character";
type FilterCategory = "gender" | "species" | "status";
-const filters: { title: string, key: FilterCategory, options: string[] }[] = [
- { title: 'Gender', key: 'gender', options: ['Female', 'Male', 'Unknown'] },
- { title: 'Species', key: 'species', options: ['Human', 'Alien', 'Unknown'] },
- { title: 'Status', key: 'status', options: ['Alive', 'Dead', 'Unknown'] }
+const filters: { title: string; key: FilterCategory; options: string[] }[] = [
+ { title: "Gender", key: "gender", options: ["Female", "Male", "Unknown"] },
+ { title: "Species", key: "species", options: ["Human", "Alien", "Unknown"] },
+ { title: "Status", key: "status", options: ["Alive", "Dead", "Unknown"] },
];
export default function CharactersPage() {
-
- const [characters, setCharacters] = useState
([]);
- const [selectedCharacter, setSelectedCharacter] = useState(null);
- const [searchQuery, setsearchQuery] = useState("");
- const [selectedFilters, setSelectedFilters] = useState>(() => {
- const storedFilters = sessionStorage.getItem("selectedFilters");
-
- if (storedFilters) {
- return JSON.parse(storedFilters);
- }
-
- return {
- gender: [],
- species: [],
- status: []
- };
- });
- const [favoriteCharacterIds, setFavoriteCharacterIds] = useState(() => {
- const storedFavorites = localStorage.getItem("favoriteCharacterIds");
-
- if (storedFavorites) {
- return JSON.parse(storedFavorites);
- }
-
- return [];
- });
-
- useEffect(() => {
- sessionStorage.setItem(
- "selectedFilters",
- JSON.stringify(selectedFilters)
- );
- }, [selectedFilters]);
-
- useEffect(() => {
- localStorage.setItem(
- "favoriteCharacterIds",
- JSON.stringify(favoriteCharacterIds)
- );
- }, [favoriteCharacterIds]);
-
- useEffect(() => {
- async function fetchCharacters() {
- try {
- const response = await fetch("https://rickandmortyapi.com/api/character");
-
- if (!response.ok) {
- throw new Error(`Could not fetch characters: ${response.status}`)
- }
-
- const data = await response.json();
- setCharacters(data.results);
- }
- catch (error) {
- console.error(error);
- }
- }
- fetchCharacters()
- }, [])
-
- const filteredCharacters = characters.filter((character) => {
- const matchesSearch = character.name.toLowerCase().includes(searchQuery.toLowerCase());
- const matchesFilters = filters.every((filter) => {
- const selectedOptions = selectedFilters[filter.key];
-
- if (selectedOptions.length === 0) {
- return true;
- }
-
- return selectedOptions.includes(character[filter.key].toLowerCase());
- });
-
- return matchesSearch && matchesFilters;
- });
-
- function handleFilterChange(category: FilterCategory, value: string, checked: boolean) {
- setSelectedFilters((previousFilters) => ({
- ...previousFilters,
- [category]: checked
- ? [...previousFilters[category], value]
- : previousFilters[category].filter((selectedValue) => selectedValue !== value)
- }));
+ const [characters, setCharacters] = useState([]);
+ const [selectedCharacter, setSelectedCharacter] = useState(
+ null,
+ );
+ const [searchQuery, setsearchQuery] = useState("");
+ const [selectedFilters, setSelectedFilters] = useState<
+ Record
+ >(() => {
+ const storedFilters = sessionStorage.getItem("selectedFilters");
+
+ if (storedFilters) {
+ return JSON.parse(storedFilters);
}
- function handleToggleFavorite(characterId: number) {
- setFavoriteCharacterIds((previousFavorites) =>
- previousFavorites.includes(characterId)
- ? previousFavorites.filter((id) => id !== characterId)
- : [...previousFavorites, characterId]
- );
- }
-
- function showPreviousCharacter() {
- if (!selectedCharacter) return;
-
- const currentIndex = filteredCharacters.findIndex(
- (character) => character.id === selectedCharacter.id
+ return {
+ gender: [],
+ species: [],
+ status: [],
+ };
+ });
+ const [favoriteCharacterIds, setFavoriteCharacterIds] = useState(
+ () => {
+ const storedFavorites = localStorage.getItem("favoriteCharacterIds");
+
+ if (storedFavorites) {
+ return JSON.parse(storedFavorites);
+ }
+
+ return [];
+ },
+ );
+
+ useEffect(() => {
+ sessionStorage.setItem("selectedFilters", JSON.stringify(selectedFilters));
+ }, [selectedFilters]);
+
+ useEffect(() => {
+ localStorage.setItem(
+ "favoriteCharacterIds",
+ JSON.stringify(favoriteCharacterIds),
+ );
+ }, [favoriteCharacterIds]);
+
+ useEffect(() => {
+ async function fetchCharacters() {
+ try {
+ const response = await fetch(
+ "https://rickandmortyapi.com/api/character",
);
- if (currentIndex === -1) return;
-
- const previousIndex =
- currentIndex === 0 ? filteredCharacters.length - 1 : currentIndex - 1;
+ if (!response.ok) {
+ throw new Error(`Could not fetch characters: ${response.status}`);
+ }
- setSelectedCharacter(filteredCharacters[previousIndex]);
+ const data = await response.json();
+ setCharacters(data.results);
+ } catch (error) {
+ console.error(error);
+ }
}
+ fetchCharacters();
+ }, []);
- function showNextCharacter() {
- if (!selectedCharacter) return;
-
- const currentIndex = filteredCharacters.findIndex(
- (character) => character.id === selectedCharacter.id
- );
+ const filteredCharacters = characters.filter((character) => {
+ const matchesSearch = character.name
+ .toLowerCase()
+ .includes(searchQuery.toLowerCase());
+ const matchesFilters = filters.every((filter) => {
+ const selectedOptions = selectedFilters[filter.key];
- if (currentIndex === -1) return;
+ if (selectedOptions.length === 0) {
+ return true;
+ }
- const nextIndex =
- currentIndex === filteredCharacters.length - 1 ? 0 : currentIndex + 1;
-
- setSelectedCharacter(filteredCharacters[nextIndex]);
- }
+ return selectedOptions.includes(character[filter.key].toLowerCase());
+ });
- return (
-
-
-
-
- {filteredCharacters.map((character) => (
- handleToggleFavorite(character.id)}
- onDetails={() => setSelectedCharacter(character)}
- />
- ))}
-
-
-
- {selectedCharacter && (
- setSelectedCharacter(null)}
- onPrevious={showPreviousCharacter}
- onNext={showNextCharacter}
- isFavorite={favoriteCharacterIds.includes(selectedCharacter.id)}
- onToggleFavorite={() => handleToggleFavorite(selectedCharacter.id)}
- />
- )}
-
- )
+ return matchesSearch && matchesFilters;
+ });
+
+ function handleFilterChange(
+ category: FilterCategory,
+ value: string,
+ checked: boolean,
+ ) {
+ setSelectedFilters((previousFilters) => ({
+ ...previousFilters,
+ [category]: checked
+ ? [...previousFilters[category], value]
+ : previousFilters[category].filter(
+ (selectedValue) => selectedValue !== value,
+ ),
+ }));
+ }
+
+ function handleToggleFavorite(characterId: number) {
+ setFavoriteCharacterIds((previousFavorites) =>
+ previousFavorites.includes(characterId)
+ ? previousFavorites.filter((id) => id !== characterId)
+ : [...previousFavorites, characterId],
+ );
+ }
+
+ function showPreviousCharacter() {
+ if (!selectedCharacter) return;
+
+ const currentIndex = filteredCharacters.findIndex(
+ (character) => character.id === selectedCharacter.id,
+ );
+
+ if (currentIndex === -1) return;
+
+ const previousIndex =
+ currentIndex === 0 ? filteredCharacters.length - 1 : currentIndex - 1;
+
+ setSelectedCharacter(filteredCharacters[previousIndex]);
+ }
+
+ function showNextCharacter() {
+ if (!selectedCharacter) return;
+
+ const currentIndex = filteredCharacters.findIndex(
+ (character) => character.id === selectedCharacter.id,
+ );
+
+ if (currentIndex === -1) return;
+
+ const nextIndex =
+ currentIndex === filteredCharacters.length - 1 ? 0 : currentIndex + 1;
+
+ setSelectedCharacter(filteredCharacters[nextIndex]);
+ }
+
+ return (
+
+
+
+
+ {filteredCharacters.map((character) => (
+ handleToggleFavorite(character.id)}
+ onDetails={() => setSelectedCharacter(character)}
+ />
+ ))}
+
+
+
+ {selectedCharacter && (
+ setSelectedCharacter(null)}
+ onPrevious={showPreviousCharacter}
+ onNext={showNextCharacter}
+ isFavorite={favoriteCharacterIds.includes(selectedCharacter.id)}
+ onToggleFavorite={() => handleToggleFavorite(selectedCharacter.id)}
+ />
+ )}
+
+ );
}
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
index b9c2bbb..aafc02e 100644
--- a/src/pages/HomePage.tsx
+++ b/src/pages/HomePage.tsx
@@ -1,11 +1,11 @@
-import Hero from "../components/Hero";
+import Hero from "../components/Hero";
function HomePage() {
- return (
-
-
-
- );
+ return (
+
+
+
+ );
}
-export default HomePage;
\ No newline at end of file
+export default HomePage;
diff --git a/src/types/Character.ts b/src/types/Character.ts
index 39eb0ea..91ea5a4 100644
--- a/src/types/Character.ts
+++ b/src/types/Character.ts
@@ -7,7 +7,7 @@ export type Character = {
origin: {
name: string;
url: string;
- }
+ };
image: string;
episode: string[];
-}
\ No newline at end of file
+};
diff --git a/vite.config.ts b/vite.config.ts
index d789a7c..f2ac363 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,11 +1,8 @@
-import react, { reactCompilerPreset } from '@vitejs/plugin-react'
-import babel from '@rolldown/plugin-babel'
-import { defineConfig } from 'vite'
+import react, { reactCompilerPreset } from "@vitejs/plugin-react";
+import babel from "@rolldown/plugin-babel";
+import { defineConfig } from "vite";
// https://vite.dev/config/
export default defineConfig({
- plugins: [
- react(),
- babel({ presets: [reactCompilerPreset()] })
- ],
-})
+ plugins: [react(), babel({ presets: [reactCompilerPreset()] })],
+});