From 55b649ef89a02917a81742641763bf96e1ec61f2 Mon Sep 17 00:00:00 2001 From: Mari Zhevagin Date: Thu, 15 Oct 2026 23:37:18 +0200 Subject: [PATCH 1/8] sletting av oppskrift - backend --- server/src/schema/resolvers/user.ts | 24 ++++++++++++++++++++++++ server/src/schema/typeDefs/user.graphql | 2 ++ 2 files changed, 26 insertions(+) diff --git a/server/src/schema/resolvers/user.ts b/server/src/schema/resolvers/user.ts index 14117aa..ef73d57 100644 --- a/server/src/schema/resolvers/user.ts +++ b/server/src/schema/resolvers/user.ts @@ -264,6 +264,30 @@ export const userResolvers = { where: { id: context.userId }, data: { passwordHash }, }); + return true; + }, + // sletter oppskriften bare hvis brukeren er forfatteren + deleteMyRecipe: async ( + _: unknown, + args: { id: string }, + context: Context, + ) => { + if (!context.userId) { + throw new GraphQLError('Du må være logget inn.', { + extensions: { code: 'UNAUTHENTICATED' }, + }); + } + + // sletter ingenting hvis forfatter er en annen + const result = await prisma.recipe.deleteMany({ + where: { id: args.id, authorId: context.userId }, + }); + + if (result.count === 0) { + throw new GraphQLError('Fant ikke oppskriften eller du eier den ikke.', { + extensions: { code: 'FORBIDDEN' }, + }); + } return true; }, diff --git a/server/src/schema/typeDefs/user.graphql b/server/src/schema/typeDefs/user.graphql index 3cf43af..ea40853 100644 --- a/server/src/schema/typeDefs/user.graphql +++ b/server/src/schema/typeDefs/user.graphql @@ -32,4 +32,6 @@ extend type Mutation { changePassword(currentPassword: String!, newPassword: String!): Boolean! # legger til eller fjerner en favoritt toggleFavorite(recipeId: ID!): Boolean! + # sletter en oppskrift brukeren selv har laget + deleteMyRecipe(id: ID!): Boolean! } From 55b42e2ca2e6964d15951ea2724d0a1d1ae9323a Mon Sep 17 00:00:00 2001 From: Mari Zhevagin Date: Fri, 16 Oct 2026 16:24:45 +0200 Subject: [PATCH 2/8] backend fiks og en liten bilde fiks --- client/src/components/ui/RecipeImage.tsx | 14 +++++++++++++- server/src/schema/resolvers/user.ts | 10 +++++----- server/src/schema/typeDefs/user.graphql | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/client/src/components/ui/RecipeImage.tsx b/client/src/components/ui/RecipeImage.tsx index a052cff..929a020 100644 --- a/client/src/components/ui/RecipeImage.tsx +++ b/client/src/components/ui/RecipeImage.tsx @@ -7,6 +7,18 @@ type RecipeImageProps = { className?: string; }; +// legger til /project2/ foran opplastede bilder +function toImageSrc(imageUrl: string) { + if ( + imageUrl.startsWith('http') || + imageUrl.startsWith('data:') || + imageUrl.startsWith('/project2/') + ) { + return imageUrl; + } + return `/project2/${imageUrl.replace(/^\/+/, '')}`; +} + // viser bildet til oppskriften eller en tegning hvis bildet mangler export function RecipeImage({ imageUrl, @@ -19,7 +31,7 @@ export function RecipeImage({ if (imageUrl && !failed) { return ( {alt} setFailed(true)} className={`object-cover ${className}`} diff --git a/server/src/schema/resolvers/user.ts b/server/src/schema/resolvers/user.ts index ef73d57..04c7b87 100644 --- a/server/src/schema/resolvers/user.ts +++ b/server/src/schema/resolvers/user.ts @@ -266,10 +266,10 @@ export const userResolvers = { }); return true; }, - // sletter oppskriften bare hvis brukeren er forfatteren - deleteMyRecipe: async ( + // sletter oppskriftene bare hvis brukeren er forfatteren + deleteMyRecipes: async ( _: unknown, - args: { id: string }, + args: { ids: string[] }, context: Context, ) => { if (!context.userId) { @@ -280,7 +280,7 @@ export const userResolvers = { // sletter ingenting hvis forfatter er en annen const result = await prisma.recipe.deleteMany({ - where: { id: args.id, authorId: context.userId }, + where: { id: { in: args.ids}, authorId: context.userId }, }); if (result.count === 0) { @@ -289,7 +289,7 @@ export const userResolvers = { }); } - return true; + return result.count; }, }, diff --git a/server/src/schema/typeDefs/user.graphql b/server/src/schema/typeDefs/user.graphql index ea40853..9c9ee79 100644 --- a/server/src/schema/typeDefs/user.graphql +++ b/server/src/schema/typeDefs/user.graphql @@ -33,5 +33,5 @@ extend type Mutation { # legger til eller fjerner en favoritt toggleFavorite(recipeId: ID!): Boolean! # sletter en oppskrift brukeren selv har laget - deleteMyRecipe(id: ID!): Boolean! + deleteMyRecipe(ids: [ID!]): Int! } From 79857349447213508054ed12813bb90689b61730 Mon Sep 17 00:00:00 2001 From: Mari Zhevagin Date: Fri, 16 Oct 2026 18:22:02 +0200 Subject: [PATCH 3/8] skrivefeil fiks --- server/src/schema/typeDefs/user.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/schema/typeDefs/user.graphql b/server/src/schema/typeDefs/user.graphql index 9c9ee79..96cde7e 100644 --- a/server/src/schema/typeDefs/user.graphql +++ b/server/src/schema/typeDefs/user.graphql @@ -33,5 +33,5 @@ extend type Mutation { # legger til eller fjerner en favoritt toggleFavorite(recipeId: ID!): Boolean! # sletter en oppskrift brukeren selv har laget - deleteMyRecipe(ids: [ID!]): Int! + deleteMyRecipes(ids: [ID!]!): Int! } From ca762641b057a024d5641123bc7d69305083762f Mon Sep 17 00:00:00 2001 From: Mari Zhevagin Date: Fri, 16 Oct 2026 20:20:30 +0200 Subject: [PATCH 4/8] =?UTF-8?q?laget=20eget=20ikon=20da=20jeg=20ikke=20fan?= =?UTF-8?q?t=20noe=20jeg=20ville=20ha=20p=C3=A5=20mui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/icons/TrashIcon.tsx | 49 +++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 client/src/components/icons/TrashIcon.tsx diff --git a/client/src/components/icons/TrashIcon.tsx b/client/src/components/icons/TrashIcon.tsx new file mode 100644 index 0000000..fc34574 --- /dev/null +++ b/client/src/components/icons/TrashIcon.tsx @@ -0,0 +1,49 @@ +// ikonet jeg ville ha fantes ikke i mui så må lage det selv med hjelp av ai + +type TrashIconProps = { + open: boolean; +}; + +// søppelbøtte som åpner lokket og blir fylt når valg er på +export function TrashIcon({ open }: TrashIconProps) { + return ( + + ); +} + From 07ddce673aaaf352c01194558436efd8b85fa7e6 Mon Sep 17 00:00:00 2001 From: Mari Zhevagin Date: Fri, 16 Oct 2026 21:22:49 +0200 Subject: [PATCH 5/8] fikset sletteknapp - frontend --- .../profile/DeleteRecipesDialog.tsx | 103 ++++++++++++++++++ client/src/graphql/profile.ts | 10 ++ client/src/routes/ProfilePage.tsx | 94 ++++++++++++++-- 3 files changed, 199 insertions(+), 8 deletions(-) create mode 100644 client/src/components/profile/DeleteRecipesDialog.tsx diff --git a/client/src/components/profile/DeleteRecipesDialog.tsx b/client/src/components/profile/DeleteRecipesDialog.tsx new file mode 100644 index 0000000..7a9da18 --- /dev/null +++ b/client/src/components/profile/DeleteRecipesDialog.tsx @@ -0,0 +1,103 @@ +import { useState } from 'react'; +import * as RadixDialog from '@radix-ui/react-dialog'; +import { useMutation } from '@apollo/client/react'; +import { Button } from '../ui/Button'; +import { DELETE_MY_RECIPES, MY_RECIPES } from '../../graphql/profile'; + +type DeleteRecipesDialogProps = { + recipes: { id: string; title: string }[]; + onDeleted: () => void; +}; + +// spør om man er sikker og sletter de valgte oppskriftene +export function DeleteRecipesDialog({ + recipes, + onDeleted, +}: DeleteRecipesDialogProps) { + const [open, setOpen] = useState(false); + const [deleteRecipes, { loading, error }] = useMutation(DELETE_MY_RECIPES, { + refetchQueries: [MY_RECIPES], + }); + + const count = recipes.length; + + async function handleDelete() { + try { + await deleteRecipes({ + variables: { ids: recipes.map((recipe) => recipe.id) }, + // fjerner oppskriftene fra apollo + update(cache) { + recipes.forEach((recipe) => { + cache.evict({ + id: cache.identify({ __typename: 'Recipe', id: recipe.id }), + }); + }); + cache.gc(); + }, + }); + setOpen(false); + onDeleted(); + } catch { + // feilmeldingen vises i popupen + } + } + + return ( + + + + + + + + + {count === 1 + ? 'Vil du slette 1 oppskrift?' + : `Vil du slette ${count} oppskrifter?`} + + + Oppskriftene blir slettet permanent: + + +
    + {recipes.map((recipe) => ( +
  • {recipe.title}
  • + ))} +
