From 1b73b5d7fa0e58778447fddffe463c32c9529399 Mon Sep 17 00:00:00 2001 From: Mari Zhevagin Date: Sat, 10 Oct 2026 14:11:48 +0200 Subject: [PATCH 1/6] mye forskjelling for innlogingsmuligheter --- client/src/components/layout/Header.tsx | 41 ++++++++++++------- .../components/layout/LoginRequiredDialog.tsx | 40 ++++++++++++++++++ client/src/components/layout/SearchBar.tsx | 4 +- client/src/lib/AuthProvider.tsx | 38 +++++++++++++++++ client/src/lib/authContext.ts | 13 ++++++ client/src/lib/useCurrentUser.ts | 21 +++------- client/src/main.tsx | 12 ++++-- client/src/routes/LoginPage.tsx | 14 +++++-- 8 files changed, 144 insertions(+), 39 deletions(-) create mode 100644 client/src/components/layout/LoginRequiredDialog.tsx create mode 100644 client/src/lib/AuthProvider.tsx create mode 100644 client/src/lib/authContext.ts diff --git a/client/src/components/layout/Header.tsx b/client/src/components/layout/Header.tsx index 032d8cc..37b26f1 100644 --- a/client/src/components/layout/Header.tsx +++ b/client/src/components/layout/Header.tsx @@ -6,6 +6,7 @@ import { Dialog } from '../ui/Dialog'; import NewRecipePage from '../../routes/NewRecipePage'; import { RecipeStepIndicator } from '../new-recipe/RecipeStepIndicator'; import { useCurrentUser } from '../../lib/useCurrentUser'; +import { LoginRequiredDialog } from './LoginRequiredDialog'; // Fokusmarkering ved tastaturnavigasjon const focusRing = @@ -42,6 +43,13 @@ export default function Header() { // Holder styr på hvilket steg i Ny oppskrift brukeren er på const [currentStep, setCurrentStep] = useState(1); + // knapp spm åpner ny oppskift eller innloggings popup når hvis man ikke er logget inn + const newRecipeButton = ( + + ); + return (
@@ -79,22 +87,25 @@ export default function Header() { {/* Ny oppskrift */}
  • - - Ny Oppskrift - - } - title="Ny oppskrift" - headerContent={ - - } - > - + } + > + + + ) : ( + - + )}
  • diff --git a/client/src/components/layout/LoginRequiredDialog.tsx b/client/src/components/layout/LoginRequiredDialog.tsx new file mode 100644 index 0000000..bcd94d9 --- /dev/null +++ b/client/src/components/layout/LoginRequiredDialog.tsx @@ -0,0 +1,40 @@ +import type { ReactNode } from 'react'; +import * as RadixDialog from '@radix-ui/react-dialog'; +import { useNavigate } from 'react-router'; +import { Button } from '../ui/Button'; + +type LoginRequiredDialogProps = { + trigger: ReactNode; + message: string; +}; + +// popup som ber om å logge inn før man kan bruke favoritter eller ny oppskrift +export function LoginRequiredDialog({ + trigger, + message, +}: LoginRequiredDialogProps) { + const navigate = useNavigate(); + + return ( + + {/* knappene som åpner popupen */} + {trigger} + + + {/* popup */} + + Du må logge inn for å bruke dette + {message} +
    + + + + +
    +
    +
    +
    + ); +} \ No newline at end of file diff --git a/client/src/components/layout/SearchBar.tsx b/client/src/components/layout/SearchBar.tsx index c76ef3e..1a19a8f 100644 --- a/client/src/components/layout/SearchBar.tsx +++ b/client/src/components/layout/SearchBar.tsx @@ -1,4 +1,4 @@ -import { useState, type FormEvent } from 'react'; +import { useState, type SubmitEvent } from 'react'; import { useNavigate } from 'react-router'; type SearchBarProps = { @@ -10,7 +10,7 @@ export default function SearchBar({ initialQuery }: SearchBarProps) { const [input, setInput] = useState(initialQuery); const navigate = useNavigate(); - function handleSubmit(event: FormEvent) { + function handleSubmit(event: SubmitEvent) { // Hindrer at skjemaet laster siden på nytt event.preventDefault(); diff --git a/client/src/lib/AuthProvider.tsx b/client/src/lib/AuthProvider.tsx new file mode 100644 index 0000000..3f83706 --- /dev/null +++ b/client/src/lib/AuthProvider.tsx @@ -0,0 +1,38 @@ +import { useState, type ReactNode } from 'react'; +import type { User } from '../types/user'; +import { AuthContext } from './authContext'; + +// midelertidige bruker til jeg får koblet meg til database +const MOCK_GUEST_USER: User = { id: 'guest', name: 'Gjest', authProvider: 'GUEST' }; +const MOCK_TEST_USER: User = { id: 'test', name: 'Testbruker', authProvider: 'TEST' }; + +// midlertidig innlogging for testbrukeren +const MOCK_TEST_USERNAME = 'testesen'; +const MOCK_TEST_PASSWORD = 'test123'; + +type AuthProviderProps = { children: ReactNode}; + +export function AuthProvider({ children }: AuthProviderProps) { + const [user, setUser] = useState(null); + + const continueAsGuest = () => { setUser(MOCK_GUEST_USER); }; + + const loginAsTestUser = (username: string, password: string) => { + if ( + username === MOCK_TEST_USERNAME && password === MOCK_TEST_PASSWORD ) { + setUser(MOCK_TEST_USER); + return true; + } + return false; + }; + + const logout = () => { + setUser(null); + }; + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/client/src/lib/authContext.ts b/client/src/lib/authContext.ts new file mode 100644 index 0000000..5b64260 --- /dev/null +++ b/client/src/lib/authContext.ts @@ -0,0 +1,13 @@ +import { createContext } from 'react'; +import type { User } from '../types/user'; + +// hentes med useCurrentUser +export type AuthContextValue = { + user: User | null; + loading: boolean; + continueAsGuest: () => void; + loginAsTestUser: (username: string, password: string) => boolean; + logout: () => void; +}; +// felles boks for innlogget bruker +export const AuthContext = createContext(null); diff --git a/client/src/lib/useCurrentUser.ts b/client/src/lib/useCurrentUser.ts index b10dd7e..e5c2a1f 100644 --- a/client/src/lib/useCurrentUser.ts +++ b/client/src/lib/useCurrentUser.ts @@ -1,19 +1,10 @@ -import type { User } from '../types/user'; +import { useContext } from 'react'; +import { AuthContext } from './authContext'; -// Midlertidig bruker til backend er klar. Sett til null for å teste som utlogget. -const MOCK_USER: User | null = null; - -/* const MOCK_USER: User | null = { - id: '1', - name: 'testesen', - authProvider: 'TEST', -}; */ - -// Hvem som er innlogget +// Hvem som er logget inn og funksjonen for å logge inn og ut export function useCurrentUser() { - return { - user: MOCK_USER, - loading: false, - }; + const auth = useContext(AuthContext); + if (auth === null) throw new Error('Fant ikke AuthProvider'); + return auth; } diff --git a/client/src/main.tsx b/client/src/main.tsx index 2eb4526..d23e3d1 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client'; import { BrowserRouter } from 'react-router'; import { ApolloProvider } from '@apollo/client/react'; import { GoogleOAuthProvider } from '@react-oauth/google'; +import { AuthProvider } from './lib/AuthProvider.tsx'; import './index.css'; import client from './lib/apolloClient.ts'; import App from './App.tsx'; @@ -16,10 +17,13 @@ createRoot(document.getElementById('root')!).render( > {/* Gir tilgang til Apollo klienten */} - {/* gjør at basename matcher base i vite config */} - - - + {/* holder styr på hvem som er innlogget */} + + {/* gjør at basename matcher base i vite config */} + + + + , diff --git a/client/src/routes/LoginPage.tsx b/client/src/routes/LoginPage.tsx index 30299f9..b871510 100644 --- a/client/src/routes/LoginPage.tsx +++ b/client/src/routes/LoginPage.tsx @@ -1,4 +1,5 @@ import { useNavigate } from 'react-router'; +import { useCurrentUser } from '../lib/useCurrentUser'; import { Button } from '../components/ui/Button'; import { GoogleLogin, type CredentialResponse } from '@react-oauth/google'; import { useEffect, useRef, useState } from 'react'; @@ -6,12 +7,19 @@ import { useEffect, useRef, useState } from 'react'; // Innloggingsside export default function LoginPage() { const navigate = useNavigate(); + const { continueAsGuest } = useCurrentUser(); //Midlertidig. token skal sendes til backend senere function handleGoogleSuccess(response: CredentialResponse) { console.log('Google-token:', response.credential); } + // logger inn som gjest og går til forside + function handleGuest() { + continueAsGuest(); + navigate('/'); + } + // for å måle bredden rundt google kanpp const googleBoxRef = useRef(null); const [googleButtonWidth, setGoogleButtonWidth] = useState(400); @@ -51,7 +59,7 @@ export default function LoginPage() { />
  • - Lagre favorittene dine og del egne oppskrifter. + Lagre favorittene dine og del egne oppskrifter på tvers av enheter.

    @@ -66,11 +74,11 @@ export default function LoginPage() { -

    Se oppskrifter uten å logge inn.

    +

    Kom i gang uten en konto.

    From e4836e61d9e57ddd7708c018ddc5811ff4099f9b Mon Sep 17 00:00:00 2001 From: Mari Zhevagin Date: Sat, 10 Oct 2026 14:17:53 +0200 Subject: [PATCH 2/6] =?UTF-8?q?popup=20ved=20ny=20oppskrift=20forvinner=20?= =?UTF-8?q?n=C3=A5=20n=C3=A5r=20man=20kommer=20til=20innloggingsside?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/layout/LoginRequiredDialog.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/client/src/components/layout/LoginRequiredDialog.tsx b/client/src/components/layout/LoginRequiredDialog.tsx index bcd94d9..4069dba 100644 --- a/client/src/components/layout/LoginRequiredDialog.tsx +++ b/client/src/components/layout/LoginRequiredDialog.tsx @@ -23,18 +23,25 @@ export function LoginRequiredDialog({ {/* popup */} - Du må logge inn for å bruke dette - {message} + + Du må logge inn for å bruke dette + + + {message} +
    - + {/* lukker popup og går til innloggingsside */} + + +
    ); -} \ No newline at end of file +} From 21c43f7deac278502425fa0d4d99da652f0f99ba Mon Sep 17 00:00:00 2001 From: Mari Zhevagin Date: Sat, 10 Oct 2026 15:35:15 +0200 Subject: [PATCH 3/6] sjekker brukernavn og passord for testbruker --- client/src/routes/TestUserLoginPage.tsx | 26 +++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/client/src/routes/TestUserLoginPage.tsx b/client/src/routes/TestUserLoginPage.tsx index ff341e1..bb70f07 100644 --- a/client/src/routes/TestUserLoginPage.tsx +++ b/client/src/routes/TestUserLoginPage.tsx @@ -1,10 +1,11 @@ import { useState, type SubmitEvent } from 'react'; -import { Link } from 'react-router'; +import { Link, useNavigate } from 'react-router'; import VisibilityIcon from '@mui/icons-material/Visibility'; import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; import { Button } from '../components/ui/Button'; import { Field } from '../components/ui/Field'; import { Input } from '../components/ui/Input'; +import { useCurrentUser } from '../lib/useCurrentUser'; // Innlogging for testbrukeren export default function TestUserLoginPage() { @@ -12,13 +13,21 @@ export default function TestUserLoginPage() { const [password, setPassword] = useState(''); // Om passordet vises som tekst eller prikker const [showPassword, setShowPassword] = useState(false); + const navigate = useNavigate(); + const { loginAsTestUser } = useCurrentUser(); + // feilmelding hvis brukernavn eller passord er feil + const [error, setError] = useState(''); function handleSubmit(event: SubmitEvent) { - // Hindrer at siden lastes på nytt + // hindrer at siden lastes på nytt event.preventDefault(); - // Midlertidig: kobles til backend senere - console.log('testbruker', username); + // sjekker brukernavn og passord + if (loginAsTestUser(username, password)) { + navigate('/'); + } else { + setError('Feil brukernavn eller passord.'); + } } return ( @@ -52,7 +61,8 @@ export default function TestUserLoginPage() { className="pr-12" required /> - {/* Øyeknapp som viser eller skjuler passordet */} + + {/* knapp som viser eller skjuler passordet */}
    - + {error && ( +

    + {error} +

    + )} From 303f4f2676e20853a4303aa3b2f18e4961069048 Mon Sep 17 00:00:00 2001 From: Mari Zhevagin Date: Tue, 13 Oct 2026 16:34:21 +0200 Subject: [PATCH 4/6] min side, registrering, innlogging --- client/.env.example | 6 +- client/src/components/layout/Header.tsx | 35 +++- .../components/profile/PreferencesForm.tsx | 121 +++++++++++ client/src/lib/AuthProvider.tsx | 2 +- client/src/routes/LoginPage.tsx | 71 ++++--- client/src/routes/ProfilePage.tsx | 193 +++++++++++++++++- client/src/routes/TestUserLoginPage.tsx | 8 +- 7 files changed, 387 insertions(+), 49 deletions(-) create mode 100644 client/src/components/profile/PreferencesForm.tsx diff --git a/client/.env.example b/client/.env.example index e7c8e53..2f3dbb3 100644 --- a/client/.env.example +++ b/client/.env.example @@ -1,5 +1,5 @@ # Adressen til Graphql backend. Kopier dette til .env.local VITE_GRAPHQL_URL=/project2/graphql -# Client ID fra Google Cloud -# jeg trenger en google bruker fra dere hvis dere ønsker å teste google innlogging -VITE_GOOGLE_CLIENT_ID=1070343515871-rvhhskd48nla3u2p3bsp7b1sl2st385m.apps.googleusercontent.com \ No newline at end of file + +# Google innlogging er satt på vent til vi ser om vi får HTTPS +# VITE_GOOGLE_CLIENT_ID=1070343515871-rvhhskd48nla3u2p3bsp7b1sl2st385m.apps.googleusercontent.com \ No newline at end of file diff --git a/client/src/components/layout/Header.tsx b/client/src/components/layout/Header.tsx index 37b26f1..1e575dc 100644 --- a/client/src/components/layout/Header.tsx +++ b/client/src/components/layout/Header.tsx @@ -34,22 +34,32 @@ function userClass({ isActive }: { isActive: boolean }) { } export default function Header() { - //hvem som er logget inn + // hvem som er logget inn const { user } = useCurrentUser(); // Søkeordet fra URL-en const [searchParams] = useSearchParams(); const query = searchParams.get('q') ?? ''; - // Holder styr på hvilket steg i Ny oppskrift brukeren er på + // holder styr på hvilket steg i Ny oppskrift brukeren er på const [currentStep, setCurrentStep] = useState(1); - // knapp spm åpner ny oppskift eller innloggings popup når hvis man ikke er logget inn + // knapp som åpner ny oppskrift eller innloggingspopup hvis man ikke er logget inn const newRecipeButton = ( ); + // åpner innloggings popupen + const favoritesButton = ( + + ); + return (
    @@ -73,10 +83,18 @@ export default function Header() { + {/* favoritter går gjennom når innlogget popup hvis ikke */}
  • - - Favoritter - + {user ? ( + + Favoritter + + ) : ( + + )}
  • @@ -85,7 +103,7 @@ export default function Header() {
  • - {/* Ny oppskrift */} + {/* ny oppskrift */}
  • {user ? ( )}
  • +
  • {user ? 'Bruker' : 'Logg inn'} @@ -117,4 +136,4 @@ export default function Header() {
  • ); -} +} \ No newline at end of file diff --git a/client/src/components/profile/PreferencesForm.tsx b/client/src/components/profile/PreferencesForm.tsx new file mode 100644 index 0000000..84ad35b --- /dev/null +++ b/client/src/components/profile/PreferencesForm.tsx @@ -0,0 +1,121 @@ +import { useState } from 'react'; + +// allergiene brukeren kan velge +const ALLERGIES = ['Gluten', 'Laktose', 'Nøtter', 'Egg', 'Skalldyr', 'Soya']; + +type Option = { value: string; label: string }; + +type RadioGroupProps = { + legend: string; + name: string; + value: string; + options: Option[]; + onChange: (value: string) => void; +}; + +// valgknapper der bare ett valg kan være aktivt +function RadioGroup({ legend, name, value, options, onChange }: RadioGroupProps) { + return ( +
    + {legend} +
    + {options.map((option) => ( + + ))} +
    +
    + ); +} + +// språk, måleenheter og allergier +export function PreferencesForm() { + const [language, setLanguage] = useState('nb'); + const [units, setUnits] = useState('metric'); + const [allergies, setAllergies] = useState([]); + + // legger til allergien hvis den ikke er valgt + function toggleAllergy(name: string) { + if (allergies.includes(name)) { + setAllergies(allergies.filter((allergy) => allergy !== name)); + } else { + setAllergies([...allergies, name]); + } + } + + return ( +
    +
    +

    + Preferanser +

    +

    + Brukes til å tilpasse oppskriftene til deg. +

    +
    + + + + + +
    + Allergier +

    Oppskrifter med disse blir markert.

    +
    + {ALLERGIES.map((name) => { + const selected = allergies.includes(name); + return ( + + ); + })} +
    +
    +
    + ); +} \ No newline at end of file diff --git a/client/src/lib/AuthProvider.tsx b/client/src/lib/AuthProvider.tsx index 3f83706..1ba04dc 100644 --- a/client/src/lib/AuthProvider.tsx +++ b/client/src/lib/AuthProvider.tsx @@ -7,7 +7,7 @@ const MOCK_GUEST_USER: User = { id: 'guest', name: 'Gjest', authProvider: 'GUEST const MOCK_TEST_USER: User = { id: 'test', name: 'Testbruker', authProvider: 'TEST' }; // midlertidig innlogging for testbrukeren -const MOCK_TEST_USERNAME = 'testesen'; +const MOCK_TEST_USERNAME = 'test'; const MOCK_TEST_PASSWORD = 'test123'; type AuthProviderProps = { children: ReactNode}; diff --git a/client/src/routes/LoginPage.tsx b/client/src/routes/LoginPage.tsx index b871510..92ba67e 100644 --- a/client/src/routes/LoginPage.tsx +++ b/client/src/routes/LoginPage.tsx @@ -4,12 +4,16 @@ import { Button } from '../components/ui/Button'; import { GoogleLogin, type CredentialResponse } from '@react-oauth/google'; import { useEffect, useRef, useState } from 'react'; +// Google knappen vises bare hvis client ID er satt i .env +// Google innlogging er satt på pause enn så lenge +const hasGoogle = Boolean(import.meta.env.VITE_GOOGLE_CLIENT_ID); + // Innloggingsside export default function LoginPage() { const navigate = useNavigate(); const { continueAsGuest } = useCurrentUser(); - //Midlertidig. token skal sendes til backend senere + // Midlertidig. token skal sendes til backend senere function handleGoogleSuccess(response: CredentialResponse) { console.log('Google-token:', response.credential); } @@ -20,13 +24,14 @@ export default function LoginPage() { navigate('/'); } - // for å måle bredden rundt google kanpp + // for å måle bredden rundt google-knappen const googleBoxRef = useRef(null); const [googleButtonWidth, setGoogleButtonWidth] = useState(400); // måler hver gang den endrer størrelse useEffect(() => { const box = googleBoxRef.current; + // stopper hvis Google-knappen er skjult if (!box) return; const observer = new ResizeObserver(() => { @@ -43,46 +48,48 @@ export default function LoginPage() {

    Velkommen tilbake

    - Logg inn med Google, utforsk oppskriftene som gjest eller testbruker. + {hasGoogle + ? 'Logg inn med Google, utforsk oppskriftene som gjest eller testbruker.' + : 'Utforsk oppskriftene som gjest eller logg inn som testbruker.'}

    - {/* Midlertidig: Google og gjest kobles til backend senere */} -
    -
    - console.log('Google innlogging feilet')} - text="signin_with" - size="large" - width={googleButtonWidth} - shape="rectangular" - /> -
    -

    - Lagre favorittene dine og del egne oppskrifter på tvers av enheter. -

    -
    + {hasGoogle && ( + <> + {/* midlertidig: Google kobles til backend senere */} +
    +
    + console.log('Google innlogging feilet')} + text="signin_with" + size="large" + width={googleButtonWidth} + shape="rectangular" + /> +
    +

    + Lagre favorittene dine og del egne oppskrifter på tvers av enheter. +

    +
    - {/* Skiller Google innlogging fra de andre valgene */} -
    - - eller - -
    + {/* skiller Google innlogging fra de andre valgene */} +
    + + eller + +
    + + )}
    -

    Kom i gang uten en konto.

    - {/* Går til logg inn siden for testbrukeren */} + {/* går til logg inn siden for testbrukeren */}
    ); -} +} \ No newline at end of file diff --git a/client/src/routes/ProfilePage.tsx b/client/src/routes/ProfilePage.tsx index 6caadce..ae3f2f4 100644 --- a/client/src/routes/ProfilePage.tsx +++ b/client/src/routes/ProfilePage.tsx @@ -1,4 +1,191 @@ -// TODO: preferanser og brukerens egne oppskrifter koblet til spsifikke brukere +import { useState } from 'react'; +import { Link, useNavigate } from 'react-router'; +import { Button } from '../components/ui/Button'; +import { PreferencesForm } from '../components/profile/PreferencesForm'; +import { useCurrentUser } from '../lib/useCurrentUser'; + +type Tab = 'preferences' | 'favorites' | 'shared' | 'sharedWithMe'; +type RecipeTab = Exclude; + +// tekstene for hver oppskriftsfane (tomme til backend er koblet på) +const RECIPE_TABS: Record = { + favorites: { label: 'Favoritter', empty: 'Du har ingen favoritter ennå.' }, + shared: { label: 'Mine oppskrifter', empty: 'Du har ikke laget noen oppskrifter ennå.' }, + sharedWithMe: { label: 'Delt med meg', empty: 'Ingen har delt oppskrifter med deg ennå.' }, +}; + +// pillene i mobilmenyen +const MOBILE_TABS: { value: Tab; label: string }[] = [ + { value: 'preferences', label: 'Preferanser' }, + { value: 'favorites', label: 'Favoritter' }, + { value: 'shared', label: 'Mine oppskrifter' }, + { value: 'sharedWithMe', label: 'Delt med meg' }, +]; + +// rosa fokusring +const FOCUS_RING = + 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pink-light'; + export default function ProfilePage() { - return

    Min side

    -} \ No newline at end of file + const { user, logout } = useCurrentUser(); + const navigate = useNavigate(); + const [activeTab, setActiveTab] = useState('preferences'); + + // RequireAuth sørger for at user finnes + if (!user) return null; + + const isGuest = user.authProvider === 'GUEST'; + + // på desktop vises preferanser alltid til venstre, så høyre side viser favoritter i stedet + const desktopTab: RecipeTab = activeTab === 'preferences' ? 'favorites' : activeTab; + + function handleLogout() { + logout(); + navigate('/'); + } + + return ( +
    +
    + {isGuest ? ( + // gjest enkel topp med bokstav og navn +
    +
    + {user.name.charAt(0)} +
    +
    +

    {user.name}

    +

    Logget inn som gjest

    +
    +
    + ) : ( + // innlogget olivengrønt panel +
    +

    Din oppskriftsbok

    +

    Hei {user.name}

    +

    + Favorittene dine, oppskriftene du har delt og det andre har delt med deg. +

    + + {/* TODO: hent ekte tall fra backend */} +
      + {[ + { count: 0, label: 'mine oppskrifter' }, + { count: 0, label: 'favoritter' }, + { count: 0, label: 'delte' }, + { count: 0, label: 'delt med deg' }, + ].map((stat) => ( +
    • + {stat.count} + {stat.label} +
    • + ))} +
    + +
    + + +
    +
    + )} + + {/* gjest: rosa boks som oppfordrer til å lage bruker */} + {isGuest && ( +
    +

    + Du er logget inn som gjest +

    +

    Ta vare på det du lagrer

    +

    + Det du lagrer nå, finnes bare i denne nettleseren. Lag en bruker for å + beholde det på alle enhetene dine. +

    + {/* TODO: bytt til /register når den siden finnes */} + + +
    + )} + + {/* mobilmeny som kan sveipes */} + + + {/* preferanser på mobil bare når fanen er valgt */} +
    + +
    +
    + + {/* oppskriftsfanene */} +
    +
    + {(Object.keys(RECIPE_TABS) as RecipeTab[]).map((tab) => ( + + ))} +
    + +
    +

    {RECIPE_TABS[desktopTab].empty}

    + {desktopTab === 'favorites' && ( + + Utforsk oppskrifter + + )} +
    +
    +
    + ); +} diff --git a/client/src/routes/TestUserLoginPage.tsx b/client/src/routes/TestUserLoginPage.tsx index bb70f07..c7470ca 100644 --- a/client/src/routes/TestUserLoginPage.tsx +++ b/client/src/routes/TestUserLoginPage.tsx @@ -22,6 +22,12 @@ export default function TestUserLoginPage() { // hindrer at siden lastes på nytt event.preventDefault(); + // sjekker at begge feltene er fylt ut + if (!username.trim() || !password) { + setError('Fyll inn brukernavn og passord.'); + return; + } + // sjekker brukernavn og passord if (loginAsTestUser(username, password)) { navigate('/'); @@ -45,7 +51,6 @@ export default function TestUserLoginPage() { onChange={(event) => setUsername(event.target.value)} placeholder="Brukernavn" autoComplete="username" - required /> @@ -59,7 +64,6 @@ export default function TestUserLoginPage() { placeholder="Passord" autoComplete="current-password" className="pr-12" - required /> {/* knapp som viser eller skjuler passordet */} From 3a4324e5de9605bdb5827458f4cfb1bb2a55f32f Mon Sep 17 00:00:00 2001 From: Mari Zhevagin Date: Tue, 13 Oct 2026 16:34:43 +0200 Subject: [PATCH 5/6] min side, registrering, innlogging --- client/.env.example | 2 +- client/src/App.tsx | 4 +- client/src/components/layout/Header.tsx | 2 +- .../components/profile/PreferencesForm.tsx | 14 +- client/src/lib/AuthProvider.tsx | 72 +++++++-- client/src/lib/authContext.ts | 3 +- client/src/lib/useCurrentUser.ts | 1 - client/src/routes/LoginPage.tsx | 69 ++++++--- client/src/routes/ProfilePage.tsx | 55 +++++-- client/src/routes/RegisterPage.tsx | 143 ++++++++++++++++++ client/src/routes/TestUserLoginPage.tsx | 30 ++-- client/src/types/user.ts | 2 +- 12 files changed, 322 insertions(+), 75 deletions(-) create mode 100644 client/src/routes/RegisterPage.tsx diff --git a/client/.env.example b/client/.env.example index 2f3dbb3..048c6ba 100644 --- a/client/.env.example +++ b/client/.env.example @@ -2,4 +2,4 @@ VITE_GRAPHQL_URL=/project2/graphql # Google innlogging er satt på vent til vi ser om vi får HTTPS -# VITE_GOOGLE_CLIENT_ID=1070343515871-rvhhskd48nla3u2p3bsp7b1sl2st385m.apps.googleusercontent.com \ No newline at end of file +#VITE_GOOGLE_CLIENT_ID=1070343515871-rvhhskd48nla3u2p3bsp7b1sl2st385m.apps.googleusercontent.com \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index 4723f7e..6fc1945 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -11,8 +11,9 @@ import DesignTestPage from './routes/DesignTestPage'; import LoginPage from './routes/LoginPage'; import TestUserLoginPage from './routes/TestUserLoginPage'; import RequireAuth from './components/layout/RequireAuth'; +import RegisterPage from './routes/RegisterPage'; -// Flyttet testsiden til DesignTestPage under routes +// flyttet testsiden til DesignTestPage under routes export default function App() { return ( @@ -28,6 +29,7 @@ export default function App() { } /> } /> + } /> {/* alle sidene som krever innlogging */} }> diff --git a/client/src/components/layout/Header.tsx b/client/src/components/layout/Header.tsx index 1e575dc..579c141 100644 --- a/client/src/components/layout/Header.tsx +++ b/client/src/components/layout/Header.tsx @@ -136,4 +136,4 @@ export default function Header() {
    ); -} \ No newline at end of file +} diff --git a/client/src/components/profile/PreferencesForm.tsx b/client/src/components/profile/PreferencesForm.tsx index 84ad35b..03e8d32 100644 --- a/client/src/components/profile/PreferencesForm.tsx +++ b/client/src/components/profile/PreferencesForm.tsx @@ -14,7 +14,13 @@ type RadioGroupProps = { }; // valgknapper der bare ett valg kan være aktivt -function RadioGroup({ legend, name, value, options, onChange }: RadioGroupProps) { +function RadioGroup({ + legend, + name, + value, + options, + onChange, +}: RadioGroupProps) { return (
    {legend} @@ -93,7 +99,9 @@ export function PreferencesForm() {
    Allergier -

    Oppskrifter med disse blir markert.

    +

    + Oppskrifter med disse blir markert. +

    {ALLERGIES.map((name) => { const selected = allergies.includes(name); @@ -118,4 +126,4 @@ export function PreferencesForm() {
    ); -} \ No newline at end of file +} diff --git a/client/src/lib/AuthProvider.tsx b/client/src/lib/AuthProvider.tsx index 1ba04dc..a9db75f 100644 --- a/client/src/lib/AuthProvider.tsx +++ b/client/src/lib/AuthProvider.tsx @@ -2,37 +2,77 @@ import { useState, type ReactNode } from 'react'; import type { User } from '../types/user'; import { AuthContext } from './authContext'; -// midelertidige bruker til jeg får koblet meg til database -const MOCK_GUEST_USER: User = { id: 'guest', name: 'Gjest', authProvider: 'GUEST' }; -const MOCK_TEST_USER: User = { id: 'test', name: 'Testbruker', authProvider: 'TEST' }; +// midlertidige brukere til jeg får koblet meg til databasen +const MOCK_GUEST_USER: User = { + id: 'guest', + name: 'Gjest', + authProvider: 'GUEST', +}; +const MOCK_TEST_USER: User = { + id: 'test', + name: 'Testbruker', + authProvider: 'TEST', +}; -// midlertidig innlogging for testbrukeren -const MOCK_TEST_USERNAME = 'test'; -const MOCK_TEST_PASSWORD = 'test123'; +// en konto er brukernavn, passord og brukeren som logges inn +type Account = { username: string; password: string; user: User }; -type AuthProviderProps = { children: ReactNode}; +// midlertidig liste over kontoer til backend er koblet på +const MOCK_ACCOUNTS: Account[] = [ + { username: 'test', password: 'test123', user: MOCK_TEST_USER }, +]; + +type AuthProviderProps = { children: ReactNode }; export function AuthProvider({ children }: AuthProviderProps) { const [user, setUser] = useState(null); + // kontoene som finnes, nye legges til når noen lager bruker + const [accounts, setAccounts] = useState(MOCK_ACCOUNTS); + + const continueAsGuest = () => { + setUser(MOCK_GUEST_USER); + }; + + // logger inn hvis brukernavn og passord stemmer + const login = (username: string, password: string) => { + const account = accounts.find( + (account) => + account.username === username && account.password === password, + ); - const continueAsGuest = () => { setUser(MOCK_GUEST_USER); }; + if (account) { + setUser(account.user); + return true; + } + return false; + }; - const loginAsTestUser = (username: string, password: string) => { - if ( - username === MOCK_TEST_USERNAME && password === MOCK_TEST_PASSWORD ) { - setUser(MOCK_TEST_USER); - return true; - } + // lager ny bruker og logger inn. false hvis brukernavnet er tatt + const register = (username: string, password: string) => { + if (accounts.some((account) => account.username === username)) { return false; + } + + const newUser: User = { + id: username, + name: username, + authProvider: 'PASSWORD', }; + setAccounts([...accounts, { username, password, user: newUser }]); + setUser(newUser); + return true; + }; + const logout = () => { setUser(null); }; return ( - + {children} ); -} \ No newline at end of file +} diff --git a/client/src/lib/authContext.ts b/client/src/lib/authContext.ts index 5b64260..8e11abc 100644 --- a/client/src/lib/authContext.ts +++ b/client/src/lib/authContext.ts @@ -6,7 +6,8 @@ export type AuthContextValue = { user: User | null; loading: boolean; continueAsGuest: () => void; - loginAsTestUser: (username: string, password: string) => boolean; + login: (username: string, password: string) => boolean; + register: (username: string, password: string) => boolean; logout: () => void; }; // felles boks for innlogget bruker diff --git a/client/src/lib/useCurrentUser.ts b/client/src/lib/useCurrentUser.ts index e5c2a1f..aa8a1c8 100644 --- a/client/src/lib/useCurrentUser.ts +++ b/client/src/lib/useCurrentUser.ts @@ -1,7 +1,6 @@ import { useContext } from 'react'; import { AuthContext } from './authContext'; - // Hvem som er logget inn og funksjonen for å logge inn og ut export function useCurrentUser() { const auth = useContext(AuthContext); diff --git a/client/src/routes/LoginPage.tsx b/client/src/routes/LoginPage.tsx index 92ba67e..749f529 100644 --- a/client/src/routes/LoginPage.tsx +++ b/client/src/routes/LoginPage.tsx @@ -1,14 +1,13 @@ -import { useNavigate } from 'react-router'; +import { Link, useNavigate } from 'react-router'; import { useCurrentUser } from '../lib/useCurrentUser'; import { Button } from '../components/ui/Button'; import { GoogleLogin, type CredentialResponse } from '@react-oauth/google'; import { useEffect, useRef, useState } from 'react'; -// Google knappen vises bare hvis client ID er satt i .env -// Google innlogging er satt på pause enn så lenge +// Google knappen vises bare hvis client ID er satt i .env const hasGoogle = Boolean(import.meta.env.VITE_GOOGLE_CLIENT_ID); -// Innloggingsside +// innloggingsside export default function LoginPage() { const navigate = useNavigate(); const { continueAsGuest } = useCurrentUser(); @@ -47,15 +46,11 @@ export default function LoginPage() { return (

    Velkommen tilbake

    -

    - {hasGoogle - ? 'Logg inn med Google, utforsk oppskriftene som gjest eller testbruker.' - : 'Utforsk oppskriftene som gjest eller logg inn som testbruker.'} -

    +

    Logg inn med brukeren din, eller utforsk oppskriftene som gjest.

    {hasGoogle && ( <> - {/* midlertidig: Google kobles til backend senere */} + {/* Midlertidig: Google kobles til backend senere */}

    - Lagre favorittene dine og del egne oppskrifter på tvers av enheter. + Lagre favorittene dine og del egne oppskrifter på tvers av + enheter.

    - - {/* skiller Google innlogging fra de andre valgene */} -
    - - eller - -
    )} -
    - -

    Kom i gang uten en konto.

    +

    + Har du ikke bruker?{' '} + + Lag bruker + +

    +
    + + {/* skiller innlogging fra de andre valgene */} +
    + + eller +
    - {/* går til logg inn siden for testbrukeren */} + {/* går til innloggingssiden, testbrukeren er brukernavn: test , passord: test123 */} -

    Prøv med en ferdig testkonto.

    +

    + Prøv med en ferdig testkonto: test /{' '} + test123 +

    +
    + +
    + +

    Kom i gang uten en konto.

    ); -} \ No newline at end of file +} diff --git a/client/src/routes/ProfilePage.tsx b/client/src/routes/ProfilePage.tsx index ae3f2f4..611654d 100644 --- a/client/src/routes/ProfilePage.tsx +++ b/client/src/routes/ProfilePage.tsx @@ -10,8 +10,14 @@ type RecipeTab = Exclude; // tekstene for hver oppskriftsfane (tomme til backend er koblet på) const RECIPE_TABS: Record = { favorites: { label: 'Favoritter', empty: 'Du har ingen favoritter ennå.' }, - shared: { label: 'Mine oppskrifter', empty: 'Du har ikke laget noen oppskrifter ennå.' }, - sharedWithMe: { label: 'Delt med meg', empty: 'Ingen har delt oppskrifter med deg ennå.' }, + shared: { + label: 'Mine oppskrifter', + empty: 'Du har ikke laget noen oppskrifter ennå.', + }, + sharedWithMe: { + label: 'Delt med meg', + empty: 'Ingen har delt oppskrifter med deg ennå.', + }, }; // pillene i mobilmenyen @@ -22,7 +28,7 @@ const MOBILE_TABS: { value: Tab; label: string }[] = [ { value: 'sharedWithMe', label: 'Delt med meg' }, ]; -// rosa fokusring +// rosa fokusring const FOCUS_RING = 'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pink-light'; @@ -37,7 +43,8 @@ export default function ProfilePage() { const isGuest = user.authProvider === 'GUEST'; // på desktop vises preferanser alltid til venstre, så høyre side viser favoritter i stedet - const desktopTab: RecipeTab = activeTab === 'preferences' ? 'favorites' : activeTab; + const desktopTab: RecipeTab = + activeTab === 'preferences' ? 'favorites' : activeTab; function handleLogout() { logout(); @@ -61,10 +68,13 @@ export default function ProfilePage() { ) : ( // innlogget olivengrønt panel
    -

    Din oppskriftsbok

    +

    + Din oppskriftsbok +

    Hei {user.name}

    - Favorittene dine, oppskriftene du har delt og det andre har delt med deg. + Favorittene dine, oppskriftene du har delt og det andre har delt + med deg.

    {/* TODO: hent ekte tall fra backend */} @@ -107,13 +117,18 @@ export default function ProfilePage() {

    Du er logget inn som gjest

    -

    Ta vare på det du lagrer

    +

    + Ta vare på det du lagrer +

    - Det du lagrer nå, finnes bare i denne nettleseren. Lag en bruker for å - beholde det på alle enhetene dine. + Det du lagrer nå finnes bare i denne nettleseren. Lag en bruker + for å beholde det på alle enhetene dine.

    - {/* TODO: bytt til /register når den siden finnes */} - + + + + + setRepeatPassword(event.target.value)} + placeholder="Skriv passordet en gang til" + autoComplete="new-password" + /> + + + {error && ( +

    + {error} +

    + )} + + + + +

    + Har du allerede bruker?{' '} + + Logg inn + +

    + + + Tilbake til innloggingsvalg + + + ); +} diff --git a/client/src/routes/TestUserLoginPage.tsx b/client/src/routes/TestUserLoginPage.tsx index c7470ca..aa70d92 100644 --- a/client/src/routes/TestUserLoginPage.tsx +++ b/client/src/routes/TestUserLoginPage.tsx @@ -7,29 +7,29 @@ import { Field } from '../components/ui/Field'; import { Input } from '../components/ui/Input'; import { useCurrentUser } from '../lib/useCurrentUser'; -// Innlogging for testbrukeren +// innlogging med brukernavn og passord export default function TestUserLoginPage() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); // Om passordet vises som tekst eller prikker const [showPassword, setShowPassword] = useState(false); const navigate = useNavigate(); - const { loginAsTestUser } = useCurrentUser(); - // feilmelding hvis brukernavn eller passord er feil + const { login } = useCurrentUser(); + // feilmelding hvis noe er feil const [error, setError] = useState(''); function handleSubmit(event: SubmitEvent) { // hindrer at siden lastes på nytt event.preventDefault(); - // sjekker at begge feltene er fylt ut + // sjekker at begge feltene er fylt ut if (!username.trim() || !password) { setError('Fyll inn brukernavn og passord.'); return; } // sjekker brukernavn og passord - if (loginAsTestUser(username, password)) { + if (login(username, password)) { navigate('/'); } else { setError('Feil brukernavn eller passord.'); @@ -39,9 +39,7 @@ export default function TestUserLoginPage() { return (

    Innlogging

    -

    - Logg inn med testbrukeren for en ferdiglaget bruker med oppskrifter. -

    +

    Logg inn med brukernavnet og passordet ditt.

    @@ -65,7 +63,7 @@ export default function TestUserLoginPage() { autoComplete="current-password" className="pr-12" /> - + {/* knapp som viser eller skjuler passordet */}
    + {error && (

    {error}

    )} + +

    + Har du ikke bruker?{' '} + + Lag bruker + +

    + Date: Tue, 13 Oct 2026 20:09:17 +0200 Subject: [PATCH 6/6] Update .env.example --- client/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/.env.example b/client/.env.example index 048c6ba..b421087 100644 --- a/client/.env.example +++ b/client/.env.example @@ -2,4 +2,4 @@ VITE_GRAPHQL_URL=/project2/graphql # Google innlogging er satt på vent til vi ser om vi får HTTPS -#VITE_GOOGLE_CLIENT_ID=1070343515871-rvhhskd48nla3u2p3bsp7b1sl2st385m.apps.googleusercontent.com \ No newline at end of file +#VITE_GOOGLE_CLIENT_ID=