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
183 changes: 163 additions & 20 deletions client/src/components/layout/NewRecipeButton.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,190 @@
import { useState, type ReactNode } from 'react';
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';
import { useCallback, useState, type ReactNode } from 'react'
import { useMutation } from '@apollo/client/react'
import { Dialog } from '../ui/Dialog'
import NewRecipePage, { type RecipeFormData } from '../../routes/NewRecipePage'
import { RecipeStepIndicator } from '../new-recipe/RecipeStepIndicator'
import LeaveRecipeDialog from '../new-recipe/LeaveRecipeDialog'
import { useCurrentUser } from '../../lib/useCurrentUser'
import { LoginRequiredDialog } from './LoginRequiredDialog'
import { SAVE_DRAFT, MY_DRAFTS } from '../../graphql/profile'

type NewRecipeButtonProps = {
// knappen som vises, er forskjellig på pc og mobil
trigger: ReactNode;
};
trigger: ReactNode
}

// åpner ny oppskrift eller innloggingspopup hvis man ikke er logget inn
export function NewRecipeButton({ trigger }: NewRecipeButtonProps) {
const { user } = useCurrentUser();
// hvordan steg i ny oppskrift man er på
const [currentStep, setCurrentStep] = useState(1);
const [isOpen, setIsOpen] = useState(false);
const { user } = useCurrentUser()

const [currentStep, setCurrentStep] = useState(1)
const [isOpen, setIsOpen] = useState(false)
const [showLeaveConfirm, setShowLeaveConfirm] = useState(false)
const [formData, setFormData] = useState<RecipeFormData | null>(null)
const [formKey, setFormKey] = useState(0)
const [draftError, setDraftError] = useState<string | null>(null)

const [saveDraft, { loading: savingDraft }] = useMutation(SAVE_DRAFT, {
refetchQueries: [MY_DRAFTS],
})

const handleFormChange = useCallback((data: RecipeFormData) => {
setFormData(data)
}, [])

const hasChanges = Boolean(
formData && (
formData.title.trim() ||
formData.description.trim() ||
formData.imageFile ||
formData.tags.length > 0 ||
formData.sourceAuthor.trim() ||
formData.sourceUrl.trim() ||
formData.ingredientNote.trim() ||
formData.instructionText.trim() ||
formData.ingredients.some((item) =>
item.name.trim() || item.amount.trim()
) ||
formData.steps.some((step) =>
step.text.trim() || step.minutes.trim()
)
)
)

const closeAndReset = () => {
setShowLeaveConfirm(false)
setIsOpen(false)
setCurrentStep(1)
setFormData(null)
setDraftError(null)
setFormKey((key) => key + 1)
}

const handleOpenChange = (nextOpen: boolean) => {
if (nextOpen) {
setIsOpen(true)
return
}

if (showLeaveConfirm || savingDraft) return

if (hasChanges) {
setDraftError(null)
setShowLeaveConfirm(true)
} else {
closeAndReset()
}
}

// Brukes både av knappene i stegene og av bekreftelsesdialogen.
// Valgfri data-parameter gjør at et steg kan sende sine nyeste skjemadata direkte.
const handleSaveDraft = async (data?: RecipeFormData) => {
const draftData = data ?? formData
if (!draftData || savingDraft) return

setDraftError(null)

try {
const ingredients = draftData.ingredients
.filter((item) => item.name.trim())
.map((item, index) => ({
name: item.name.trim(),
amount: Number(item.amount.replace(',', '.')) || 0,
unit: item.unit,
order: index + 1,
}))

const steps = draftData.instructionMode === 'steps'
? draftData.steps
.filter((step) => step.text.trim())
.map((step, index) => ({
order: index + 1,
instruction: step.text.trim(),
}))
: draftData.instructionText
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.map((instruction, index) => ({
order: index + 1,
instruction,
}))

await saveDraft({
variables: {
input: {
title: draftData.title.trim() || 'Uten tittel',
description: draftData.description.trim(),
prepTimeMinutes: draftData.prepTime,
servings: draftData.servings,
category: draftData.category,
tagNames: draftData.tags,
ingredients,
steps,
},
},
})

closeAndReset()
} catch (error) {
console.error('Kunne ikke lagre utkast:', error)
setDraftError(
error instanceof Error
? error.message
: 'Kunne ikke lagre utkastet. Prøv igjen.',
)
}
}

const handleDiscard = () => {
closeAndReset()
}

const handlePublished = () => {
closeAndReset()
}

if (!user) {
return (
<LoginRequiredDialog
trigger={trigger}
message="Logg inn eller fortsett som gjest for å lage egne oppskrifter."
/>
);
)
}

