mirror of
https://github.com/complexcaresolutions/frontend.porwoll.de.git
synced 2026-03-17 18:43: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",
|
"@c2s/payload-contracts": "github:complexcaresolutions/payload-contracts",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^12.34.0",
|
"framer-motion": "^12.34.0",
|
||||||
|
"lucide-react": "^0.564.0",
|
||||||
"next": "16.0.10",
|
"next": "16.0.10",
|
||||||
"react": "19.2.1",
|
"react": "19.2.1",
|
||||||
"react-dom": "19.2.1",
|
"react-dom": "19.2.1",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ importers:
|
||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^12.34.0
|
specifier: ^12.34.0
|
||||||
version: 12.34.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
|
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:
|
next:
|
||||||
specifier: 16.0.10
|
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)
|
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:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
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:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
|
|
@ -3498,6 +3506,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
|
|
||||||
|
lucide-react@0.564.0(react@19.2.1):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.1
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import Link from 'next/link'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { Container } from '../ui/Container'
|
import { Container } from '../ui/Container'
|
||||||
import { SectionHeader } from '../ui/SectionHeader'
|
import { SectionHeader } from '../ui/SectionHeader'
|
||||||
|
import { DynamicIcon } from '../ui/DynamicIcon'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
interface CardGridBlockProps {
|
interface CardGridBlockProps {
|
||||||
|
|
@ -40,13 +41,17 @@ export function CardGridBlock({ block }: CardGridBlockProps) {
|
||||||
{cards.map((card, index) => {
|
{cards.map((card, index) => {
|
||||||
const cardTitle = (card.title as string) || ''
|
const cardTitle = (card.title as string) || ''
|
||||||
const cardDescription = (card.description as string) || (card.text 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 cardIcon = card.icon as string | undefined
|
||||||
|
const iconPosition = (card.iconPosition as string) || 'top'
|
||||||
const cardImage = card.image as Record<string, unknown> | undefined
|
const cardImage = card.image as Record<string, unknown> | undefined
|
||||||
const cardImageUrl = cardImage?.url as string | undefined
|
const cardImageUrl = cardImage?.url as string | undefined
|
||||||
const cardLink = card.link as Record<string, unknown> | undefined
|
const cardLink = card.link as Record<string, unknown> | undefined
|
||||||
const cardLinkLabel = (cardLink?.label as string) || 'Mehr erfahren'
|
const cardLinkLabel = (cardLink?.label as string) || 'Mehr erfahren'
|
||||||
const cardLinkHref = (cardLink?.href as string) || (cardLink?.url as string) || ''
|
const cardLinkHref = (cardLink?.href as string) || (cardLink?.url as string) || ''
|
||||||
|
|
||||||
|
const isIconLeft = mediaType === 'icon' && iconPosition === 'left'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={index}
|
key={index}
|
||||||
|
|
@ -54,22 +59,31 @@ export function CardGridBlock({ block }: CardGridBlockProps) {
|
||||||
'p-[50px_40px]',
|
'p-[50px_40px]',
|
||||||
'border border-light-soft bg-white',
|
'border border-light-soft bg-white',
|
||||||
'shadow-card transition-all duration-300',
|
'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 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ delay: index * 0.1, duration: 0.5 }}
|
transition={{ delay: index * 0.1, duration: 0.5 }}
|
||||||
>
|
>
|
||||||
{/* Icon */}
|
{/* Icon left */}
|
||||||
{cardIcon && (
|
{mediaType === 'icon' && cardIcon && isIconLeft && (
|
||||||
<div className="text-[64px] text-dark mb-6">
|
<div className="shrink-0 text-dark">
|
||||||
<span>{cardIcon}</span>
|
<DynamicIcon name={cardIcon} size={48} strokeWidth={1.5} />
|
||||||
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Image */}
|
{/* Image */}
|
||||||
{cardImageUrl && (
|
{mediaType === 'image' && cardImageUrl && (
|
||||||
<div className="mb-6 -mx-[40px] -mt-[50px]">
|
<div className="mb-6 -mx-[40px] -mt-[50px]">
|
||||||
<Image
|
<Image
|
||||||
src={cardImageUrl}
|
src={cardImageUrl}
|
||||||
|
|
@ -105,6 +119,7 @@ export function CardGridBlock({ block }: CardGridBlockProps) {
|
||||||
{cardLinkLabel} →
|
{cardLinkLabel} →
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</motion.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