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
2,849 changes: 2,316 additions & 533 deletions package-lock.json

Large diffs are not rendered by default.

17 changes: 14 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
"db:migrate": "npm run prisma:migrate -w backend",
"db:deploy": "npm run prisma:deploy -w backend",
"db:seed": "npm run prisma:seed -w backend",
"db:studio": "npm run prisma:studio -w backend"
"db:studio": "npm run prisma:studio -w backend",
"test:run": "vitest run",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
},
"dependencies": {
"@apollo/server": "^5.5.1",
Expand All @@ -30,33 +33,41 @@
"lucide-react": "^1.58.0",
"pg": "^8.23.1",
"react": "^19.2.8",
"react-dom": "^19.2.8"
"react-dom": "^19.2.8",
"react-router-dom": "^7.18.4"
},
"devDependencies": {
"@babel/core": "^7.29.7",
"@eslint/js": "^10.0.1",
"@rolldown/plugin-babel": "^0.2.3",
"@tailwindcss/vite": "^4.3.3",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.3",
"@types/babel__core": "^7.20.5",
"@types/cors": "^2.8.19",
"@types/jest": "^30.0.0",
"@types/node": "^24.19.2",
"@types/pg": "^8.23.1",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.7",
"@types/testing-library__react": "^10.0.1",
"@vitejs/plugin-react": "^6.1.1",
"autoprefixer": "^10.6.1",
"babel-plugin-react-compiler": "^1.0.0",
"eslint": "^10.10.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.6",
"globals": "^17.12.0",
"jsdom": "^29.1.1",
"postcss": "^8.5.31",
"prettier": "^3.9.9",
"prisma": "^7.10.1",
"shadcn": "^1.0.0",
"tailwindcss": "^4.3.3",
"tsx": "^4.23.15",
"tw-animate-css": "^1.4.0",
"typescript": "~6.0.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.69.0",
"vite": "^8.3.0",
"vitest": "^5.0.2"
Expand Down
2 changes: 2 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import "tailwindcss";

.counter {
font-size: 16px;
padding: 5px 10px;
Expand Down
39 changes: 29 additions & 10 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
import './App.css'
import React, { useState } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { SkipLink } from './components/skipLink/SkipLink';
import { Navbar } from './components/navbar/Navbar';
import { ProtectedRoute } from './components/ProtectedRoute';

function App() {
const iconsUrl = `${import.meta.env.BASE_URL}icons.svg`
// Placeholder or imported pages
const SearchPage = () => <main style={{ padding: '2rem' }}><h1>Elements</h1></main>;
const CalculatorPage = () => <main style={{ padding: '2rem' }}><h1>Routine Calculator</h1></main>;
const GymnastsPage = () => <main style={{ padding: '2rem' }}><h1>Gymnasts</h1></main>;
const AboutPage = () => <main style={{ padding: '2rem' }}><h1>About</h1></main>;
const NotFoundPage = () => <main style={{ padding: '2rem' }}><h1>404 - Side ikke funnet</h1></main>;

export function App() {
// Replace with your actual auth state (e.g., Context, Redux, or Zustand)
const [isLoggedIn, setIsLoggedIn] = useState(false);

return (
<main>
<svg className="icon" role="presentation" aria-hidden="true">
<use href={`${iconsUrl}#documentation-icon`}></use>
</svg>
</main>
)
<BrowserRouter>
{/* 1. SkipLink has to be first in the DOM */}
<SkipLink mainId="main-content" />
<Navbar isLoggedIn={isLoggedIn} setIsLoggedIn={setIsLoggedIn} />
<main id="main-content" tabIndex={-1} style={{ outline: 'none' }}>
<Routes>
<Route path="/" element={<SearchPage />} />
<Route path="/calculator" element={<CalculatorPage />} />
<Route path="/about" element={<AboutPage />} />
</Routes>
</main>
</BrowserRouter>
);
}

export default App
export default App;
18 changes: 18 additions & 0 deletions src/components/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';

interface ProtectedRouteProps {
isLoggedIn: boolean;
redirectPath?: string;
}

export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
isLoggedIn,
redirectPath = '/',
}) => {
if (!isLoggedIn) {
return <Navigate to={redirectPath} replace />;
}

return <Outlet />;
};
138 changes: 138 additions & 0 deletions src/components/navbar/Navbar.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/* 1. Outer Header distributes space between Left, Center, and Right groups */
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-md) var(--space-xl);
background: var(--primary-color);
border-bottom: 1px solid var(--border-color);
box-sizing: border-box;
width: 100%;
}

/* 2. Center Links: Use justify-content: center and fixed gap */
.navLinks {
display: flex;
align-items: center;
justify-content: center; /* Keeps links neatly centered as a cluster */
gap: var(--space-lg); /* Controlled spacing between links */
}

