Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
coverage/
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ The webpage can be accessed through a virtual machine at:

## Overview

This project collects and displays data from an external REST API for the popular TV show _Rick and Morty_. Users can search for and filter characters using the search bar or filter menu. More detailed information about each character can be viewed by clicking the `Details` button. Each character card also has a star button that allows users to add characters to their favorites.
This project collects and displays data from an external REST API for the popular TV show _Rick and Morty_. To demonstrate the application's functionality and navigation without using an unnecessarily large dataset, only the first page of characters is fetched from the API. Users can search for and filter characters using the search bar or filter menu. More detailed information about each character can be viewed by clicking the `Details` button. Each character card also has a star button that allows users to add characters to their favorites.

The API documentation can be found here:

- https://rickandmortyapi.com/documentation

## Get started

Before running the project, make sure you have `Node.js` installed on your machine. Install the project dependencies with:
Before running the project, make sure you have `Node.js v24.6.X+` and `npm v11.+` installed on your machine. Install the project dependencies with:

```bash
npm install
Expand All @@ -32,7 +32,7 @@ The webpage will then be available at the local URL provided in the terminal (lo

## Tests

The project has a test coverage of 88,73%. To measure and review the current test coverage, run the following command in the terminal:
The project has a test line coverage of 90%. To measure and review the current test coverage, run the following command in the terminal:

```bash
npm test -- --coverage
Expand All @@ -44,6 +44,15 @@ Each component has its own test file covering its most important functionality.
npm test
```

We performed manual testing for responiveness and user experience. This was done in both landscape and portrait orientations on the following devices:

- iPhone 16
- iPhone 17
- iPhone 13
- iPad Air
- Macbook Air
- Macbook Pro

## Developed by

- Emma Fredriksen
Expand Down Expand Up @@ -74,5 +83,5 @@ The user interface is built with our own React components and plain CSS, without
## Use of AI

During development, our use of AI has been limited. Any code written with the assistance of AI is marked with a comment above the relevant code snippet. We chose this approach based on our understanding of AI and our previous experience. AI can be a great accelerator when programming, but using it effectively requires a good understanding of the fundamental principles behind the technology being used. Without this understanding it is hard to evaluate the code produced by AI and make informed decisions. Since we are still developing our understanding of React, TypeScript and the other technologies used in this project, we decided it was better to use AI as a coach rather than a developer. This approach affected our development speed, and we spent more time exploring documentation and other learning resources. However, it also gave us a better understanding of the project as a whole and helped us avoid introducing unnecessary code that we did not fully understand or know how to maintain.
<br>

We also found that establishing a shared standard for documenting AI-assisted code made it easier to be transparent about the use of AI and to review each other's work. Knowing which parts were developed with AI assistance made it easier to discuss the reasoning behind the code and evaluate whether we understood and agreed with the proposed solutions. At the same time, we recognize that we could probably have achieved a higher development speed by using AI more actively, and AI might also have helped us identify more efficient solutions in some areas. However, for this project, we felt that the benefits of prioritizing learning, transparency, and understanding outweighed the potential increase in productivity. We therefore intend to continue using this approach while gradually increasing our use of AI as our own knowledge and ability to evaluate its output improve.
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>project-1</title>
<title>Rick and Morty Character Explorer</title>
</head>
<body>
<div id="root"></div>
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import HeroPage from "./pages/HeroPage";
import CharactersPage from "./pages/CharactersPage";
import Footer from "./components/Footer";

function App() {
return (
<main>
<HeroPage />
<CharactersPage />
<Footer />
</main>
);
}
Expand Down
19 changes: 18 additions & 1 deletion src/components/CharacterDetailCard.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { cleanup, render, screen } from "@testing-library/react";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
Expand Down Expand Up @@ -209,4 +209,21 @@ describe("CharacterDetailCard", () => {

expect(onPrevious).toHaveBeenCalledOnce();
});

it("closes the detail card when 'esc' is pressed", () => {
const onClose = vi.fn();
renderWithQueryClient(
<CharacterDetailCard
character={mockCharacter}
onClose={onClose}
onPrevious={vi.fn()}
onNext={vi.fn()}
isFavorite={false}
onToggleFavorite={vi.fn()}
/>,
);
fireEvent.keyDown(window, { key: "Escape" });

expect(onClose).toHaveBeenCalledTimes(1);
});
});
15 changes: 15 additions & 0 deletions src/components/CharacterDetailCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import UserRound from "../assets/user-round.svg";
import X from "../assets/X.svg";
import Star from "../assets/star.svg";
import StarFilled from "../assets/star-filled.svg";
import { useEffect } from "react";

type CharacterDetailCardProps = {
character: Character;
Expand Down Expand Up @@ -60,6 +61,20 @@ export default function CharacterDetailCard({
queryFn: () => fetchEpisodes(character),
});

// Closes detailcard when "esc" is pressed
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
onClose();
}
};

window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [onClose]);

return (
<div
className="detail-overlay"
Expand Down
12 changes: 12 additions & 0 deletions src/components/Footer.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.footer {
width: 100%;
height: 6rem;
background-color: #161616;
color: white;
font-size: medium;
margin-inline: auto;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
9 changes: 9 additions & 0 deletions src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import "./Footer.css";

export default function Footer() {
return (
<footer className="footer">
©2026 Rick and Morty Character Explorer. All rights reserved.
</footer>
);
}
6 changes: 3 additions & 3 deletions src/pages/CharactersPage.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.content {
min-height: 100vh;
display: flex;
flex-direction: column;
gap: 1.5rem;
Expand Down Expand Up @@ -51,7 +52,7 @@
min-width: 0;
}

.character-cards>* {
.character-cards > * {
justify-self: center;
}

Expand Down Expand Up @@ -192,8 +193,7 @@
}

.character-cards {
grid-template-columns:
repeat(auto-fill, minmax(min(100%, 12rem), 1fr));
grid-template-columns: repeat(auto-fill, minmax(min(100%, 12rem), 1fr));
}

.filter-options,
Expand Down
10 changes: 5 additions & 5 deletions src/pages/CharactersPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,11 +244,11 @@ describe("CharactersPage", () => {

await screen.findByText("Rick Sanchez");

await user.click(
screen.getByRole("checkbox", {
name: "Female",
}),
);
const femaleCheckboxes = screen.getAllByRole("checkbox", {
name: "Female",
});

await user.click(femaleCheckboxes[0]);

expect(screen.getByText("Summer Smith")).toBeInTheDocument();

Expand Down
26 changes: 18 additions & 8 deletions src/pages/CharactersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import {
} from "../utils/filterCharacters";

const filters: { title: string; key: FilterCategory; options: string[] }[] = [
{ title: "Favorite", key: "favorite", options: ["My favorites"] },
{ title: "Favorites", key: "favorite", options: ["My favorites"] },
{ title: "Gender", key: "gender", options: ["Female", "Male", "Unknown"] },
{ title: "Species", key: "species", options: ["Human", "Alien", "Unknown"] },
{ title: "Status", key: "status", options: ["Alive", "Dead", "Unknown"] }
{ title: "Status", key: "status", options: ["Alive", "Dead", "Unknown"] },
];

// AI was used to create the fetchCharacters function
Expand Down Expand Up @@ -48,9 +48,17 @@ export default function CharactersPage() {
const storedFilters = sessionStorage.getItem("selectedFilters");

if (storedFilters) {
return JSON.parse(storedFilters);
try {
return JSON.parse(storedFilters);
} catch {
return {
favorite: [],
gender: [],
species: [],
status: [],
};
}
}

return {
favorite: [],
gender: [],
Expand All @@ -65,7 +73,11 @@ export default function CharactersPage() {
const storedFavorites = localStorage.getItem("favoriteCharacterIds");

if (storedFavorites) {
return JSON.parse(storedFavorites);
try {
return JSON.parse(storedFavorites);
} catch {
return [];
}
}

return [];
Expand Down Expand Up @@ -197,9 +209,7 @@ export default function CharactersPage() {
</summary>
<div className="filter-options">{renderFilterOptions()}</div>
</details>
<aside className="filter-sidebar">
{renderFilterOptions()}
</aside>
<aside className="filter-sidebar">{renderFilterOptions()}</aside>
</div>
{selectedCharacter && (
<CharacterDetailCard
Expand Down
4 changes: 2 additions & 2 deletions src/pages/HeroPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "./HeroPage.css";

function HeroPage() {
return (
<main className="hero">
<section className="hero">
<section className="hero-content">
<div className="hero-text">
<img className="logo" src={Logo} alt="Rick and Morty" />
Expand All @@ -32,7 +32,7 @@ function HeroPage() {
alt="Rick and Morty characters"
/>
</section>
</main>
</section>
);
}

Expand Down