Profile: About me first, Core strengths card, resume-aligned skills

- Add about[] config and AboutCard; reorder sidebar; hide duplicate bio/PDF on avatar
- Resume card uses configurable section title (Core strengths) and PDF link below
- Extend skills list to match resume; sanitize about + resume.sectionTitle

Made-with: Cursor
This commit is contained in:
ilia 2026-03-25 10:42:12 -04:00
parent 58f6088f66
commit 7bfd474b8c
9 changed files with 151 additions and 24 deletions

View File

@ -80,10 +80,15 @@ const CONFIG = {
seo: {
title: 'Ilia Dobkin — SDET & Test Automation Engineer',
description:
'Software Development Engineer in Test with deep experience in Cypress, Playwright, Selenium, CI/CD, and end-to-end test automation.',
'Software Development Engineer in Test with 20+ years across product, platform, and industrial test automation — Cypress, Playwright, Selenium, CI/CD, accessibility, and regulated domains.',
imageURL: '',
},
location: 'Thornhill, Ontario, Canada',
about: [
'Driven software engineer with 20+ years of experience spanning product, platform, and industrial test automation — from global audit and financial systems (CaseWare, MNP, JazzIt) to modern web delivery for startups and enterprises alike.',
'Builds and maintains a self-hosted infrastructure lab (Proxmox, Ansible, Caddy, CI runners) that mirrors production-grade DevOps practices.',
'Seeking roles where I can provide testing guidance, strengthen CI/CD operations, and collaborate with teams to optimize product delivery.',
],
social: {
linkedin: 'idobkin',
x: '',
@ -108,48 +113,90 @@ const CONFIG = {
},
resume: {
fileUrl: '/resume.pdf',
sectionTitle: 'Core strengths',
previewLines: [
'Driven software engineer with deep experience in product, platform, and industrial test automation systems, from audit and financial software to modern web delivery.',
'Seeking roles where I can provide testing guidance, strengthen development operations through continuous integration, and collaborate with teams to optimize product delivery.',
'',
'Core strengths: end-to-end test automation (Cypress, Playwright, Selenium), CI/CD integration, accessibility (AODA/WCAG), and frameworks built for team adoption.',
'E2E test automation (Cypress, Playwright, Selenium), BDD (SpecFlow, Cucumber), and accessibility (AODA / WCAG).',
'Observability (Grafana, Prometheus), IaC (Terraform), and API and performance testing (Postman, Artillery).',
'Reusable frameworks built for team adoption.',
],
},
skills: [
'Cypress',
'Playwright',
'Selenium',
'SilkTest',
'SpecFlow',
'Cucumber',
'Gherkin',
'BDD',
'TypeScript',
'JavaScript',
'C#',
'Python',
'Java',
'.NET',
'ASP.NET',
'Node.js',
'Spring Boot',
'PyTest',
'JUnit',
'HTML',
'CSS',
'jQuery',
'REST API',
'Page object model',
'Cross-browser testing',
'Mobile testing',
'GitHub Actions',
'GitHub',
'GitLab CI',
'Bitbucket',
'Jenkins',
'Azure DevOps',
'Bitbucket Pipelines',
'Ansible',
'Terraform',
'CI/CD',
'Self-hosted runners',
'Docker',
'Proxmox',
'AWS',
'GCP',
'AWS Lambda',
'Azure',
'GCP',
'PostgreSQL',
'MySQL',
'SQL Server',
'DB2',
'Informatica',
'ETL',
'Postman',
'Artillery',
'JMeter',
'Grafana',
'Prometheus',
'SpecFlow',
'Cucumber',
'Gherkin',
'Sentry',
'DataDog',
'AODA / WCAG',
'Agile / Scrum',
'Shift-left QA',
'Jira',
'Confluence',
'Git',
'TestRail',
'Twilio',
'WordPress',
'CaseWare',
'CaseView',
'Crystal Reports',
'Linux',
'Caddy',
'TrueNAS',
'Vaultwarden',
'Gitea',
'SonarQube',
'n8n',
'DNS',
'Local LLM / GPU',
],
experiences: [],
certifications: [],

10
global.d.ts vendored
View File

@ -217,6 +217,11 @@ interface Resume {
*/
fileUrl?: string;
/**
* Card heading (e.g. Core strengths)
*/
sectionTitle?: string;
/**
* Preview lines shown on the profile page
*/
@ -345,6 +350,11 @@ interface Config {
*/
location?: string;
/**
* About me paragraphs (shown at top of sidebar; when set, GitHub bio is hidden on the avatar card)
*/
about?: Array<string>;
/**
* Social links
*/

Binary file not shown.

View File

@ -0,0 +1,42 @@
import { skeleton } from '../../utils';
const AboutCard = ({
loading,
paragraphs,
}: {
loading: boolean;
paragraphs: string[];
}) => {
return (
<div className="card shadow-lg card-sm bg-base-100">
<div className="card-body">
<div className="mx-3">
<h5 className="card-title">
{loading ? (
skeleton({ widthCls: 'w-36', heightCls: 'h-8' })
) : (
<span className="text-base-content opacity-70">About me</span>
)}
</h5>
</div>
<div className="text-base-content text-sm mt-2 mx-3">
{loading ? (
<div className="flex flex-col gap-2">
{skeleton({ widthCls: 'w-full', heightCls: 'h-4', shape: 'rounded' })}
{skeleton({ widthCls: 'w-11/12', heightCls: 'h-4', shape: 'rounded' })}
{skeleton({ widthCls: 'w-full', heightCls: 'h-4', shape: 'rounded' })}
</div>
) : (
<div className="space-y-3 opacity-80 leading-relaxed">
{paragraphs.map((p, i) => (
<p key={i}>{p}</p>
))}
</div>
)}
</div>
</div>
</div>
);
};
export default AboutCard;

View File

@ -8,6 +8,10 @@ interface AvatarCardProps {
loading: boolean;
avatarRing: boolean;
resumeFileUrl?: string;
/** When true, omit GitHub bio (e.g. when About me is in config). */
suppressBio?: boolean;
/** When true, omit resume button (PDF is linked from Core strengths). */
suppressResumeDownload?: boolean;
}
/**
@ -23,6 +27,8 @@ const AvatarCard: React.FC<AvatarCardProps> = ({
loading,
avatarRing,
resumeFileUrl,
suppressBio = false,
suppressResumeDownload = false,
}): React.JSX.Element => {
return (
<div className="card shadow-lg card-sm bg-base-100">
@ -70,13 +76,16 @@ const AvatarCard: React.FC<AvatarCardProps> = ({
</span>
)}
</h5>
<div className="mt-3 text-base-content font-mono">
{loading || !profile
? skeleton({ widthCls: 'w-48', heightCls: 'h-5' })
: profile.bio}
</div>
{!suppressBio && (
<div className="mt-3 text-base-content font-mono">
{loading || !profile
? skeleton({ widthCls: 'w-48', heightCls: 'h-5' })
: profile.bio}
</div>
)}
</div>
{resumeFileUrl &&
!suppressResumeDownload &&
(loading ? (
<div className="mt-6">
{skeleton({ widthCls: 'w-40', heightCls: 'h-8' })}

View File

@ -16,6 +16,7 @@ import { DEFAULT_THEMES } from '../constants/default-themes';
import ThemeChanger from './theme-changer';
import { BG_COLOR } from '../constants';
import AvatarCard from './avatar-card';
import AboutCard from './about-card';
import { Profile } from '../interfaces/profile';
import DetailsCard from './details-card';
import SkillCard from './skill-card';
@ -200,11 +201,22 @@ const GitProfile = ({ config }: { config: Config }) => {
themeConfig={sanitizedConfig.themeConfig}
/>
)}
{sanitizedConfig.about.length !== 0 && (
<AboutCard
loading={loading}
paragraphs={sanitizedConfig.about}
/>
)}
<AvatarCard
profile={profile}
loading={loading}
avatarRing={sanitizedConfig.themeConfig.displayAvatarRing}
resumeFileUrl={sanitizedConfig.resume.fileUrl}
suppressBio={sanitizedConfig.about.length !== 0}
suppressResumeDownload={
Boolean(sanitizedConfig.resume.fileUrl) &&
sanitizedConfig.resume.previewLines.length !== 0
}
/>
<DetailsCard
profile={profile}
@ -212,20 +224,21 @@ const GitProfile = ({ config }: { config: Config }) => {
github={sanitizedConfig.github}
social={sanitizedConfig.social}
/>
{sanitizedConfig.resume.fileUrl &&
sanitizedConfig.resume.previewLines.length !== 0 && (
<ResumeCard
loading={loading}
fileUrl={sanitizedConfig.resume.fileUrl}
sectionTitle={sanitizedConfig.resume.sectionTitle}
previewLines={sanitizedConfig.resume.previewLines}
/>
)}
{sanitizedConfig.skills.length !== 0 && (
<SkillCard
loading={loading}
skills={sanitizedConfig.skills}
/>
)}
{sanitizedConfig.resume.fileUrl &&
sanitizedConfig.resume.previewLines.length !== 0 && (
<ResumeCard
loading={loading}
fileUrl={sanitizedConfig.resume.fileUrl}
previewLines={sanitizedConfig.resume.previewLines}
/>
)}
{sanitizedConfig.certifications.length !== 0 && (
<CertificationCard
loading={loading}

View File

@ -4,12 +4,14 @@ import { resolvePublicUrl, skeleton } from '../../utils';
interface ResumeCardProps {
loading: boolean;
fileUrl: string;
sectionTitle: string;
previewLines: string[];
}
const ResumeCard = ({
loading,
fileUrl,
sectionTitle,
previewLines,
}: ResumeCardProps) => {
const pdfPath = resolvePublicUrl(fileUrl);
@ -65,7 +67,7 @@ const ResumeCard = ({
{loading ? (
skeleton({ widthCls: 'w-32', heightCls: 'h-8' })
) : (
<span className="text-base-content opacity-70">Resume</span>
<span className="text-base-content opacity-70">{sectionTitle}</span>
)}
</h5>
</div>

View File

@ -68,6 +68,7 @@ export interface SanitizedSocial {
export interface SanitizedResume {
fileUrl?: string;
sectionTitle: string;
previewLines: Array<string>;
}
@ -131,6 +132,7 @@ export interface SanitizedConfig {
projects: SanitizedProjects;
seo: SanitizedSEO;
location?: string;
about: Array<string>;
social: SanitizedSocial;
resume: SanitizedResume;
skills: Array<string>;

View File

@ -76,6 +76,7 @@ export const getSanitizedConfig = (
imageURL: config?.seo?.imageURL,
},
location: config?.location,
about: config?.about?.filter((p) => p.trim()) || [],
social: {
linkedin: config?.social?.linkedin,
x: config?.social?.x,
@ -100,6 +101,7 @@ export const getSanitizedConfig = (
},
resume: {
fileUrl: config?.resume?.fileUrl || '',
sectionTitle: config?.resume?.sectionTitle || 'Core strengths',
previewLines: config?.resume?.previewLines || [],
},
skills: config?.skills || [],