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
19 changes: 19 additions & 0 deletions .github/workflows/validate-case-studies-data.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Validate Case Studies Data

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
validate_case_studies_data:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: chrisdickinson/setup-yq@latest
with:
yq-version: 'v4.9.5'
- run: |
jsonschema -i <(yq eval --tojson -j data/case-studies/case-studies.yml) data/schemas/case-studies.json
echo "case studies data is valid!"
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,27 @@ To add an event to the Community Events, do the following:
You can see the structure and types of the expected configuration in [the JSON schema](/data/schemas/events.json).
2. Publish the changes creating a pull request. The changes will be validated by [GitHub Actions Workflow](.github/workflows/validate-events-data.yml) to prevent misconfiguration.

### Case Studies

To add a case study, do the following:
1. Fill the case study info in the [case-studies.yml](/data/case-studies/case-studies.yml) with the next:
- `id`, a unique identifier for the case study.
- `type`, the case study category: either `multiplatform` or `server-side`.
- `description`, a markdown-enabled text description of the case study (supports # header **bold** and [links](https://example.com), paragraphs are made with two new lines).
- `logo` (optional), an array of 0-2 image paths relative to the `/public/` directory.
- `signature` (optional), an object with `name` and `position` fields for the quote author.
- `isExternal` (optional), a boolean indicating if the case story is from an external source (default: false).
- `link` (optional), a URL to the full case story.
- `linkText` (optional), custom text for the link (default: "Read the full story").
- `linkStyle` (optional), either `button` or `text` for the link style.
- `platforms` (optional), an array of platform tags such as `android`, `ios`, `desktop`, `frontend`, `backend`, or `compose-multiplatform`.
- `media` (optional), a media object with `type` set to either `youtube` (with `videoId`) or `image` (with `path` relative to `/public/`).
- `featuredOnMainPage` (optional), a boolean to mark the case as featured on the main page.

You can see the structure and types of the expected configuration in [the JSON schema](/data/schemas/case-studies.json) and in the [example file](/data/case-studies/_case-study.example.yaml).
2. The order of case studies in the file defines the order of their appearance on the website. Place new case studies accordingly.
3. Publish the changes creating a pull request. The changes will be validated by [GitHub Actions Workflow](.github/workflows/validate-case-studies-data.yml) to prevent misconfiguration.

## Local deployment

Currently, there is no way to deploy the Kotlin website locally. This ticket tracks the effort of adding support for local testing: [KT-47049](https://youtrack.jetbrains.com/issue/KT-47049).
Expand Down
Binary file added assets/images/open-graph/case-studies.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 95 additions & 0 deletions blocks/case-studies/card/case-studies-card.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
.card {
border-radius: 16px;
overflow: hidden;
background: #fff;
}

.content {
--font-color: rgba(25, 25, 28, 1);
display: flex;
flex-direction: column;
padding: 32px;
gap: 24px;
color: var(--font-color);

@media (width <= 616px) {
padding: 24px;
}
}

.logos {
display: flex;
align-items: center;
gap: 32px;
}

.logo {
height: 64px;
width: auto;
object-fit: contain;
}

.mediaImage {
display: block;
width: 100%;
height: auto;
}

.description {
color: var(--font-color);
font-size: 16px;
line-height: 24px;

& h2 {
margin-top: 0;
margin-bottom: 24px;
font-size: 35px;
line-height: 42px;
font-weight: 600;
}

& p {
margin-top: 0;
margin-bottom: 16px;

&:last-child {
margin-bottom: 0;
}
}

& a {
color: var(--font-color);
}
}

.signature {
color: var(--font-color);
}

.name {
color: var(--font-color);
line-height: 24px;
font-weight: 700;
}

.position {
color: var(--font-color);
opacity: 0.6;
line-height: 24px;
}

.platforms {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px 12px;
height: 24px;
margin-left: -2px; /* visual compensation */
}

.link {
display: inline-block;
align-self: flex-start;
letter-spacing: 0.01em;
line-height: 24px;
}
83 changes: 83 additions & 0 deletions blocks/case-studies/card/case-studies-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import YoutubePlayer from '@jetbrains/kotlin-web-site-ui/out/components/youtube-player';
import { useTextStyles } from '@rescui/typography';
import React from 'react';
import cn from 'classnames';
import styles from './case-studies-card.module.css';
import { CaseItem } from '../case-studies';
import { PlatformIcon } from '../platform-icon/platform-icon';
import { mdToHtml } from '../../../utils/mdToHtml';

type CaseStudyCardProps = CaseItem & {
className?: string;
};

export const CaseStudyCard: React.FC<CaseStudyCardProps> = ({ className, ...item }) => {
const textCn = useTextStyles();
const logos = item.logo ?? [];
const logo = logos[0];
const optionalSecondLogo = logos[1];

const isYoutube = item.media?.type === 'youtube';
const videoId = item.media?.type === 'youtube' ? item.media.videoId : undefined;
const imageSrc = item.media?.type === 'image' ? item.media.path : undefined;

return (
<article className={cn(styles.card, className)}>
<div className={styles.content}>
{logo &&
<div className={styles.logos}>
<img className={styles.logo} src={logo} alt={item.id} height={64} />
{optionalSecondLogo && (
<img className={styles.logo} src={optionalSecondLogo} alt="" height={64} />
)}
</div>
}

{item.description &&
<div
className={cn(styles.description, textCn('rs-text-2'))}
dangerouslySetInnerHTML={{ __html: mdToHtml(item.description) }}
/>}

{item.signature &&
<div className={textCn('rs-text-2')}>
<strong className={styles.name}>{item.signature.name}</strong>
<div className={styles.position}>{item.signature.position}</div>
</div>
}

{item.link &&
<a
className={cn(styles.link, `ktl-text-1 rs-link ${item.isExternal ? 'rs-link_external' : ''}`)}
href={item.link}
>
{item.linkText || 'Read the full story'}
</a>
}

{item.platforms && item.platforms.length > 0 &&
<div className={styles.platforms} aria-label="Platforms">
{item.platforms.map((platform) =>
<PlatformIcon key={platform} platform={platform} />
)}
</div>
}
</div>

{item.media &&
<div className={styles.media}>
{isYoutube ?
<YoutubePlayer
className={styles.youtubePlayer}
mode={0}
id={videoId}
previewImgSrc={`https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`}
/>
:
<img className={styles.mediaImage} src={imageSrc} alt={`${item.id} case`} />
}
</div>
}
</article>
);
};
53 changes: 53 additions & 0 deletions blocks/case-studies/case-studies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export type CaseType = 'multiplatform' | 'server-side';

export type CaseTypeSwitch = 'all' | CaseType;

export const Platforms = [
'android',
'ios',
'desktop',
'frontend',
'backend',
] as const;

export const PlatformNames: Record<typeof Platforms[number], string> = {
"android": "Android",
"ios": "iOS",
"desktop": "Desktop",
"frontend": "Frontend",
"backend": "Backend",
}

export type CasePlatform = typeof Platforms[number] | 'compose-multiplatform';

type Signature = {
name: string;
position: string;
}

type YoutubeMedia = {
type: 'youtube';
videoId: string;
};

type ImageMedia = {
type: 'image';
path: string;
};

type Media = YoutubeMedia | ImageMedia;

export interface CaseItem {
id: string;
type: CaseType;
description: string;
isExternal?: boolean;
link?: string;
linkText?: string;
linkStyle?: 'button' | 'text';
logo?: string[];
signature?: Signature;
platforms?: CasePlatform[];
media?: Media;
featuredOnMainPage?: boolean;
}
57 changes: 57 additions & 0 deletions blocks/case-studies/filter/case-studies-filter.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
.content {
padding: 24px 0 72px;
display: grid;
grid-template-columns: 1fr 1fr;
grid-column-gap: 80px;
grid-template-areas:
"group1 group1"
"group2 group3";
@media (width <= 1000px) {
display: block;
grid-template-columns: unset;
grid-template-areas: unset;
}
}

.group:first-child {
grid-area: group1;
}

.group:not(:last-child) {
margin-bottom: 32px;
}

.groupTitle {
margin: 0 0 8px;
}

.checkboxes {
display: flex;
align-items: center;
gap: 8px 24px;

@media (width <= 1000px) {
flex-wrap: wrap;
}
}

.checkbox {
display: flex;
align-items: center;
}

.composeLabel {
display: flex;
gap: 8px;

@media (width <= 374px) {
display: inline;

& img {
position: relative;
top: 6px;
margin-left: 5px;
margin-top: 5px;
}
}
}
Loading