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
48 changes: 24 additions & 24 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
import { useEffect, useState } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { Navbar } from './components/navbar/Navbar';
import { SkipLink } from './components/skipLink/SkipLink';
import { ProtectedRoute } from './components/ProtectedRoute';
import { AuthProvider } from './components/auth/AuthProvider';
import { useEffect, useState } from 'react'
import { Navigate, Route, Routes } from 'react-router-dom'
import { Navbar } from './components/navbar/Navbar'
import { SkipLink } from './components/skipLink/SkipLink'
import { ProtectedRoute } from './components/ProtectedRoute'
import { AuthProvider } from './components/auth/AuthProvider'

// One page for each route (src/pages/). The parts of the pages are in src/components/.
import AboutPage from '@/pages/AboutPage';
import CalculatorPage from '@/pages/CalculatorPage';
import ElementDetailPage from '@/pages/ElementDetailPage';
import ElementsPage from '@/pages/ElementsPage';
import LoginPage from '@/pages/LoginPage';
import NotFoundPage from '@/pages/NotFoundPage';
import YourGymnastsPage from '@/pages/YourGymnastsPage';
import AboutPage from '@/pages/AboutPage'
import CalculatorPage from '@/pages/CalculatorPage'
import ElementDetailPage from '@/pages/ElementDetailPage'
import ElementsPage from '@/pages/ElementsPage'
import LoginPage from '@/pages/LoginPage'
import NotFoundPage from '@/pages/NotFoundPage'
import YourGymnastsPage from '@/pages/YourGymnastsPage'

import './index.css';
import './styles/variables.css';
import './styles/global.css';
import './index.css'
import './styles/variables.css'
import './styles/global.css'

export function App() {
const [isDarkMode, setIsDarkMode] = useState(
() => window.localStorage.getItem('theme') === 'dark',
);
)

useEffect(() => {
document.documentElement.dataset.theme = isDarkMode ? 'dark' : 'light';
window.localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
document.documentElement.dataset.theme = isDarkMode ? 'dark' : 'light'
window.localStorage.setItem('theme', isDarkMode ? 'dark' : 'light')

return () => {
delete document.documentElement.dataset.theme;
};
}, [isDarkMode]);
delete document.documentElement.dataset.theme
}
}, [isDarkMode])

return (
<AuthProvider>
Expand Down Expand Up @@ -59,7 +59,7 @@ export function App() {
</Routes>
</div>
</AuthProvider>
);
)
}

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

interface ProtectedRouteProps {
/** Where an anonymous visitor is sent. The page they wanted is kept, so the login can send them back. */
redirectPath?: string;
redirectPath?: string
}

