diff --git a/blocks/case-studies/card/case-studies-card.module.css b/blocks/case-studies/card/case-studies-card.module.css new file mode 100644 index 00000000000..1e1c189812d --- /dev/null +++ b/blocks/case-studies/card/case-studies-card.module.css @@ -0,0 +1,172 @@ +.card { + display: flex; + flex-direction: column; + gap: 16px; + padding: 20px; + border-radius: 12px; + background: #fff; + box-shadow: 0 2px 8px rgba(0,0,0,0.06); + border: 1px solid rgba(0,0,0,0.12); + max-width: 500px; + box-sizing: border-box; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.badge { + font-size: 12px; + font-weight: 600; + padding: 6px 10px; + border-radius: 999px; + line-height: 1; + white-space: nowrap; +} + +.badgeMultiplatform { + background: #eef7ff; + color: #1565c0; + border: 1px solid rgba(21,101,192,0.15); +} + +.badgeServerSide { + background: #eef9f2; + color: #1b5e20; + border: 1px solid rgba(27,94,32,0.15); +} + +.logos { + display: flex; + align-items: center; + gap: 10px; +} + +.logosDouble {} + +.logo { + height: 28px; + width: auto; + object-fit: contain; + max-width: 140px; + opacity: 0.95; +} + +.logoSecond { + border-left: 1px solid rgba(0,0,0,0.06); + padding-left: 10px; +} + +.media { + border-radius: 10px; + overflow: hidden; + background: #f6f7f9; +} + +.mediaVideo { + position: relative; + padding-bottom: 56.25%; + height: 0; +} + +.iframe { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} + +.mediaImage { + width: 100%; + height: auto; + display: block; +} + +.description { + color: #1f2328; + font-size: 16px; + line-height: 1.6; +} +.description :global(a) { + color: #0b65c2; + text-decoration: underline; +} +.description :global(strong) { + font-weight: 700; +} + +.signature { + margin-top: 4px; + margin-bottom: 24px; +} +.signatureLine1 { + font-weight: 600; + color: #101214; +} +.signatureLine2 { + color: #555c63; +} + +.platforms { + display: flex; + flex-wrap: wrap; + gap: 10px 14px; + align-items: center; +} + +.platform { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border: 1px solid rgba(0,0,0,0.06); + border-radius: 999px; + background: #fafbfc; + color: #2b2f35; + font-size: 13px; + line-height: 1; +} + +.platformIcon { + width: 16px; + height: 16px; + object-fit: contain; +} + +.platformLabel { + text-transform: capitalize; +} + +.actions { + display: flex; + gap: 12px; + align-items: center; + flex-wrap: wrap; +} + +.link { + color: #0b65c2; + text-decoration: underline; + font-weight: 600; +} + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px 14px; + border-radius: 8px; + border: 1px solid #0b65c2; + color: #0b65c2; + font-weight: 600; + text-decoration: none; + transition: all 0.15s ease; +} + +.button:hover { + background: #0b65c2; + color: #fff; +} diff --git a/blocks/case-studies/card/case-studies-card.tsx b/blocks/case-studies/card/case-studies-card.tsx new file mode 100644 index 00000000000..6fb83d50c8b --- /dev/null +++ b/blocks/case-studies/card/case-studies-card.tsx @@ -0,0 +1,239 @@ +import React from 'react'; +import cn from 'classnames'; +import styles from './case-studies-card.module.css'; +import { AndroidIcon, AppleIcon, ServerIcon, ComputerIcon, GlobusIcon } from '@rescui/icons'; +import { CaseStudyItem, CaseStudyType, isExternalCaseStudy, CasePlatform } from '../case-studies'; + + +/** + * Resolve asset path from YAML: + * - http/https or path starting with "/" => return as is + * - otherwise => treat as a filename under /images/case-studies/ + */ +const resolveAssetPath = (v?: string) => { + if (!v) return ''; + return `/images/case-studies/${v}`; +}; + +/** + * Very small markdown -> HTML for bold (**text**) and links [text](url). + * Safe subset suitable for our content. Extend if needed. + */ +const mdToHtml = (md: string) => { + // escape basic HTML first + const esc = md + .replace(/&/g, '&') + .replace(//g, '>'); + + // bold: **text** + const withBold = esc.replace(/\*\*(.+?)\*\*/g, '$1'); + + // links: [text](url) + const withLinks = withBold.replace( + /\[([^\]]+?)\]\((https?:\/\/[^\s)]+)\)/g, + '$1' + ); + + return withLinks; +}; + +const badgeText: Record = { + 'multiplatform': 'Kotlin Multiplatform', + 'server-side': 'Server-side' +}; + +const badgeClass: Record = { + 'multiplatform': 'badgeMultiplatform', + 'server-side': 'badgeServerSide' +}; + +// Platform icon path builder. If you keep icons in (for example) /images/platforms/*.svg, +// they’ll be resolved automatically by key. If an icon is missing, we still render the label. +const getPlatformIcon = (p: CasePlatform) => { + switch (p) { + case 'android': + return ; + case 'ios': + return ; + case 'desktop': + return ; + case 'frontend': + return ; + case 'backend': + return ; + case 'compose-multiplatform': + return Compose Multiplatform icon hideBrokenIcon(e.currentTarget)} />; + default: + return null; + } +}; + +type Props = { + item: CaseStudyItem; + className?: string; +}; + +export const CaseStudyCard: React.FC = ({ item, className }) => { + const logos = item.logo ?? []; + const logoSrc1 = resolveAssetPath(logos[0]); + const logoSrc2 = resolveAssetPath(logos[1]); + + const hasMedia = Boolean(item.media); + const isYoutube = item.media?.type === 'youtube'; + const mediaUrl = item.media?.type === 'youtube' ? item.media.url : undefined; + const mediaImgSrc = item.media?.type === 'image' ? resolveAssetPath(item.media.path) : undefined; + + return ( +
+ {/* Header: type badge and logos (0–2) */} +
+ + {badgeText[item.type]} + + + {(logoSrc1 || logoSrc2) && ( +
+ {logoSrc1 && ( + Logo + )} + {logoSrc2 && ( + Second logo + )} +
+ )} +
+ + {/* Media (optional): YouTube or Image */} + {hasMedia && ( +
+ {isYoutube && mediaUrl && ( +
+