mirror of
https://github.com/complexcaresolutions/frontend.porwoll.de.git
synced 2026-03-17 15:13:42 +00:00
feat: add dynamic Lucide icon rendering to CardGridBlock
Installs lucide-react and creates DynamicIcon component that maps icon name strings to rendered Lucide icons. CardGridBlock now supports mediaType (none/image/icon) and iconPosition (top/left). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fe83855f40
commit
9fe9876de2
4 changed files with 95 additions and 39 deletions
|
|
@ -12,6 +12,7 @@
|
|||
"@c2s/payload-contracts": "github:complexcaresolutions/payload-contracts",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.34.0",
|
||||
"lucide-react": "^0.564.0",
|
||||
"next": "16.0.10",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ importers:
|
|||
framer-motion:
|
||||
specifier: ^12.34.0
|
||||
version: 12.34.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
|
||||
lucide-react:
|
||||
specifier: ^0.564.0
|
||||
version: 0.564.0(react@19.2.1)
|
||||
next:
|
||||
specifier: 16.0.10
|
||||
version: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
|
||||
|
|
@ -1492,6 +1495,11 @@ packages:
|
|||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
lucide-react@0.564.0:
|
||||
resolution: {integrity: sha512-JJ8GVTQqFwuliifD48U6+h7DXEHdkhJ/E87kksGByII3qHxtPciVb8T8woQONHBQgHVOl7rSMrrip3SeVNy7Fg==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
|
|
@ -3498,6 +3506,10 @@ snapshots:
|
|||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
||||
lucide-react@0.564.0(react@19.2.1):
|
||||
dependencies:
|
||||
react: 19.2.1
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import Link from 'next/link'
|
|||
import Image from 'next/image'
|
||||
import { Container } from '../ui/Container'
|
||||
import { SectionHeader } from '../ui/SectionHeader'
|
||||
import { DynamicIcon } from '../ui/DynamicIcon'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface CardGridBlockProps {
|
||||
|
|
@ -40,13 +41,17 @@ export function CardGridBlock({ block }: CardGridBlockProps) {
|
|||
{cards.map((card, index) => {
|
||||
const cardTitle = (card.title as string) || ''
|
||||
const cardDescription = (card.description as string) || (card.text as string) || ''
|
||||
const mediaType = (card.mediaType as string) || 'none'
|
||||
const cardIcon = card.icon as string | undefined
|
||||
const iconPosition = (card.iconPosition as string) || 'top'
|
||||
const cardImage = card.image as Record<string, unknown> | undefined
|
||||
const cardImageUrl = cardImage?.url as string | undefined
|
||||
const cardLink = card.link as Record<string, unknown> | undefined
|
||||
const cardLinkLabel = (cardLink?.label as string) || 'Mehr erfahren'
|
||||
const cardLinkHref = (cardLink?.href as string) || (cardLink?.url as string) || ''
|
||||
|
||||
const isIconLeft = mediaType === 'icon' && iconPosition === 'left'
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={index}
|
||||
|
|
@ -54,57 +59,67 @@ export function CardGridBlock({ block }: CardGridBlockProps) {
|
|||
'p-[50px_40px]',
|
||||
'border border-light-soft bg-white',
|
||||
'shadow-card transition-all duration-300',
|
||||
'hover:-translate-y-[10px] hover:shadow-card-hover'
|
||||
'hover:-translate-y-[10px] hover:shadow-card-hover',
|
||||
isIconLeft && 'flex gap-6'
|
||||
)}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: index * 0.1, duration: 0.5 }}
|
||||
>
|
||||
{/* Icon */}
|
||||
{cardIcon && (
|
||||
<div className="text-[64px] text-dark mb-6">
|
||||
<span>{cardIcon}</span>
|
||||
{/* Icon left */}
|
||||
{mediaType === 'icon' && cardIcon && isIconLeft && (
|
||||
<div className="shrink-0 text-dark">
|
||||
<DynamicIcon name={cardIcon} size={48} strokeWidth={1.5} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Image */}
|
||||
{cardImageUrl && (
|
||||
<div className="mb-6 -mx-[40px] -mt-[50px]">
|
||||
<Image
|
||||
src={cardImageUrl}
|
||||
alt={cardTitle}
|
||||
width={400}
|
||||
height={250}
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={isIconLeft ? 'flex-1' : ''}>
|
||||
{/* Icon top */}
|
||||
{mediaType === 'icon' && cardIcon && !isIconLeft && (
|
||||
<div className="text-dark mb-6">
|
||||
<DynamicIcon name={cardIcon} size={64} strokeWidth={1.5} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<h3 className="font-heading font-bold text-[1.07em] tracking-[3px] uppercase text-dark mt-0 mb-[30px]">
|
||||
{cardTitle}
|
||||
</h3>
|
||||
{/* Image */}
|
||||
{mediaType === 'image' && cardImageUrl && (
|
||||
<div className="mb-6 -mx-[40px] -mt-[50px]">
|
||||
<Image
|
||||
src={cardImageUrl}
|
||||
alt={cardTitle}
|
||||
width={400}
|
||||
height={250}
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-gray leading-[1.8em] m-0">
|
||||
{cardDescription}
|
||||
</p>
|
||||
{/* Title */}
|
||||
<h3 className="font-heading font-bold text-[1.07em] tracking-[3px] uppercase text-dark mt-0 mb-[30px]">
|
||||
{cardTitle}
|
||||
</h3>
|
||||
|
||||
{/* Link */}
|
||||
{cardLinkHref && (
|
||||
<Link
|
||||
href={cardLinkHref}
|
||||
className={cn(
|
||||
'block pt-[50px] text-right',
|
||||
'font-heading text-[0.85em] tracking-[2px] uppercase',
|
||||
'text-gray-light hover:text-dark',
|
||||
'border-none transition-colors duration-500'
|
||||
)}
|
||||
>
|
||||
{cardLinkLabel} →
|
||||
</Link>
|
||||
)}
|
||||
{/* Description */}
|
||||
<p className="text-gray leading-[1.8em] m-0">
|
||||
{cardDescription}
|
||||
</p>
|
||||
|
||||
{/* Link */}
|
||||
{cardLinkHref && (
|
||||
<Link
|
||||
href={cardLinkHref}
|
||||
className={cn(
|
||||
'block pt-[50px] text-right',
|
||||
'font-heading text-[0.85em] tracking-[2px] uppercase',
|
||||
'text-gray-light hover:text-dark',
|
||||
'border-none transition-colors duration-500'
|
||||
)}
|
||||
>
|
||||
{cardLinkLabel} →
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
28
src/components/ui/DynamicIcon.tsx
Normal file
28
src/components/ui/DynamicIcon.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { icons, type LucideIcon } from 'lucide-react'
|
||||
|
||||
interface DynamicIconProps {
|
||||
name: string
|
||||
size?: number
|
||||
className?: string
|
||||
strokeWidth?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a Lucide icon by name string.
|
||||
* Names can be kebab-case ("shield-check") or PascalCase ("ShieldCheck").
|
||||
*/
|
||||
export function DynamicIcon({ name, size = 24, className, strokeWidth = 2 }: DynamicIconProps) {
|
||||
// Convert kebab-case to PascalCase: "shield-check" → "ShieldCheck"
|
||||
const pascalName = name
|
||||
.split('-')
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join('')
|
||||
|
||||
const IconComponent = icons[pascalName as keyof typeof icons] as LucideIcon | undefined
|
||||
|
||||
if (!IconComponent) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <IconComponent size={size} className={className} strokeWidth={strokeWidth} />
|
||||
}
|
||||
Loading…
Reference in a new issue