Skip to content
6 changes: 3 additions & 3 deletions client/.env.example
Original file line number Diff line number Diff line change
@@ -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

# Google innlogging er satt på vent til vi ser om vi får HTTPS
#VITE_GOOGLE_CLIENT_ID=
4 changes: 3 additions & 1 deletion client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Routes>
Expand All @@ -28,6 +29,7 @@ export default function App() {

<Route path="login" element={<LoginPage />} />
<Route path="login/test" element={<TestUserLoginPage />} />
<Route path="register" element={<RegisterPage />} />

{/* alle sidene som krever innlogging */}
<Route element={<RequireAuth />}>
Expand Down
72 changes: 51 additions & 21 deletions client/src/components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -33,15 +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 som åpner ny oppskrift eller innloggingspopup hvis man ikke er logget inn
const newRecipeButton = (
<Button variant="primary" size="small">
Ny Oppskrift
</Button>
);

// åpner innloggings popupen
const favoritesButton = (
<button
type="button"
className={`cursor-pointer ${navLinkClass({ isActive: false })}`}
>
Favoritter
</button>
);

return (
<header className="border-b border-secondary bg-background px-6 py-4">
<div className="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-x-8 gap-y-4">
Expand All @@ -65,10 +83,18 @@ export default function Header() {
</NavLink>
</li>

{/* favoritter går gjennom når innlogget popup hvis ikke */}
<li>
<NavLink to="/favorites" className={navLinkClass}>
Favoritter
</NavLink>
{user ? (
<NavLink to="/favorites" className={navLinkClass}>
Favoritter
</NavLink>
) : (
<LoginRequiredDialog
trigger={favoritesButton}
message="Logg inn eller fortsett som gjest for å lagre favoritter."
/>
)}
</li>

<li>
Expand All @@ -77,25 +103,29 @@ export default function Header() {
</NavLink>
</li>

{/* Ny oppskrift */}
{/* ny oppskrift */}
<li>
<Dialog
trigger={
<Button variant="primary" size="small">
Ny Oppskrift
</Button>
}
title="Ny oppskrift"
headerContent={
<RecipeStepIndicator currentStep={currentStep} />
}
>
<NewRecipePage
currentStep={currentStep}
setCurrentStep={setCurrentStep}
{user ? (
<Dialog
trigger={newRecipeButton}
title="Ny oppskrift"
headerContent={
<RecipeStepIndicator currentStep={currentStep} />
}
>
<NewRecipePage
currentStep={currentStep}
setCurrentStep={setCurrentStep}
/>
</Dialog>
) : (
<LoginRequiredDialog
trigger={newRecipeButton}
message="Logg inn eller fortsett som gjest for å lage egne oppskrifter."
/>
</Dialog>
)}
</li>

<li>
<NavLink to={user ? '/profile' : '/login'} className={userClass}>
{user ? 'Bruker' : 'Logg inn'}
Expand Down
47 changes: 47 additions & 0 deletions client/src/components/layout/LoginRequiredDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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 (
<RadixDialog.Root>
{/* knappene som åpner popupen */}
<RadixDialog.Trigger asChild>{trigger}</RadixDialog.Trigger>
<RadixDialog.Portal>
<RadixDialog.Overlay className="fixed inset-0 bg-black/40" />
{/* popup */}
<RadixDialog.Content className="fixed top-1/2 left-1/2 w-[94%] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-large bg-background p-6 text-text shadow-lg focus:outline-none">
<RadixDialog.Title className="font-heading text-3xl">
Du må logge inn for å bruke dette
</RadixDialog.Title>
<RadixDialog.Description className="mt-2">
{message}
</RadixDialog.Description>
<div className="mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<RadixDialog.Close asChild>
<Button variant="secondary">Avbryt</Button>
</RadixDialog.Close>
{/* lukker popup og går til innloggingsside */}
<RadixDialog.Close asChild>
<Button variant="primary" onClick={() => navigate('/login')}>
Til innlogging
</Button>
</RadixDialog.Close>
</div>
</RadixDialog.Content>
</RadixDialog.Portal>
</RadixDialog.Root>
);
}
4 changes: 2 additions & 2 deletions client/src/components/layout/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, type FormEvent } from 'react';
import { useState, type SubmitEvent } from 'react';
import { useNavigate } from 'react-router';

type SearchBarProps = {
Expand All @@ -10,7 +10,7 @@ export default function SearchBar({ initialQuery }: SearchBarProps) {
const [input, setInput] = useState(initialQuery);
const navigate = useNavigate();

function handleSubmit(event: FormEvent<HTMLFormElement>) {
function handleSubmit(event: SubmitEvent<HTMLFormElement>) {
// Hindrer at skjemaet laster siden på nytt
event.preventDefault();

Expand Down
129 changes: 129 additions & 0 deletions client/src/components/profile/PreferencesForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
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 (
<fieldset className="flex flex-col gap-2">
<legend className="mb-2 text-sm font-medium">{legend}</legend>
<div className="flex gap-7">
{options.map((option) => (
<label
key={option.value}
className="flex cursor-pointer items-center gap-2 py-1"
>
<input
type="radio"
name={name}
value={option.value}
checked={value === option.value}
onChange={() => onChange(option.value)}
className="h-5 w-5 accent-primary"
/>
{option.label}
</label>
))}
</div>
</fieldset>
);
}

// språk, måleenheter og allergier
export function PreferencesForm() {
const [language, setLanguage] = useState('nb');
const [units, setUnits] = useState('metric');
const [allergies, setAllergies] = useState<string[]>([]);

// 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 (
<section
aria-labelledby="preferanser-tittel"
className="flex h-full flex-col gap-6 rounded-large border border-secondary/60 bg-background p-6 md:p-8"
>
<div>
<h2 id="preferanser-tittel" className="font-heading text-3xl">
Preferanser
</h2>
<p className="text-sm text-text/70">
Brukes til å tilpasse oppskriftene til deg.
</p>
</div>

<RadioGroup
legend="Språk"
name="language"
value={language}
onChange={setLanguage}
options={[
{ value: 'nb', label: 'Norsk' },
{ value: 'en', label: 'English' },
]}
/>

<RadioGroup
legend="Måleenheter"
name="units"
value={units}
onChange={setUnits}
options={[
{ value: 'metric', label: 'Metrisk' },
{ value: 'imperial', label: 'Imperial' },
]}
/>

<fieldset>
<legend className="text-sm font-medium">Allergier</legend>
<p className="text-xs text-text/70">
Oppskrifter med disse blir markert.
</p>
<div className="mt-3 flex flex-wrap gap-2">
{ALLERGIES.map((name) => {
const selected = allergies.includes(name);
return (
<button
key={name}
type="button"
aria-pressed={selected}
onClick={() => toggleAllergy(name)}
className={`cursor-pointer rounded-[20px] border px-5 py-2.5 transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pink-light ${
selected
? 'border-primary bg-primary font-bold text-background'
: 'border-peach bg-background font-medium hover:border-secondary hover:bg-secondary/20'
}`}
>
{selected && '✓ '}
{name}
</button>
);
})}
</div>
</fieldset>
</section>
);
}
Loading