/**
Expand All @@ -16,21 +16,21 @@ interface ProtectedRouteProps {
export const ProtectedRoute = ({
redirectPath = '/login',
}: ProtectedRouteProps) => {
const { status } = useAuth();
const location = useLocation();
const { status } = useAuth()
const location = useLocation()

if (status === 'loading') {
return (
<main className="px-5 py-8 text-left sm:px-8 lg:px-12">
<PageTitle>Checking your login</PageTitle>
<p role="status">Checking your login…</p>
</main>
);
)
}

if (status === 'anonymous') {
return <Navigate to={redirectPath} replace state={{ from: location }} />;
return <Navigate to={redirectPath} replace state={{ from: location }} />
}

return <Outlet />;
};
return <Outlet />
}
44 changes: 22 additions & 22 deletions src/components/gymnasts/DevLoginButton.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useState, type FormEvent } from "react";
import { Button } from "@/components/ui/button";
import { PILL } from "@/components/ui/pill";
import { devLogin } from "@/lib/apollo/client";
import { describeError } from "@/lib/apollo/errors";
import { useState, type FormEvent } from 'react'
import { Button } from '@/components/ui/button'
import { PILL } from '@/components/ui/pill'
import { devLogin } from '@/lib/apollo/client'
import { describeError } from '@/lib/apollo/errors'

/**
* Logs in as the coach with the e-mail address that the person types, without
Expand All @@ -14,24 +14,24 @@ import { describeError } from "@/lib/apollo/errors";
export default function DevLoginButton({
onLoggedIn,
}: {
onLoggedIn: () => Promise<unknown>;
onLoggedIn: () => Promise<unknown>
}) {
const [email, setEmail] = useState("");
const [isLoggingIn, setIsLoggingIn] = useState(false);
const [error, setError] = useState("");
const [email, setEmail] = useState('')
const [isLoggingIn, setIsLoggingIn] = useState(false)
const [error, setError] = useState('')

async function logIn(event: FormEvent) {
event.preventDefault();
if (isLoggingIn) return;
setIsLoggingIn(true);
setError("");
event.preventDefault()
if (isLoggingIn) return
setIsLoggingIn(true)
setError('')
try {
await devLogin(email);
await onLoggedIn();
await devLogin(email)
await onLoggedIn()
} catch (problem) {
setError(describeError(problem).message);
setError(describeError(problem).message)
} finally {
setIsLoggingIn(false);
setIsLoggingIn(false)
}
}

Expand All @@ -58,11 +58,11 @@ export default function DevLoginButton({
required
value={email}
onChange={(event) => {
setEmail(event.target.value);
setError("");
setEmail(event.target.value)
setError('')
}}
aria-invalid={Boolean(error)}
aria-describedby={error ? "dev-login-error" : undefined}
aria-describedby={error ? 'dev-login-error' : undefined}
className="h-11 w-full rounded-md border border-(--field-border) bg-(--field-bg) px-3 text-(--text-color) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--text-color)"
/>
<Button
Expand All @@ -71,7 +71,7 @@ export default function DevLoginButton({
className={PILL}
disabled={isLoggingIn}
>
{isLoggingIn ? "Logging in…" : "Log in as test coach"}
{isLoggingIn ? 'Logging in…' : 'Log in as test coach'}
</Button>
{error && (
<p
Expand All @@ -83,5 +83,5 @@ export default function DevLoginButton({
</p>
)}
</form>
);
)
}
19 changes: 8 additions & 11 deletions src/components/gymnasts/GymnastList.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import GymnastRow from "@/components/gymnasts/GymnastRow";
import type { GymnastSummary } from "@/lib/gymnasts";
import GymnastRow from '@/components/gymnasts/GymnastRow'
import type { GymnastSummary } from '@/lib/gymnasts'

export default function GymnastList({
gymnasts,
onDeleted,
onFailed,
}: {
gymnasts: GymnastSummary[];
onDeleted: (name: string) => void;
onFailed: (message: string) => void;
gymnasts: GymnastSummary[]
onDeleted: (name: string) => void
onFailed: (message: string) => void
}) {
return (
<section
aria-labelledby="gymnast-list-heading"
className="mt-20 sm:mt-24"
>
<section aria-labelledby="gymnast-list-heading" className="mt-20 sm:mt-24">
<h2 id="gymnast-list-heading" className="sr-only">
Gymnast list
</h2>
Expand All @@ -35,5 +32,5 @@ export default function GymnastList({
</ul>
)}
</section>
);
}
)
}
47 changes: 24 additions & 23 deletions src/components/navbar/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
import React, { useState } from 'react';
import { Menu, X } from 'lucide-react';
import { Link, NavLink, useNavigate } from 'react-router-dom';
import { useAuth } from '@/components/auth/useAuth';
import { logout } from '@/lib/apollo/client';
import React, { useState } from 'react'
import { Menu, X } from 'lucide-react'
import { Link, NavLink, useNavigate } from 'react-router-dom'
import { useAuth } from '@/components/auth/useAuth'
import { logout } from '@/lib/apollo/client'

interface NavbarProps {
isDarkMode: boolean;
onToggleDarkMode: () => void;
isDarkMode: boolean
onToggleDarkMode: () => void
}

export const Navbar: React.FC<NavbarProps> = ({
isDarkMode,
onToggleDarkMode,
}) => {
const [menuOpen, setMenuOpen] = useState(false);
const [menuOpen, setMenuOpen] = useState(false)
// The same answer as the pages read: who is logged in, as the server says it.
const { status } = useAuth();
const isLoggedIn = status === 'authenticated';
const navigate = useNavigate();
const { status } = useAuth()
const isLoggedIn = status === 'authenticated'
const navigate = useNavigate()

async function logOut() {
setMenuOpen(false);
setMenuOpen(false)
// Leave a page that is for coaches first, so the visitor is not sent to the
// login page by the page they are leaving.
void navigate('/elements');
await logout();
void navigate('/elements')
await logout()
}
const focusRing =
'focus-visible:outline-2 focus-visible:outline-(--text-color) focus-visible:outline-offset-4';
'focus-visible:outline-2 focus-visible:outline-(--text-color) focus-visible:outline-offset-4'
const authButton =
'inline-flex min-h-6 min-w-6 shrink items-center whitespace-nowrap rounded-corner border border-border-color bg-transparent px-[clamp(11px,12px,13px)] py-xs font-text text-[clamp(var(--font-size-row-title),1.2vw,1rem)] text-text-color';
'inline-flex min-h-6 min-w-6 shrink items-center whitespace-nowrap rounded-corner border border-border-color bg-transparent px-[clamp(11px,12px,13px)] py-xs font-text text-[clamp(var(--font-size-row-title),1.2vw,1rem)] text-text-color'
const navLinks = [
{ path: '/elements', label: 'Search' },
{ path: '/calculator', label: 'Calculator' },
{ path: '/about', label: 'About' },
...(isLoggedIn ? [{ path: '/gymnasts', label: 'Gymnasts' }] : []),
];
]

return (
<header className="relative box-border grid w-full grid-cols-[minmax(0,1fr)_auto] items-center gap-x-md border-b border-border-color bg-primary-color px-[clamp(0.5rem,2vw,2.5rem)] py-md min-[720px]:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]">
Expand Down Expand Up @@ -84,9 +84,7 @@ export const Navbar: React.FC<NavbarProps> = ({
aria-hidden="true"
className="flex h-[20px] w-[20px] items-center justify-center rounded-full bg-primary-color text-xs transition-transform duration-200 ease-in-out"
style={{
transform: isDarkMode
? 'translateX(22px)'
: 'translateX(0)',
transform: isDarkMode ? 'translateX(22px)' : 'translateX(0)',
}}
>
{isDarkMode ? '🌙' : '☀️'}
Expand All @@ -104,7 +102,10 @@ export const Navbar: React.FC<NavbarProps> = ({
Log out
</button>
) : (
<Link to="/login" className={`${authButton} no-underline ${focusRing}`}>
<Link
to="/login"
className={`${authButton} no-underline ${focusRing}`}
>
Log in
</Link>
))}
Expand All @@ -125,5 +126,5 @@ export const Navbar: React.FC<NavbarProps> = ({
</button>
</div>
</header>
);
};
)
}
6 changes: 3 additions & 3 deletions src/components/skipLink/SkipLink.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
interface SkipLinkProps {
mainId?: string;
mainId?: string
}

export const SkipLink = ({ mainId = 'main-content' }: SkipLinkProps) => {
Expand All @@ -10,5 +10,5 @@ export const SkipLink = ({ mainId = 'main-content' }: SkipLinkProps) => {
>
Skip to main content
</a>
);
};
)
}
2 changes: 1 addition & 1 deletion src/lib/devLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
* build has no development login (see "Login for development" in docs/API.md).
*/
export const isDevLoginEnabled = (): boolean =>
import.meta.env.VITE_DEV_LOGIN === "true";
import.meta.env.VITE_DEV_LOGIN === 'true'
Loading