diff --git a/client/src/components/layout/StickyHeader.tsx b/client/src/components/layout/StickyHeader.tsx index 8d7ad35..6434710 100644 --- a/client/src/components/layout/StickyHeader.tsx +++ b/client/src/components/layout/StickyHeader.tsx @@ -39,8 +39,11 @@ export function StickyHeader({ visible, query }: StickyHeaderProps) { -
- +
+ {/* søkefeltet krymper så knappene alltid får plass */} +
+ +
diff --git a/client/src/components/profile/PreferencesForm.tsx b/client/src/components/profile/PreferencesForm.tsx index 03e8d32..48fa3cf 100644 --- a/client/src/components/profile/PreferencesForm.tsx +++ b/client/src/components/profile/PreferencesForm.tsx @@ -1,4 +1,7 @@ import { useState } from 'react'; +import { useMutation } from '@apollo/client/react'; +import { useCurrentUser } from '../../lib/useCurrentUser'; +import { UPDATE_PREFERENCES } from '../../graphql/auth'; // allergiene brukeren kan velge const ALLERGIES = ['Gluten', 'Laktose', 'Nøtter', 'Egg', 'Skalldyr', 'Soya']; @@ -48,19 +51,55 @@ function RadioGroup({ // språk, måleenheter og allergier export function PreferencesForm() { - const [language, setLanguage] = useState('nb'); - const [units, setUnits] = useState('metric'); - const [allergies, setAllergies] = useState([]); + const { user } = useCurrentUser(); + const [language, setLanguage] = useState(user?.languagePref ?? 'nb'); + const [units, setUnits] = useState(user?.unitSystem ?? 'metric'); + const [allergies, setAllergies] = useState(user?.allergens ?? []); - // 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]); + const [updatePreferences, { error }] = useMutation(UPDATE_PREFERENCES); + // om siste endring ble lagret + const [saved, setSaved] = useState(false); + + // lagrer alle valgene hver gang noe endres + async function save( + nextLanguage: string, + nextUnits: string, + nextAllergies: string[], + ) { + setSaved(false); + try { + await updatePreferences({ + variables: { + languagePref: nextLanguage, + unitSystem: nextUnits, + allergens: nextAllergies, + }, + }); + setSaved(true); + } catch { + // feilmeldingen vises nederst i skjemaet } } + function changeLanguage(value: string) { + setLanguage(value); + save(value, units, allergies); + } + + function changeUnits(value: string) { + setUnits(value); + save(language, value, allergies); + } + + // legger til allergien hvis den ikke er valgt + function toggleAllergy(name: string) { + const next = allergies.includes(name) + ? allergies.filter((allergy) => allergy !== name) + : [...allergies, name]; + setAllergies(next); + save(language, units, next); + } + return (
+ {/* sier om valgene ble lagret også til skjermleser */} +

+ {error ? 'Kunne ikke lagre. Prøv igjen.' : saved ? 'Lagret' : ''} +

); } diff --git a/client/src/graphql/auth.ts b/client/src/graphql/auth.ts index 3f04d88..abdd6fb 100644 --- a/client/src/graphql/auth.ts +++ b/client/src/graphql/auth.ts @@ -7,6 +7,7 @@ export type BackendUser = { languagePref: string; unitSystem: string; isGuest: boolean; + allergens: string[]; }; const USER_FIELDS = gql` @@ -17,6 +18,7 @@ const USER_FIELDS = gql` languagePref unitSystem isGuest + allergens } `; @@ -61,3 +63,25 @@ export const CONTINUE_AS_GUEST = gql` } ${USER_FIELDS} `; + + +// lagrer språk, måleenheter og allergier +export const UPDATE_PREFERENCES: TypedDocumentNode< + { updatePreferences: BackendUser }, + { languagePref: string; unitSystem: string; allergens: string[] } +> = gql` + mutation UpdatePreferences( + $languagePref: String! + $unitSystem: String! + $allergens: [String!]! + ) { + updatePreferences( + languagePref: $languagePref + unitSystem: $unitSystem + allergens: $allergens + ) { + ...UserFields + } + } + ${USER_FIELDS} +`; diff --git a/client/src/types/user.ts b/client/src/types/user.ts index b45fd61..ac430b1 100644 --- a/client/src/types/user.ts +++ b/client/src/types/user.ts @@ -7,4 +7,5 @@ export type User = { username?: string; languagePref?: string; unitSystem?: string; + allergens?: string[]; }; diff --git a/server/src/index.ts b/server/src/index.ts index 180b488..f9799c0 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -18,6 +18,7 @@ const typeDefs = ['recipe.graphql', 'user.graphql'].map((file) => const resolvers = { Query: { ...recipeResolvers.Query, ...userResolvers.Query }, Mutation: { ...recipeResolvers.Mutation, ...userResolvers.Mutation }, + User: userResolvers.User, }; // for å teste med Apollo Sandbox diff --git a/server/src/schema/resolvers/user.ts b/server/src/schema/resolvers/user.ts index edd70d4..da34af4 100644 --- a/server/src/schema/resolvers/user.ts +++ b/server/src/schema/resolvers/user.ts @@ -13,6 +13,17 @@ const MIN_PASSWORD_LENGTH = 6; type Credentials = { username: string; password: string }; + +type Preferences = { + languagePref: string; + unitSystem: string; + allergens: string[]; +}; + +// valg +const LANGUAGES = ['nb', 'en']; +const UNIT_SYSTEMS = ['metric', 'imperial']; + export const userResolvers = { Query: { // innlogget bruker og null hvis ingen er logget inn @@ -107,5 +118,59 @@ export const userResolvers = { setLoginCookie(context.res, guest.id, true); return guest; }, + + // lagrer preferansene til innlogget bruker + updatePreferences: async ( + _: unknown, + args: Preferences, + context: Context, + ) => { + if (!context.userId) { + throw new GraphQLError('Du må være logget inn.', { + extensions: { code: 'UNAUTHENTICATED' }, + }); + } + + // sjekker på nytt + if ( + !LANGUAGES.includes(args.languagePref) || + !UNIT_SYSTEMS.includes(args.unitSystem) + ) { + throw new GraphQLError('Ugyldig preferanse.', { + extensions: { code: 'BAD_USER_INPUT' }, + }); + } + + // fjerner de gamle allergiene først + await prisma.user.update({ + where: { id: context.userId }, + data: { allergens: { set: [] } }, + }); + + // lagrer de nye valgene og lager allergien hvis den ikke finnes + return prisma.user.update({ + where: { id: context.userId }, + data: { + languagePref: args.languagePref, + unitSystem: args.unitSystem, + allergens: { + connectOrCreate: args.allergens.map((name) => ({ + where: { name }, + create: { name }, + })), + }, + }, + }); + }, + }, + + User: { + // henter navnene på allergiene til brukeren + allergens: async (user: { id: string }) => { + const allergens = await prisma.allergen.findMany({ + where: { users: { some: { id: user.id } } }, + }); + return allergens.map((allergen) => allergen.name); + }, }, }; diff --git a/server/src/schema/typeDefs/user.graphql b/server/src/schema/typeDefs/user.graphql index c60f6d9..766dae4 100644 --- a/server/src/schema/typeDefs/user.graphql +++ b/server/src/schema/typeDefs/user.graphql @@ -5,6 +5,7 @@ type User { languagePref: String! unitSystem: String! isGuest: Boolean! + allergens: [String!]! } extend type Query { @@ -19,4 +20,10 @@ extend type Mutation { login(username: String!, password: String!): User! logout: Boolean! continueAsGuest: User! + # lagrer språk, måleenheter og allergier for brukere + updatePreferences( + languagePref: String! + unitSystem: String! + allergens: [String!]! + ): User! }