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
33 changes: 29 additions & 4 deletions client/src/components/favorites/FavoriteButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import { useCurrentUser } from '../../lib/useCurrentUser';
import { LoginRequiredDialog } from '../layout/LoginRequiredDialog';
import { MY_FAVORITES, TOGGLE_FAVORITE } from '../../graphql/favorites';
import { useState } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { quick } from '../../lib/motion';

type FavoriteButtonProps = {
recipeId: string;
Expand All @@ -24,6 +27,8 @@ export function FavoriteButton({ recipeId, title }: FavoriteButtonProps) {
// henter favorittene på nytt så hjertene og favorittsiden blir riktige
refetchQueries: [MY_FAVORITES],
});
// teller trykk så ringen spilles av på nytt hver gang
const [burst, setBurst] = useState(0);

// ikke innlogget så hjertet åpner innloggingspopupen
if (!user) {
Expand All @@ -47,19 +52,39 @@ export function FavoriteButton({ recipeId, title }: FavoriteButtonProps) {
data?.myFavorites.some((recipe) => recipe.id === recipeId) ?? false;

return (
<button
<motion.button
type="button"
onClick={() => toggleFavorite()}
// hjertet spretter når det blir favoritt og krymper litt når man trykker
initial={false}
animate={{ scale: isFavorite ? [1, 1.25, 1] : 1 }}
whileTap={{ scale: 0.85 }}
transition={quick}
onClick={() => {
if (!isFavorite) setBurst((b) => b + 1);
toggleFavorite();
}}
disabled={loading}
aria-pressed={isFavorite}
aria-label={`Lagre ${title} som favoritt`}
className={BUTTON}
className={`relative ${BUTTON}`}
>
{/* egen AnimatePresence så ringen alltid animeres også inne i lister */}
<AnimatePresence>
{burst > 0 && (
<motion.span
key={burst}
initial={{ scale: 0.6, opacity: 1 }}
animate={{ scale: 1.7, opacity: 0 }}
transition={{ duration: 0.45, ease: 'easeOut' }}
className="pointer-events-none absolute inset-0 rounded-full border-2 border-primary"
/>
)}
</AnimatePresence>
{isFavorite ? (
<FavoriteIcon aria-hidden="true" />
) : (
<FavoriteBorderIcon aria-hidden="true" />
)}
</button>
</motion.button>
);
}
4 changes: 2 additions & 2 deletions client/src/components/layout/LoginRequiredDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ export function LoginRequiredDialog({
{/* knappene som åpner popupen */}
<RadixDialog.Trigger asChild>{trigger}</RadixDialog.Trigger>
<RadixDialog.Portal>
<RadixDialog.Overlay className="fixed inset-0 z-50 bg-black/40" />
<RadixDialog.Overlay className="fixed inset-0 z-50 bg-black/40 data-[state=open]:animate-overlay-in data-[state=closed]:animate-overlay-out motion-reduce:animate-none" />
{/* popup */}
<RadixDialog.Content className="fixed top-1/2 left-1/2 z-50 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.Content className="fixed top-1/2 left-1/2 z-50 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 data-[state=open]:animate-dialog-in data-[state=closed]:animate-dialog-out motion-reduce:animate-none">
<RadixDialog.Title className="font-heading text-3xl">
Du må logge inn for å bruke dette
</RadixDialog.Title>
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/profile/DeleteRecipesDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ export function DeleteRecipesDialog({
</Button>
</RadixDialog.Trigger>
<RadixDialog.Portal>
<RadixDialog.Overlay className="fixed inset-0 z-50 bg-black/40" />
<RadixDialog.Content className="fixed top-1/2 left-1/2 z-50 max-h-[92vh] w-[94%] max-w-md -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-large bg-background p-6 text-text shadow-lg focus:outline-none">
<RadixDialog.Overlay className="fixed inset-0 z-50 bg-black/40 data-[state=open]:animate-overlay-in data-[state=closed]:animate-overlay-out motion-reduce:animate-none" />
<RadixDialog.Content className="fixed top-1/2 left-1/2 z-50 max-h-[92vh] w-[94%] max-w-md -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-large bg-background p-6 text-text shadow-lg focus:outline-none data-[state=open]:animate-dialog-in data-[state=closed]:animate-dialog-out motion-reduce:animate-none">
<RadixDialog.Title className="font-display text-3xl lining-nums">
{count === 1
? 'Vil du slette 1 oppskrift?'
Expand Down
65 changes: 51 additions & 14 deletions client/src/components/profile/EditProfileDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { useState, type ReactNode, type SubmitEvent } from 'react';
import * as RadixDialog from '@radix-ui/react-dialog';
import { useMutation } from '@apollo/client/react';
import { AnimatePresence, motion } from 'motion/react';
import { Button } from '../ui/Button';
import { Field } from '../ui/Field';
import { Input } from '../ui/Input';
import { useCurrentUser } from '../../lib/useCurrentUser';
import { CHANGE_PASSWORD, UPDATE_PROFILE } from '../../graphql/auth';
import { fadeUp, quick } from '../../lib/motion';

// passordet må være minst så mange tegn
const MIN_PASSWORD_LENGTH = 6;
Expand Down Expand Up @@ -54,6 +56,11 @@ export function EditProfileDialog({ trigger }: EditProfileDialogProps) {
try {
await updateProfile({ variables: { name: name.trim() } });
setNameMessage('Navnet er lagret.');
// skjuler meldingen etter litt men bare hvis den ikke er byttet ut
setTimeout(
() => setNameMessage((m) => (m === 'Navnet er lagret.' ? '' : m)),
2500,
);
} catch (error) {
setNameMessage(
error instanceof Error ? error.message : 'Kunne ikke lagre navnet.',
Expand Down Expand Up @@ -87,6 +94,12 @@ export function EditProfileDialog({ trigger }: EditProfileDialogProps) {
setNewPassword('');
setRepeatPassword('');
setPasswordMessage('Passordet er byttet.');
// skjuler meldingen etter litt men bare hvis den ikke er byttet ut
setTimeout(
() =>
setPasswordMessage((m) => (m === 'Passordet er byttet.' ? '' : m)),
2500,
);
} catch (error) {
setPasswordMessage(
error instanceof Error ? error.message : 'Kunne ikke bytte passord.',
Expand All @@ -98,10 +111,10 @@ export function EditProfileDialog({ trigger }: EditProfileDialogProps) {
<RadixDialog.Root onOpenChange={handleOpenChange}>
<RadixDialog.Trigger asChild>{trigger}</RadixDialog.Trigger>
<RadixDialog.Portal>
<RadixDialog.Overlay className="fixed inset-0 z-50 bg-black/40" />
<RadixDialog.Overlay className="fixed inset-0 z-50 bg-black/40 data-[state=open]:animate-overlay-in data-[state=closed]:animate-overlay-out motion-reduce:animate-none" />
{/* popup som kan scrolles på små skjermer */}
<RadixDialog.Content className="fixed top-1/2 left-1/2 z-50 max-h-[92vh] w-[94%] max-w-md -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-large bg-background p-6 text-text shadow-lg focus:outline-none">
<RadixDialog.Title className="font-heading text-3xl">
<RadixDialog.Content className="fixed top-1/2 left-1/2 z-50 max-h-[92vh] w-[94%] max-w-md -translate-x-1/2 -translate-y-1/2 overflow-y-auto rounded-large bg-background p-6 text-text shadow-lg focus:outline-none data-[state=open]:animate-dialog-in data-[state=closed]:animate-dialog-out motion-reduce:animate-none">
<RadixDialog.Title className="font-display text-3xl">
Rediger profil
</RadixDialog.Title>
<RadixDialog.Description className="mt-2">
Expand All @@ -119,11 +132,23 @@ export function EditProfileDialog({ trigger }: EditProfileDialogProps) {
autoComplete="name"
/>
</Field>
{nameMessage && (
<p role="status" className="font-bold text-primary">
{nameMessage}
</p>
)}
{/* meldingen tones inn hver gang teksten endres */}
<AnimatePresence mode="wait">
{nameMessage && (
<motion.p
key={nameMessage}
role="status"
className="font-bold text-primary"
variants={fadeUp}
initial="hidden"
animate="visible"
exit="hidden"
transition={quick}
>
{nameMessage}
</motion.p>
)}
</AnimatePresence>
<Button
type="submit"
variant="primary"
Expand All @@ -139,7 +164,7 @@ export function EditProfileDialog({ trigger }: EditProfileDialogProps) {
onSubmit={savePassword}
className="mt-8 flex flex-col gap-4 border-t border-peach pt-6"
>
<h3 className="font-heading text-2xl">Bytt passord</h3>
<h3 className="font-display text-2xl">Bytt passord</h3>
<Field label="Nåværende passord" htmlFor="current-password">
<Input
id="current-password"
Expand Down Expand Up @@ -168,11 +193,23 @@ export function EditProfileDialog({ trigger }: EditProfileDialogProps) {
autoComplete="new-password"
/>
</Field>
{passwordMessage && (
<p role="status" className="font-bold text-primary">
{passwordMessage}
</p>
)}
{/* meldingen tones inn hver gang teksten endres */}
<AnimatePresence mode="wait">
{passwordMessage && (
<motion.p
key={passwordMessage}
role="status"
className="font-bold text-primary"
variants={fadeUp}
initial="hidden"
animate="visible"
exit="hidden"
transition={quick}
>
{passwordMessage}
</motion.p>
)}
</AnimatePresence>
<Button
type="submit"
variant="primary"
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/profile/PreferencesForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export function PreferencesForm() {
Preferanser
</h2>
<p className="text-sm text-text/70">
Brukes til å tilpasse oppskriftene til deg.
Preferansene er ikke i bruk ennå.
</p>
</div>

Expand Down Expand Up @@ -139,7 +139,7 @@ export function PreferencesForm() {
<fieldset>
<legend className="text-sm font-medium">Allergier</legend>
<p className="text-xs text-text/70">
Oppskrifter med disse blir markert.
Oppskrifter med disse blir markert (ikke aktiv ennå).
</p>
<div className="mt-3 flex flex-wrap gap-2">
{ALLERGIES.map((name) => {
Expand Down
30 changes: 30 additions & 0 deletions client/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,36 @@

/* bredde der pc header tar over fra mobil header */
--breakpoint-desktop: 35rem;


/* popup tones inn og glir inn */
--animate-dialog-in: dialog-in 200ms ease-out;
--animate-dialog-out: dialog-out 150ms ease-in;
--animate-overlay-in: overlay-in 200ms ease-out;
--animate-overlay-out: overlay-out 150ms ease-in;

@keyframes dialog-in {
from {
opacity: 0;
transform: translateY(12px);
}
}
@keyframes dialog-out {
to {
opacity: 0;
transform: translateY(12px);
}
}
@keyframes overlay-in {
from {
opacity: 0;
}
}
@keyframes overlay-out {
to {
opacity: 0;
}
}
}

/* Grunnleggende styling */
Expand Down
2 changes: 2 additions & 0 deletions client/src/lib/motion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { Transition, Variants } from 'motion/react';

// felles fart så alle animasjoner føles like
export const quick: Transition = { duration: 0.2, ease: 'easeOut' };
// rolig gli for ting som flytter seg
export const glide: Transition = { duration: 0.3, ease: 'easeOut' };

// spring for ting som skal gli eller sprette
export const spring: Transition = {
Expand Down
5 changes: 0 additions & 5 deletions client/src/routes/FavoritesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Link } from 'react-router';
import { useQuery } from '@apollo/client/react';
import { RecipeCard } from '../components/recipe/RecipeCard';
import { FavoriteButton } from '../components/favorites/FavoriteButton';
import { MY_FAVORITES } from '../graphql/favorites';

// oppskriftene brukeren har lagret som favoritter
Expand Down Expand Up @@ -39,10 +38,6 @@ export default function FavoritesPage() {
{favorites.map((recipe) => (
<li key={recipe.id} className="relative h-full">
<RecipeCard recipe={recipe} />
{/* hjertet åpner ikke oppskriften */}
<div className="absolute top-3 right-3">
<FavoriteButton recipeId={recipe.id} title={recipe.title} />
</div>
</li>
))}
</ul>
Expand Down
Loading