/* 3. Left Logo */
.logo {
font-family: var(--font-family-title);
font-size: clamp(1.2rem, 2vw, 1.6rem);
font-weight: bold;
color: var(--text-color);
text-decoration: none;
white-space: nowrap;
}

/* 4. Right Controls Group */
.controls {
display: flex;
align-items: center;
gap: var(--space-md);
flex-shrink: 0;
}

/* Typography & Links */
.logo {
font-family: var(--font-family-title);
font-size: clamp(1.2rem, 2vw, 1.6rem);
font-weight: bold;
color: var(--text-color);
text-decoration: none;
white-space: nowrap;
}

.link {
font-family: var(--font-family-text);
color: var(--text-color);
text-decoration: none;
font-weight: 500; /* Default font weight for inactive links */
font-size: 1.1rem;
position: relative;
padding: var(--space-xs) 0;
white-space: nowrap;
transition: color 0.15s ease, font-weight 0.15s ease;
}

/* Hover effect: Subtle underline reveal and weight transition */
.link::after {
content: '';
position: absolute;
bottom: -6px; /* Offsets the underline slightly below the text */
left: 0;
right: 0;
height: 3px; /* Thick bar matching the screenshot */
background: var(--text-color);
border-radius: 2px;
transform: scaleX(0); /* Hidden by default */
transition: transform 0.2s ease-in-out;
}

.link:hover::after {
transform: scaleX(1); /* Underline expands smoothly on hover */
}

/* Active Page State */
.active {
font-weight: 700; /* Bold text for active page */
}

.active::after {
transform: scaleX(1); /* Underline stays visible for the active page */
}

/* Toggle Switch Styling */
.toggle {
width: 48px;
height: 26px;
padding: 2px;
border-radius: var(--corner-radius);
border: 1px solid var(--border-subtle, var(--border-color));
background: var(--element-background, var(--background-color-element));
cursor: pointer;
display: flex;
align-items: center;
box-sizing: border-box;
position: relative;
}

.thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--primary-color);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
transition: transform 0.2s ease-in-out;
}

/* Sliding animation when aria-checked="true" */
.toggle[aria-checked="true"] .thumb {
transform: translateX(22px);
}

.loginBtn {
font-family: var(--font-family-text);
padding: var(--space-xs) var(--space-md);
border-radius: var(--corner-radius);
border: 1px solid var(--border-color);
background: transparent;
color: var(--text-color);
cursor: pointer;
white-space: nowrap;
}

/* UU Keyboard Navigation Focus State */
.logo:focus-visible,
.link:focus-visible,
.toggle:focus-visible,
.loginBtn:focus-visible {
outline: 2px solid var(--text-color);
outline-offset: 4px;
}
58 changes: 58 additions & 0 deletions src/components/navbar/Navbar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom'; // Import jest-dom for additional matchers
import { BrowserRouter } from 'react-router-dom';
import { describe, it, expect, vi } from 'vitest';
import { Navbar } from './Navbar';

// Hjelpefunksjon for å rendre Navbar pakket inn i React Router
const renderNavbar = (isLoggedIn = false, setIsLoggedIn = vi.fn()) => {
return render(
<BrowserRouter>
<Navbar isLoggedIn={isLoggedIn} setIsLoggedIn={setIsLoggedIn} />
</BrowserRouter>
);
};

describe('Navbar', () => {
it('viser logo og offentlige navlenker', () => {
renderNavbar(false);

expect(screen.getByRole('link', { name: /wag cop planner home/i })).toBeInTheDocument();
expect(screen.getByRole('link', { name: /search/i })).toBeInTheDocument();
expect(screen.getByRole('link', { name: /calculator/i })).toBeInTheDocument();
expect(screen.getByRole('link', { name: /about/i })).toBeInTheDocument();
});

it('skjuler "Gymnasts"-lenken når brukeren ikke er innlogget', () => {
renderNavbar(false);

expect(screen.queryByRole('link', { name: /gymnasts/i })).not.toBeInTheDocument();
});

it('viser "Gymnasts"-lenken når brukeren er innlogget', () => {
renderNavbar(true);

expect(screen.getByRole('link', { name: /gymnasts/i })).toBeInTheDocument();
});

it('veksler aria-checked på tema-bryteren ved klikk', () => {
renderNavbar();

const themeToggle = screen.getByRole('switch', { name: /toggle dark theme/i });
expect(themeToggle).toHaveAttribute('aria-checked', 'false');

fireEvent.click(themeToggle);

expect(themeToggle).toHaveAttribute('aria-checked', 'true');
});

it('kaller setIsLoggedIn når innloggingsknappen trykkes', () => {
const setIsLoggedInMock = vi.fn();
renderNavbar(false, setIsLoggedInMock);

const loginButton = screen.getByRole('button', { name: /log in with feide/i });
fireEvent.click(loginButton);

expect(setIsLoggedInMock).toHaveBeenCalledWith(true);
});
});
Loading