+ + {error && ( +

+ {error.message} +

+ )} + +
+ + + + +
+
+
+
+ ); +} diff --git a/client/src/graphql/profile.ts b/client/src/graphql/profile.ts index e885df6..f7bbfa6 100644 --- a/client/src/graphql/profile.ts +++ b/client/src/graphql/profile.ts @@ -13,3 +13,13 @@ export const MY_RECIPES: TypedDocumentNode<{ myRecipes: Recipe[] }> = gql` } } `; + +// sletter oppskrifter brukeren selv har laget +export const DELETE_MY_RECIPES: TypedDocumentNode< + { deleteMyRecipes: number }, + { ids: string[] } +> = gql` + mutation DeleteMyRecipes($ids: [ID!]!) { + deleteMyRecipes(ids: $ids) + } +`; diff --git a/client/src/routes/ProfilePage.tsx b/client/src/routes/ProfilePage.tsx index 4ad12cf..e2a36fb 100644 --- a/client/src/routes/ProfilePage.tsx +++ b/client/src/routes/ProfilePage.tsx @@ -3,6 +3,10 @@ import { Link, useNavigate } from 'react-router'; import { Button } from '../components/ui/Button'; import { PreferencesForm } from '../components/profile/PreferencesForm'; import { EditProfileDialog } from '../components/profile/EditProfileDialog'; +import { DeleteRecipesDialog } from '../components/profile/DeleteRecipesDialog'; +import { TrashIcon } from '../components/icons/TrashIcon'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import RadioButtonUncheckedIcon from '@mui/icons-material/RadioButtonUnchecked'; import { useCurrentUser } from '../lib/useCurrentUser'; import { useQuery } from '@apollo/client/react'; import { MY_RECIPES } from '../graphql/profile'; @@ -42,6 +46,9 @@ export default function ProfilePage() { const { user, logout } = useCurrentUser(); const navigate = useNavigate(); const [activeTab, setActiveTab] = useState('preferences'); + // om man holder på å velge oppskrifter som skal slettes + const [selecting, setSelecting] = useState(false); + const [selectedIds, setSelectedIds] = useState([]); // oppskriftene brukeren har laget og den henter på nytt hver gang siden åpnes på nytt const { data: myRecipesData } = useQuery(MY_RECIPES, { @@ -75,6 +82,24 @@ export default function ProfilePage() { navigate('/login'); } + // valg vises bare under mine oppskrifter + const isSelecting = selecting && desktopTab === 'shared'; + + // slår valg av eller på og tømmer det som var valgt + function toggleSelecting() { + setSelecting(!selecting); + setSelectedIds([]); + } + + // legger til eller fjerner en oppskrift fra valget + function toggleSelected(id: string) { + if (selectedIds.includes(id)) { + setSelectedIds(selectedIds.filter((selectedId) => selectedId !== id)); + } else { + setSelectedIds([...selectedIds, id]); + } + } + return (
@@ -102,7 +127,7 @@ export default function ProfilePage() {

{/* TODO: hent ekte tall fra backend */} -
    +
      {[ { count: myRecipes.length, label: 'mine oppskrifter' }, { count: favorites.length, label: 'favoritter' }, @@ -110,7 +135,7 @@ export default function ProfilePage() { { count: 0, label: 'delt med deg' }, ].map((stat) => (
    • - + {stat.count} {stat.label} @@ -123,7 +148,7 @@ export default function ProfilePage() { trigger={ @@ -132,7 +157,7 @@ export default function ProfilePage() { @@ -228,14 +253,67 @@ export default function ProfilePage() { ))}
+ {/* søppelbøtte som slår på valg av egne oppskrifter */} + {desktopTab === 'shared' && myRecipes.length > 0 && ( +
+ {isSelecting && ( + <> + + {selectedIds.length} valgt + + + selectedIds.includes(recipe.id), + )} + onDeleted={toggleSelecting} + /> + + )} + +
+ )} + {tabRecipes.length > 0 ? ( -
    +
      {tabRecipes.map((recipe) => (
    • -
      - -
      + {isSelecting ? ( + // dekker kortet så trykk velger i stedet for å åpne oppskriften + + ) : ( +
      + +
      + )}
    • ))}
    From def0b54f6d17cbed9c2b94dd7eaacda6ce7e6c13 Mon Sep 17 00:00:00 2001 From: Mari Zhevagin Date: Fri, 16 Oct 2026 21:26:13 +0200 Subject: [PATCH 6/8] =?UTF-8?q?Kj=C3=B8rte=20prettier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/icons/TrashIcon.tsx | 1 - client/src/routes/ProfilePage.tsx | 8 ++++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/client/src/components/icons/TrashIcon.tsx b/client/src/components/icons/TrashIcon.tsx index fc34574..7c96b1f 100644 --- a/client/src/components/icons/TrashIcon.tsx +++ b/client/src/components/icons/TrashIcon.tsx @@ -46,4 +46,3 @@ export function TrashIcon({ open }: TrashIconProps) { ); } - diff --git a/client/src/routes/ProfilePage.tsx b/client/src/routes/ProfilePage.tsx index e2a36fb..889979b 100644 --- a/client/src/routes/ProfilePage.tsx +++ b/client/src/routes/ProfilePage.tsx @@ -273,9 +273,13 @@ export default function ProfilePage() { type="button" onClick={toggleSelecting} aria-pressed={isSelecting} - aria-label={isSelecting ? 'Avslutt valg' : 'Velg oppskrifter å slette'} + aria-label={ + isSelecting ? 'Avslutt valg' : 'Velg oppskrifter å slette' + } className={`flex cursor-pointer rounded-lg p-2 transition-colors ${FOCUS_RING} ${ - isSelecting ? 'text-primary' : 'text-[#52552f] hover:text-primary' + isSelecting + ? 'text-primary' + : 'text-[#52552f] hover:text-primary' }`} > From 7d71d0f697da01f3d0715045cd02f193d8bcc96f Mon Sep 17 00:00:00 2001 From: Mari Zhevagin Date: Fri, 16 Oct 2026 21:40:04 +0200 Subject: [PATCH 7/8] prettier --- server/src/schema/resolvers/user.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server/src/schema/resolvers/user.ts b/server/src/schema/resolvers/user.ts index 04c7b87..6ad5a25 100644 --- a/server/src/schema/resolvers/user.ts +++ b/server/src/schema/resolvers/user.ts @@ -280,13 +280,16 @@ export const userResolvers = { // sletter ingenting hvis forfatter er en annen const result = await prisma.recipe.deleteMany({ - where: { id: { in: args.ids}, authorId: context.userId }, + where: { id: { in: args.ids }, authorId: context.userId }, }); if (result.count === 0) { - throw new GraphQLError('Fant ikke oppskriften eller du eier den ikke.', { - extensions: { code: 'FORBIDDEN' }, - }); + throw new GraphQLError( + 'Fant ikke oppskriften eller du eier den ikke.', + { + extensions: { code: 'FORBIDDEN' }, + }, + ); } return result.count; From 763d816ba7894fa8bd97b93a4bcea45223850eac Mon Sep 17 00:00:00 2001 From: Mari Brokke Zhevagin Date: Fri, 16 Oct 2026 21:45:30 +0200 Subject: [PATCH 8/8] liten kommentar fiks --- server/src/schema/resolvers/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/schema/resolvers/user.ts b/server/src/schema/resolvers/user.ts index 6ad5a25..0fb6e5b 100644 --- a/server/src/schema/resolvers/user.ts +++ b/server/src/schema/resolvers/user.ts @@ -285,7 +285,7 @@ export const userResolvers = { if (result.count === 0) { throw new GraphQLError( - 'Fant ikke oppskriften eller du eier den ikke.', + 'Fant ikke oppskriften eller så eier du den ikke.', { extensions: { code: 'FORBIDDEN' }, },