Skip to content
Open
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
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The full project description and specification are in [docs/Specifications for p

## Link to VM

http://it2810-03.idi.ntnu.no/project2
http://it2810-03.idi.ntnu.no/project2/

## Core features

Expand All @@ -20,6 +20,19 @@ http://it2810-03.idi.ntnu.no/project2
- **Profile:** Your preferences and saved plants in one place.
- **Language and theme:** The app is available in English and Norwegian, and has a light and a dark theme.

## Accessibility

The app includes the following accessibility features:

- **Page structure:** Semantic `header`, `nav`, `main` and `article` elements organise the content. Sidebar navigation has an accessible name, and the current page is marked with `aria-current="page"`.
- **Keyboard controls:** Navigation uses links, and actions use native buttons. Sidebar links, pagination, sorting controls and avatar options have explicit focus styles. Plant Match instructions are also shown when their control receives focus.
- **Forms and images:** Search, filters and profile inputs have associated labels. Plant images use the plant name as alternative text, and profile and avatar images also include alternative text.
- **Control states:** The sort menu exposes its expanded state, and sort and avatar choices expose their selected state with ARIA attributes. Pagination announces page changes through an `aria-live="polite"` region.
- **Plant cards:** The inactive side of a flipped card is hidden from assistive technology with `aria-hidden`, and its information button is removed from the keyboard tab order.
- **Reduced motion:** The card flip and My Matches hover transitions are disabled when the user requests reduced motion.

The deployed production version received an Accessibility score of **100 in Lighthouse** in our latest reported run. Automated component tests also check selected accessible names, navigation state and sorting states. This does not establish full WCAG compliance; a complete manual keyboard and screen reader audit has not been verified here.

## Tech stack

| Part | Technology |
Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The sections below describe each part in more detail.

The frontend is built with React and TypeScript (Vite).

- **Routing:** Routes are defined in `AppRoutes.tsx` with React Router, and the app is served under the base path `/project2`.
- **Routing:** Routes are defined in `AppRoutes.tsx` with React Router, and the app is served under the base path `/project2/`. Vite defines this base in `frontend/vite.config.ts`; `BrowserRouter` uses the same value through `import.meta.env.BASE_URL`, including the trailing slash for home links.
- **Reusable logic in hooks:** Logic without UI is kept in hooks. `useDebounce` delays updates while the user is typing, and `useSessionState` stores state in `sessionStorage`.
- **Internationalisation:** Text is stored in translation files for Norwegian and English (i18next).
- **Shared types and helpers:** `Plant` and `Preference` are defined in `types/`, and helper functions such as `isPreferenceMatch` in `utils/`.
Expand Down
Binary file modified frontend/public/plant-placeholder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 15 additions & 9 deletions frontend/src/AppRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { lazy, Suspense } from "react";
import { Route, Routes } from "react-router-dom";
import PlantMatchPage from "./pages/plantMatch/PlantMatchPage.tsx";
import ExplorePlants from "./pages/explorePlants/ExplorePlants.tsx";
import MyMatchesPage from "./pages/myMatches/MyMatchesPage.tsx";
import Profile from "./pages/profile/Profile.tsx";

const ExplorePlants = lazy(
() => import("./pages/explorePlants/ExplorePlants.tsx"),
);
const MyMatchesPage = lazy(() => import("./pages/myMatches/MyMatchesPage.tsx"));
const Profile = lazy(() => import("./pages/profile/Profile.tsx"));

// eslint-disable-next-line react-refresh/only-export-components
export const ROUTES = {
Expand All @@ -14,12 +18,14 @@ export const ROUTES = {

function AppRoutes() {
return (
<Routes>
<Route path={ROUTES.plantMatch} element={<PlantMatchPage />} />
<Route path={ROUTES.explore} element={<ExplorePlants />} />
<Route path={ROUTES.myMatches} element={<MyMatchesPage />} />
<Route path={ROUTES.profile} element={<Profile />} />
</Routes>
<Suspense fallback={null}>
<Routes>
<Route path={ROUTES.plantMatch} element={<PlantMatchPage />} />
<Route path={ROUTES.explore} element={<ExplorePlants />} />
<Route path={ROUTES.myMatches} element={<MyMatchesPage />} />
<Route path={ROUTES.profile} element={<Profile />} />
</Routes>
</Suspense>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,7 @@

border: 2px solid transparent;

transition:
background-color 0.2s ease,
color 0.2s ease,
border-color 0.2s ease,
transform 0.2s ease;
transition: transform 0.2s ease;
}

.plant-preference-chip.is-match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,26 @@ export default function PlantPreferenceChips({
}

return (
<div className="plant-card-preferences">
<div
className="plant-card-preferences"
role="group"
aria-label={t("plantCard.preferences")}
>
{plantPreferences.map((preference) => {
const isMatch = selectedPreferences.includes(preference);
const label = t(`preferences.${preference}`);

return (
<span
key={preference}
className={`plant-preference-chip ${isMatch ? "is-match" : ""}`}
role="group"
aria-label={label}
aria-description={
isMatch ? t("plantCard.matchesPreferences") : undefined
}
>
{t(`preferences.${preference}`)}
{label}
</span>
);
})}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/plantCard/PlantCard.css
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@
justify-content: center;
gap: 14px;
background: var(--dark-green);
color: var(--text-light);
color: var(--text-on-green);
border: none;
border-radius: 999px;
font-family: inherit;
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/sidebar/Sidebar.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@

.sidebar__link[aria-current="page"] {
background: var(--dark-green);
color: var(--text-light);
color: var(--text-on-green);
font-weight: 600;
border-color: transparent;
}
Expand All @@ -68,6 +68,7 @@
.sidebar_title {
font-size: 2rem;
font-weight: 600;
margin-top: 1em;
margin-bottom: 1rem;
text-align: center;
}
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ export default function Sidebar({ isOpen, onCloseSidebar }: SidebarProps) {
const { t } = useTranslation();

return (
<aside className="sidebar" id="sidebar" data-open={isOpen}>
<aside
className="sidebar"
id="sidebar"
data-open={isOpen}
aria-labelledby="sidebar-title"
>
<button
className="close-button"
onClick={onCloseSidebar}
Expand All @@ -23,7 +28,9 @@ export default function Sidebar({ isOpen, onCloseSidebar }: SidebarProps) {
×
</button>

<p className="sidebar_title">{t("sidebar.title")}</p>
<h2 className="sidebar_title" id="sidebar-title">
{t("sidebar.title")}
</h2>
<nav className="sidebar__nav" aria-label="Sidebar navigation">
<ul>
<li>
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/components/sidebarButtons/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import "./SideBarButton.css";

export default function ThemeToggle() {
const { t } = useTranslation();
const [darkMode, setDarkMode] = useState(false);

function changeTheme() {
Expand All @@ -15,7 +17,13 @@ export default function ThemeToggle() {
}

return (
<button className="sidebar-button" onClick={changeTheme}>
<button
className="sidebar-button"
onClick={changeTheme}
aria-label={t(
darkMode ? "sidebar.switchToLightMode" : "sidebar.switchToDarkMode",
)}
>
{darkMode ? "🌞️" : "🌚"}
</button>
);
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@
"soilType": "Soil type"
},
"sidebar": {
"switchToLightMode": "Switch to light mode",
"switchToDarkMode": "Switch to dark mode",
"plantMatch": "Plant Match",
"myMatches": "My Matches",
"explorePlants": "Explore Plants",
"profile": "Profile",
"title": "I <3 Plants"
},
"plantMatchPage": {
"skipPlant": "Skip plant",
"likePlant": "Like plant",
"title": "Find your perfect match",
"howToMatch": "How to match",
"instruction1": "Like the plant? Click ✿ if you do, ✖ if you don't.",
Expand Down Expand Up @@ -76,9 +80,10 @@
"profile": {
"name": "Name Surname",
"description": "Description",
"descriptionLabel": "Profile description",
"preferences": "Preferences",
"editProfile": "Edit profile",
"imageAlt": "Profile image",
"imageAlt": "Profile picture",
"enterName": "Write your name",
"aboutYou": "Write something about you",
"avatarChooser": "Choose your avatar:",
Expand All @@ -93,6 +98,8 @@
"placeholderAbout": "Something about you..."
},
"plantCard": {
"preferences": "Plant preferences",
"matchesPreferences": "Matches your saved preferences",
"information": "Information",
"save": "Save plant",
"back": "Back",
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/i18n/locales/no/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@
"soilType": "Jordtype"
},
"sidebar": {
"switchToLightMode": "Bytt til lys modus",
"switchToDarkMode": "Bytt til mørk modus",
"plantMatch": "Plante-Matching",
"myMatches": "Mine Matches",
"explorePlants": "Utforsk Planter",
"profile": "Profil",
"title": "I <3 Plants"
},
"plantMatchPage": {
"skipPlant": "Hopp over planten",
"likePlant": "Lik planten",
"title": "Finn din perfekte match",
"howToMatch": "Slik matcher du",
"instruction1": "Liker du planten? Klikk ✿ hvis du liker den og ✖ hvis ikke.",
Expand Down Expand Up @@ -76,6 +80,7 @@
"profile": {
"name": "Navn Etternavn",
"description": "Beskrivelse",
"descriptionLabel": "Profilbeskrivelse",
"preferences": "Preferanser",
"editProfile": "Endre profil",
"imageAlt": "Profilbilde",
Expand All @@ -93,6 +98,8 @@
"placeholderAbout": "Noe om deg..."
},
"plantCard": {
"preferences": "Plantepreferanser",
"matchesPreferences": "Matcher dine lagrede preferanser",
"information": "Informasjon",
"save": "Lagre plante",
"back": "Tilbake",
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ body {
--text-light: #ffffff;
--light-background: #ffffff;
--dark-green: #3a6642;
--text-on-green: #ffffff;
--light-green: #e8f0e9;
--dark-pink: #9e3050;
--pink-text: #9e3050;
Expand All @@ -37,6 +38,7 @@ body {
--text-light: #ffffff;
--light-background: #292724;
--dark-green: #8fb996;
--text-on-green: #191816;
--light-green: #2c3a2e;
--dark-pink: #c2476b;
--light-pink: #4a2a35;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const clienten = new ApolloClient({
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ApolloProvider client={clienten}>
<BrowserRouter basename="/project2">
<BrowserRouter basename={import.meta.env.BASE_URL}>
<App />
</BrowserRouter>
</ApolloProvider>
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/pages/explorePlants/ExplorePlants.css
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@

.reset-button:hover {
background-color: var(--dark-green);
color: var(--text-light);
color: var(--text-on-green);
}

.reset-button:focus-within {
Expand Down Expand Up @@ -148,7 +148,7 @@

.pagination button:hover:not(:disabled) {
background-color: var(--dark-green);
color: var(--text-light);
color: var(--text-on-green);
}

.pagination button:focus-visible {
Expand Down Expand Up @@ -182,7 +182,7 @@

.sort-button:hover {
background-color: var(--dark-green);
color: var(--text-light);
color: var(--text-on-green);
}

.sort-button:focus-visible,
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/plantMatch/PlantMatchPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@

.like-button {
background-color: var(--dark-green);
color: var(--text-light);
color: var(--text-on-green);
}

@media (max-width: 767px) {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/pages/plantMatch/PlantMatchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ function PlantMatchPage() {
<section className="plant-match">
<button
className="swipe-button dislike-button"
aria-label={t("plantMatchPage.skipPlant")}
onClick={goToNextPlant}
disabled={!plant}
>
Expand Down Expand Up @@ -122,6 +123,7 @@ function PlantMatchPage() {

<button
className="swipe-button like-button"
aria-label={t("plantMatchPage.likePlant")}
onClick={matchPlant}
disabled={!plant}
>
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/pages/profile/Profile.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@
padding-top: 20px;
}

.profile-description-label {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}

.profile-preferences {
margin-top: 50px;
}
Expand Down
15 changes: 12 additions & 3 deletions frontend/src/pages/profile/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,19 @@ export default function Profile() {
return (
<main className="profile">
<div className="profile-info">
<img className="profile-img" src={currentAvatar} alt={name} />
<img
className="profile-img"
src={currentAvatar}
alt={`${t("profile.imageAlt")}: ${name}`}
/>
<div className="profile-text">
<h1> {name} </h1>
<p> {description} </p>
<p>
<span className="profile-description-label">
{t("profile.descriptionLabel")}:{" "}
</span>
{description}
</p>
</div>
</div>

Expand Down Expand Up @@ -127,7 +136,7 @@ export default function Profile() {
value={description}
onChange={(event) => setDescription(event.target.value)}
maxLength={100}
placeholder= {t("profile.placeholderAbout")}
placeholder={t("profile.placeholderAbout")}
/>

<h3>{t("profile.avatarChooser")} </h3>
Expand Down
Loading