return (
<Dialog
trigger={trigger}
title="Ny oppskrift"
open={isOpen}
onOpenChange={setIsOpen}
onOpenChange={handleOpenChange}
headerContent={<RecipeStepIndicator currentStep={currentStep} />}
>
<NewRecipePage
key={formKey}
currentStep={currentStep}
setCurrentStep={setCurrentStep}
onPublished={() => {
setIsOpen(false)
setCurrentStep(1)
}}
onPublished={handlePublished}
onFormChange={handleFormChange}
onSaveDraft={handleSaveDraft}
savingDraft={savingDraft}
/>

{showLeaveConfirm && (
<LeaveRecipeDialog
onContinue={() => setShowLeaveConfirm(false)}
onDiscard={handleDiscard}
onSaveDraft={() => void handleSaveDraft()}
/>
)}

{draftError && (
<p
role="alert"
className="mt-3 rounded-medium border border-peach bg-background p-3 text-sm text-primary"
>
Kunne ikke lagre utkast: {draftError}
</p>
)}
</Dialog>
)
}
92 changes: 92 additions & 0 deletions client/src/components/new-recipe/LeaveRecipeDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@

import * as RadixDialog from '@radix-ui/react-dialog'
import { motion, useReducedMotion } from 'motion/react'

type LeaveRecipeDialogProps = {
onContinue: () => void
onDiscard: () => void
onSaveDraft: () => void
}

export default function LeaveRecipeDialog({
onContinue,
onDiscard,
onSaveDraft,
}: LeaveRecipeDialogProps) {
const reduceMotion = useReducedMotion()

return (
<RadixDialog.Root
open={true}
onOpenChange={(open) => {
if (!open) onContinue()
}}
>
<RadixDialog.Portal>
{/* Mørk bakgrunn over oppskriftsskjemaet */}
<RadixDialog.Overlay
className="fixed inset-0 z-[60] bg-black/40"
/>

{/* Bekreftelsesdialog */}
<RadixDialog.Content asChild>
<motion.div
initial={{
opacity: 0,
y: reduceMotion ? 0 : 60,
}}
animate={{
opacity: 1,
y: 0,
}}
transition={{
duration: reduceMotion ? 0 : 0.35,
ease: 'easeOut',
}}
className="fixed left-1/2 top-1/2 z-[70] w-[90%] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-medium border border-peach bg-background p-6 shadow-xl focus:outline-none"
>
<RadixDialog.Title
className="font-heading text-2xl text-text"
>
Vil du forlate oppskriften?
</RadixDialog.Title>

<RadixDialog.Description
className="mt-3 text-sm text-olive"
>
Du har endringer som ikke er lagret.
Hva ønsker du å gjøre?
</RadixDialog.Description>

<div className="mt-6 flex flex-col gap-3">
<button
type="button"
onClick={onSaveDraft}
className="cursor-pointer rounded-medium bg-primary px-5 py-3 text-sm font-semibold text-white hover:opacity-90"
>
Lagre utkast
</button>

<RadixDialog.Close asChild>
<button
type="button"
className="cursor-pointer rounded-medium border border-peach px-5 py-3 text-sm font-semibold text-text hover:bg-peach/30"
>
Nei, fortsett å redigere
</button>
</RadixDialog.Close>

<button
type="button"
onClick={onDiscard}
className="cursor-pointer px-5 py-2 text-sm font-semibold text-primary hover:underline"
>
Ja, forlat uten å lagre
</button>
</div>
</motion.div>
</RadixDialog.Content>
</RadixDialog.Portal>
</RadixDialog.Root>
)
}
Loading