perf(images): convert remaining <img> tags to next/image project-wide
Clears every remaining @next/next/no-img-element warning — automatic responsive srcset, lazy-loading, and format optimization instead of always loading the original file at full size. Fixed-size icons got explicit width/height; dynamic-aspect photos got fill inside a relative wrapper.
This commit is contained in:
+2
-1
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Reveal } from "../components/Reveal";
|
||||
import { Footer } from "../components/Footer";
|
||||
import { TrustRow } from "../components/TrustRow";
|
||||
@@ -42,7 +43,7 @@ export default async function AgbPage() {
|
||||
{/* Static, not part of the CMS content — same reasoning as
|
||||
the Impressum/Datenschutz pages' own callout cards. */}
|
||||
<div className="bg-bg-muted flex flex-col gap-3 items-start p-6 rounded-md w-full">
|
||||
<img alt="" src="/icon-trust-leaf.png" className="size-7 object-contain" />
|
||||
<Image alt="" src="/icon-trust-leaf.png" width={28} height={28} className="size-7 object-contain" />
|
||||
<p
|
||||
className="font-semibold text-body text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
|
||||
@@ -270,9 +270,12 @@ export function BestellbestaetigungContent() {
|
||||
gap shows the image itself, not whatever's behind it — the
|
||||
parent's overflow-hidden clips the small overflow back
|
||||
down to a clean box either way. */}
|
||||
<img
|
||||
<Image
|
||||
alt=""
|
||||
src="/bestellbestaetigung-testimonial-photo.jpg"
|
||||
width={366}
|
||||
height={126}
|
||||
sizes="(min-width: 768px) 45vw, 100vw"
|
||||
className="absolute -inset-1 w-[calc(100%+0.5rem)] h-[calc(100%+0.5rem)] object-cover"
|
||||
/>
|
||||
<div className="hidden md:block absolute inset-y-0 right-0 w-40 bg-gradient-to-l from-bg-muted to-transparent" />
|
||||
|
||||
@@ -110,7 +110,7 @@ export default async function BlogDetailPage({
|
||||
href="/todo-cards"
|
||||
className="group flex items-center gap-6 border border-border rounded-md px-9 py-7 hover:border-brand transition-colors"
|
||||
>
|
||||
<img alt="" src="/icon-todo-passend-dazu.png" className="w-16 h-[4.6875rem] shrink-0 object-contain" />
|
||||
<Image alt="" src="/icon-todo-passend-dazu.png" width={64} height={75} className="w-16 h-[4.6875rem] shrink-0 object-contain" />
|
||||
<div className="flex-1 min-w-0 flex flex-col gap-2.5">
|
||||
<p className="font-bold text-[0.8125rem] text-brand">Passend dazu:</p>
|
||||
<div className="flex items-end justify-between gap-4 w-full">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Image from "next/image";
|
||||
import { Reveal } from "./Reveal";
|
||||
|
||||
export function About() {
|
||||
@@ -75,10 +76,12 @@ export function About() {
|
||||
style={{ minHeight: "14rem" }}
|
||||
delay={0.15}
|
||||
>
|
||||
<img
|
||||
<Image
|
||||
alt="Björn"
|
||||
src="/about-author.jpg"
|
||||
className="absolute inset-0 w-full h-full object-cover object-center pointer-events-none"
|
||||
fill
|
||||
sizes="(min-width: 768px) 58vw, 100vw"
|
||||
className="object-cover object-center pointer-events-none"
|
||||
/>
|
||||
{/* Left gradient: wide enough to cover the text-column overlap — md+ only */}
|
||||
<div className="hidden md:block absolute inset-y-0 left-0 w-72 bg-gradient-to-r from-bg-dark to-transparent pointer-events-none" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { addToCart } from "../lib/cart";
|
||||
import { useCartFly } from "./CartFly";
|
||||
|
||||
@@ -61,7 +62,7 @@ export function AddToCartInlineButton({
|
||||
>
|
||||
{added ? "Hinzugefügt ✓" : label}
|
||||
</span>
|
||||
<img alt="" src="/icon-cart-outline.png" className="h-[1.875rem] w-8 object-contain" />
|
||||
<Image alt="" src="/icon-cart-outline.png" width={32} height={30} className="h-[1.875rem] w-8 object-contain" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Image from "next/image";
|
||||
import { Reveal } from "./Reveal";
|
||||
|
||||
function Word({ children }: { children: string }) {
|
||||
@@ -16,7 +17,7 @@ function Arrow() {
|
||||
<div className="flex items-center justify-center shrink-0">
|
||||
<div className="-scale-y-100 rotate-180">
|
||||
<div className="relative" style={{ height: "var(--divider-arrow-h)", width: "var(--divider-arrow-w)" }}>
|
||||
<img alt="" src="/icon-separator.svg" className="absolute inset-0 w-full h-full" />
|
||||
<Image alt="" src="/icon-separator.svg" fill sizes="48px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,7 +61,7 @@ export function Divider() {
|
||||
className="relative"
|
||||
style={{ height: "var(--divider-sparkle-inner-h)", width: "var(--divider-sparkle-inner-w)" }}
|
||||
>
|
||||
<img alt="" src="/icon-separator-right.svg" className="absolute inset-0 w-full h-full" />
|
||||
<Image alt="" src="/icon-separator-right.svg" fill sizes="48px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,11 +61,7 @@ export function Hero() {
|
||||
Starte mit der 7-Tage-Challenge
|
||||
</span>
|
||||
<div className="relative h-[1.1875rem] w-[1.5625rem] shrink-0">
|
||||
<img
|
||||
alt=""
|
||||
src="/icon-check.svg"
|
||||
className="absolute inset-0 w-full h-full"
|
||||
/>
|
||||
<Image alt="" src="/icon-check.svg" fill sizes="26px" />
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ReactNode } from "react";
|
||||
import Image from "next/image";
|
||||
import type { TOCSection } from "./SectionTOC";
|
||||
|
||||
// Minimal Lexical JSON → JSX renderer for Payload's richText fields.
|
||||
@@ -146,8 +147,8 @@ function renderNode(node: LexicalNode, key: string): ReactNode {
|
||||
return (
|
||||
<div key={key} className="relative flex items-start gap-6 w-full my-6">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span className="flex h-7 w-6 items-center justify-center shrink-0">
|
||||
<img alt="" src="/icon-sparkle-merke-dir.png" className="w-full h-full object-contain" />
|
||||
<span className="relative flex h-7 w-6 items-center justify-center shrink-0">
|
||||
<Image alt="" src="/icon-sparkle-merke-dir.png" fill sizes="24px" className="object-contain" />
|
||||
</span>
|
||||
<span
|
||||
className="font-bold text-text-primary text-[1.625rem] whitespace-nowrap"
|
||||
@@ -158,9 +159,11 @@ function renderNode(node: LexicalNode, key: string): ReactNode {
|
||||
</div>
|
||||
{/* Hand-drawn underline image, not a plain bar — exported
|
||||
straight from the Figma node (label-underline). */}
|
||||
<img
|
||||
<Image
|
||||
alt=""
|
||||
src="/icon-merke-dir-underline.png"
|
||||
width={136}
|
||||
height={23}
|
||||
className="absolute left-8 top-[2.1875rem] w-[8.5rem] h-[1.4375rem] object-cover pointer-events-none"
|
||||
/>
|
||||
<div className="w-px self-stretch bg-brand shrink-0" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Reveal, RevealGroup, RevealItem } from "./Reveal";
|
||||
import { getWerkzeugeCards } from "../lib/payload";
|
||||
|
||||
@@ -38,8 +39,8 @@ export async function Tools() {
|
||||
className="md:col-span-4 flex gap-8 items-start rounded-md transition-transform duration-300 hover:-translate-y-1"
|
||||
>
|
||||
{/* Icon — uniform box, pre-flipped/rotated source asset */}
|
||||
<div className="flex items-center justify-center shrink-0 size-14">
|
||||
<img alt="" src={tool.icon} className="max-w-full max-h-full object-contain" />
|
||||
<div className="relative flex items-center justify-center shrink-0 size-14">
|
||||
<Image alt="" src={tool.icon} fill sizes="56px" className="object-contain" />
|
||||
</div>
|
||||
|
||||
{/* Card content — self-stretch + h-full + justify-between so
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Image from "next/image";
|
||||
import { getTrustBadges } from "../lib/payload";
|
||||
|
||||
// Content now lives in Payload (TrustBadges collection) instead of being
|
||||
@@ -15,7 +16,9 @@ export async function TrustRow() {
|
||||
<div key={item.id} className="flex items-center gap-6 md:gap-12">
|
||||
{i > 0 && <div className="hidden md:block h-10 w-px bg-border" />}
|
||||
<div className="flex gap-4 items-center">
|
||||
<img alt="" src={item.icon} className="size-8 shrink-0 object-contain" />
|
||||
<div className="relative size-8 shrink-0">
|
||||
<Image alt="" src={item.icon} fill sizes="32px" className="object-contain" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 items-start">
|
||||
<p className="font-semibold text-body text-text-primary whitespace-nowrap">{item.title}</p>
|
||||
<p className="text-body-sm text-text-muted whitespace-nowrap">{item.description}</p>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Reveal } from "../components/Reveal";
|
||||
import { Footer } from "../components/Footer";
|
||||
import { RichText, extractHeadings } from "../components/RichText";
|
||||
@@ -43,7 +44,7 @@ export default async function DatenschutzPage() {
|
||||
callout doesn't belong in the generic LegalPages richText
|
||||
field shared across all 4 legal page types. */}
|
||||
<div className="bg-bg-muted flex flex-col gap-3 items-start p-6 rounded-md w-full">
|
||||
<img alt="" src="/icon-trust-leaf.png" className="size-7 object-contain" />
|
||||
<Image alt="" src="/icon-trust-leaf.png" width={28} height={28} className="size-7 object-contain" />
|
||||
<p
|
||||
className="font-semibold text-body text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Reveal } from "../components/Reveal";
|
||||
import { Footer } from "../components/Footer";
|
||||
import { RichText, extractHeadings } from "../components/RichText";
|
||||
@@ -47,7 +48,7 @@ export default async function ImpressumPage() {
|
||||
the actual numbers/copy that need single-sourcing from
|
||||
data. */}
|
||||
<div className="bg-bg-muted flex flex-col gap-3 items-start p-6 rounded-md w-full">
|
||||
<img alt="" src="/icon-trust-leaf.png" className="size-7 object-contain" />
|
||||
<Image alt="" src="/icon-trust-leaf.png" width={28} height={28} className="size-7 object-contain" />
|
||||
<p
|
||||
className="font-semibold text-body text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
import { Fragment } from "react";
|
||||
import Image from "next/image";
|
||||
import { Reveal, RevealGroup, RevealItem } from "../../components/Reveal";
|
||||
|
||||
// Each icon's own real pixel dimensions (not uniformly square) — needed so
|
||||
// the `h-16 w-auto` sizing below infers the correct aspect ratio instead of
|
||||
// stretching/squashing to a guessed one.
|
||||
const steps = [
|
||||
{
|
||||
icon: "/icon-weekly-step-1.png",
|
||||
width: 177,
|
||||
height: 177,
|
||||
title: "1. Anmelden",
|
||||
desc: "Trage dich mit deiner E-Mail-Adresse ein und bestätige kurz.",
|
||||
},
|
||||
{
|
||||
icon: "/icon-weekly-step-2.png",
|
||||
width: 225,
|
||||
height: 177,
|
||||
title: "2. E-Mail erhalten",
|
||||
desc: "Jeden Mittwoch bekommst du deinen neuen Impuls direkt in dein Postfach.",
|
||||
},
|
||||
{
|
||||
icon: "/icon-weekly-step-3.png",
|
||||
width: 198,
|
||||
height: 198,
|
||||
title: "3. Lesen & umsetzen",
|
||||
desc: "Lies in wenigen Minuten und setze die Tipps direkt in deinem Alltag um.",
|
||||
},
|
||||
{
|
||||
icon: "/icon-weekly-step-4.png",
|
||||
width: 180,
|
||||
height: 177,
|
||||
title: "4. Dranbleiben",
|
||||
desc: "Kleine Schritte, jede Woche – für mehr Klarheit, Fokus und Leichtigkeit.",
|
||||
},
|
||||
@@ -48,9 +60,11 @@ export function HowItWorks() {
|
||||
{steps.map((step, i) => (
|
||||
<Fragment key={step.title}>
|
||||
<RevealItem className="group flex flex-col gap-4 items-center text-center flex-1 max-w-xs md:max-w-none">
|
||||
<img
|
||||
<Image
|
||||
src={step.icon}
|
||||
alt=""
|
||||
width={step.width}
|
||||
height={step.height}
|
||||
className="h-16 w-auto object-contain transition-transform duration-300 group-hover:scale-110"
|
||||
/>
|
||||
<p className="font-semibold text-body text-text-primary">{step.title}</p>
|
||||
@@ -62,9 +76,11 @@ export function HowItWorks() {
|
||||
// technique as Challenge's step connector and todo-cards'
|
||||
// identical HowItWorks, not just the same icon asset.
|
||||
<div className="flex items-center justify-center shrink-0 md:mt-[1.625rem]">
|
||||
<img
|
||||
<Image
|
||||
alt=""
|
||||
src="/icon-arrow-connector.svg"
|
||||
width={24}
|
||||
height={24}
|
||||
className="w-6 h-6 rotate-90 md:w-10 md:h-3 md:rotate-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@ export function WeeklyBenefits() {
|
||||
<ul className="flex flex-col gap-4 items-start w-full">
|
||||
{benefits.map((b) => (
|
||||
<li key={b.title} className="flex gap-[0.625rem] items-start w-full">
|
||||
<img alt="" src="/icon-check.svg" className="size-5 shrink-0 mt-0.5" />
|
||||
<Image alt="" src="/icon-check.svg" width={20} height={20} className="size-5 shrink-0 mt-0.5" />
|
||||
<div className="flex flex-col gap-0.5 items-start flex-1 min-w-0">
|
||||
<p className="font-semibold text-body text-text-primary">{b.title}</p>
|
||||
<p className="text-body-sm text-text-muted">{b.desc}</p>
|
||||
|
||||
+9
-2
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Reveal, RevealGroup, RevealItem } from "./components/Reveal";
|
||||
import { Footer } from "./components/Footer";
|
||||
import { TrustRow } from "./components/TrustRow";
|
||||
@@ -116,9 +117,12 @@ export default function NotFound() {
|
||||
actual photographed/rendered note rather than recreating its
|
||||
handwriting in CSS. */}
|
||||
<Reveal delay={0.15} className="w-full lg:flex-1 flex items-center justify-center">
|
||||
<img
|
||||
<Image
|
||||
alt="Sticky Note: 404. Nicht hier. Aber dein Ziel ist sicher nicht weit."
|
||||
src="/404-sticky-note.png"
|
||||
width={600}
|
||||
height={689}
|
||||
sizes="(min-width: 1024px) 22rem, 100vw"
|
||||
className="w-full max-w-[22rem] rotate-2 hover:rotate-0 transition-transform duration-300"
|
||||
/>
|
||||
</Reveal>
|
||||
@@ -178,9 +182,12 @@ export default function NotFound() {
|
||||
the flat mockup image instead of in Figma. */}
|
||||
<Reveal className="relative w-full flex items-stretch h-[14rem] md:h-[16rem] bg-bg-muted overflow-hidden">
|
||||
<div className="relative w-full md:w-[45%] shrink-0">
|
||||
<img
|
||||
<Image
|
||||
alt=""
|
||||
src="/404-testimonial-photo.jpg"
|
||||
width={810}
|
||||
height={291}
|
||||
sizes="(min-width: 768px) 45vw, 100vw"
|
||||
className="absolute -inset-1 w-[calc(100%+0.5rem)] h-[calc(100%+0.5rem)] object-cover"
|
||||
/>
|
||||
<div className="hidden md:block absolute inset-y-0 right-0 w-40 bg-gradient-to-l from-bg-muted to-transparent" />
|
||||
|
||||
@@ -34,7 +34,7 @@ export function Focus() {
|
||||
<ul className="flex flex-col gap-3 items-start w-full">
|
||||
{bullets.map((b) => (
|
||||
<li key={b} className="flex gap-[0.625rem] items-center w-full">
|
||||
<img alt="" src="/icon-check.svg" className="size-5 shrink-0" />
|
||||
<Image alt="" src="/icon-check.svg" width={20} height={20} className="size-5 shrink-0" />
|
||||
<span className="flex-1 font-semibold text-body text-text-primary">{b}</span>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
import { Fragment } from "react";
|
||||
import Image from "next/image";
|
||||
import { Reveal, RevealGroup, RevealItem } from "../../components/Reveal";
|
||||
|
||||
// Each icon's own real pixel dimensions (not uniformly square, e.g.
|
||||
// icon-step-2 is 180x168) — needed so the `h-16 w-auto` sizing below infers
|
||||
// the correct aspect ratio instead of stretching/squashing to a guessed one.
|
||||
const steps = [
|
||||
{
|
||||
icon: "/icon-step-1.png",
|
||||
width: 165,
|
||||
height: 177,
|
||||
title: "1. Aufschreiben",
|
||||
desc: "Notiere deine Aufgaben und alles, was dir im Kopf herumgeht.",
|
||||
},
|
||||
{
|
||||
icon: "/icon-step-2.png",
|
||||
width: 180,
|
||||
height: 168,
|
||||
title: "2. Priorisieren",
|
||||
desc: "Wähle deine 3 wichtigsten Aufgaben für heute aus und schreibe sie auf.",
|
||||
},
|
||||
{
|
||||
icon: "/icon-step-3.png",
|
||||
width: 180,
|
||||
height: 177,
|
||||
title: "3. Umsetzen",
|
||||
desc: "Konzentriere dich auf das Wesentliche und hake ab, was wirklich zählt.",
|
||||
},
|
||||
@@ -47,21 +57,21 @@ export function HowItWorks() {
|
||||
{steps.map((step, i) => (
|
||||
<Fragment key={step.title}>
|
||||
<RevealItem className="group flex flex-col gap-4 items-center text-center flex-1 max-w-xs md:max-w-none">
|
||||
{/* Plain <img>, not next/image — one of the three source PNGs
|
||||
isn't square (icon-step-2 is 68.6x64), and next/image's
|
||||
required width/height props fought with Tailwind's
|
||||
`img { height: auto }` preflight reset (console warning:
|
||||
"width or height modified, but not the other"). A fixed
|
||||
h-16 + w-auto lets each icon keep its own real aspect
|
||||
ratio without that conflict — matches how every other
|
||||
small decorative icon in this codebase (Tools.tsx etc.)
|
||||
is already a plain <img>, not next/image. group-hover
|
||||
scale is a small "step acknowledges you" touch — the
|
||||
whole text column is the hover target, not just the icon,
|
||||
so it's easy to trigger without precise pointing. */}
|
||||
<img
|
||||
{/* Explicit per-icon width/height (real pixel dimensions, not
|
||||
a guessed/uniform size) — matters since one of the three
|
||||
source PNGs isn't square (icon-step-2 is 180x168); passing
|
||||
the real intrinsic size is what avoids next/image's
|
||||
"width or height modified, but not the other" warning that
|
||||
a wrong/guessed size would trigger against the `h-16
|
||||
w-auto` sizing below. group-hover scale is a small "step
|
||||
acknowledges you" touch — the whole text column is the
|
||||
hover target, not just the icon, so it's easy to trigger
|
||||
without precise pointing. */}
|
||||
<Image
|
||||
src={step.icon}
|
||||
alt=""
|
||||
width={step.width}
|
||||
height={step.height}
|
||||
className="h-16 w-auto object-contain transition-transform duration-300 group-hover:scale-110"
|
||||
/>
|
||||
<p className="font-semibold text-body text-text-primary">{step.title}</p>
|
||||
@@ -73,9 +83,11 @@ export function HowItWorks() {
|
||||
// — same margin-based centering technique as Challenge's
|
||||
// step connector, not just the same icon asset.
|
||||
<div className="flex items-center justify-center shrink-0 md:mt-[1.625rem]">
|
||||
<img
|
||||
<Image
|
||||
alt=""
|
||||
src="/icon-arrow-connector.svg"
|
||||
width={24}
|
||||
height={24}
|
||||
className="w-6 h-6 rotate-90 md:w-10 md:h-3 md:rotate-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Reveal } from "../components/Reveal";
|
||||
import { Footer } from "../components/Footer";
|
||||
import { TrustRow } from "../components/TrustRow";
|
||||
@@ -42,7 +43,7 @@ export default async function WiderrufPage() {
|
||||
{/* Static, not part of the CMS content — same reasoning as
|
||||
the Impressum/Datenschutz/AGB pages' own callout cards. */}
|
||||
<div className="bg-bg-muted flex flex-col gap-3 items-start p-6 rounded-md w-full">
|
||||
<img alt="" src="/icon-trust-leaf.png" className="size-7 object-contain" />
|
||||
<Image alt="" src="/icon-trust-leaf.png" width={28} height={28} className="size-7 object-contain" />
|
||||
<p
|
||||
className="font-semibold text-body text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
|
||||
Reference in New Issue
Block a user