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

10
global.d.ts vendored
View File

@ -217,6 +217,11 @@ interface Resume {
*/ */
fileUrl?: string; fileUrl?: string;
/**
* Card heading (e.g. Core strengths)
*/
sectionTitle?: string;
/** /**
* Preview lines shown on the profile page * Preview lines shown on the profile page
*/ */
@ -345,6 +350,11 @@ interface Config {
*/ */
location?: string; 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 * 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; loading: boolean;
avatarRing: boolean; avatarRing: boolean;
resumeFileUrl?: string; 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, loading,
avatarRing, avatarRing,
resumeFileUrl, resumeFileUrl,
suppressBio = false,
suppressResumeDownload = false,
}): React.JSX.Element => { }): React.JSX.Element => {
return ( return (
<div className="card shadow-lg card-sm bg-base-100"> <div className="card shadow-lg card-sm bg-base-100">
@ -70,13 +76,16 @@ const AvatarCard: React.FC<AvatarCardProps> = ({
</span> </span>
)} )}
</h5> </h5>
{!suppressBio && (
<div className="mt-3 text-base-content font-mono"> <div className="mt-3 text-base-content font-mono">
{loading || !profile {loading || !profile
? skeleton({ widthCls: 'w-48', heightCls: 'h-5' }) ? skeleton({ widthCls: 'w-48', heightCls: 'h-5' })
: profile.bio} : profile.bio}
</div> </div>
)}
</div> </div>
{resumeFileUrl && {resumeFileUrl &&
!suppressResumeDownload &&
(loading ? ( (loading ? (
<div className="mt-6"> <div className="mt-6">
{skeleton({ widthCls: 'w-40', heightCls: 'h-8' })} {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 ThemeChanger from './theme-changer';
import { BG_COLOR } from '../constants'; import { BG_COLOR } from '../constants';
import AvatarCard from './avatar-card'; import AvatarCard from './avatar-card';
import AboutCard from './about-card';
import { Profile } from '../interfaces/profile'; import { Profile } from '../interfaces/profile';
import DetailsCard from './details-card'; import DetailsCard from './details-card';
import SkillCard from './skill-card'; import SkillCard from './skill-card';
@ -200,11 +201,22 @@ const GitProfile = ({ config }: { config: Config }) => {
themeConfig={sanitizedConfig.themeConfig} themeConfig={sanitizedConfig.themeConfig}
/> />
)} )}
{sanitizedConfig.about.length !== 0 && (
<AboutCard
loading={loading}
paragraphs={sanitizedConfig.about}
/>
)}
<AvatarCard <AvatarCard
profile={profile} profile={profile}
loading={loading} loading={loading}
avatarRing={sanitizedConfig.themeConfig.displayAvatarRing} avatarRing={sanitizedConfig.themeConfig.displayAvatarRing}
resumeFileUrl={sanitizedConfig.resume.fileUrl} resumeFileUrl={sanitizedConfig.resume.fileUrl}
suppressBio={sanitizedConfig.about.length !== 0}
suppressResumeDownload={
Boolean(sanitizedConfig.resume.fileUrl) &&
sanitizedConfig.resume.previewLines.length !== 0
}
/> />
<DetailsCard <DetailsCard
profile={profile} profile={profile}
@ -212,20 +224,21 @@ const GitProfile = ({ config }: { config: Config }) => {
github={sanitizedConfig.github} github={sanitizedConfig.github}
social={sanitizedConfig.social} social={sanitizedConfig.social}
/> />
{sanitizedConfig.skills.length !== 0 && (
<SkillCard
loading={loading}
skills={sanitizedConfig.skills}
/>
)}
{sanitizedConfig.resume.fileUrl && {sanitizedConfig.resume.fileUrl &&
sanitizedConfig.resume.previewLines.length !== 0 && ( sanitizedConfig.resume.previewLines.length !== 0 && (
<ResumeCard <ResumeCard
loading={loading} loading={loading}
fileUrl={sanitizedConfig.resume.fileUrl} fileUrl={sanitizedConfig.resume.fileUrl}
sectionTitle={sanitizedConfig.resume.sectionTitle}
previewLines={sanitizedConfig.resume.previewLines} previewLines={sanitizedConfig.resume.previewLines}
/> />
)} )}
{sanitizedConfig.skills.length !== 0 && (
<SkillCard
loading={loading}
skills={sanitizedConfig.skills}
/>
)}
{sanitizedConfig.certifications.length !== 0 && ( {sanitizedConfig.certifications.length !== 0 && (
<CertificationCard <CertificationCard
loading={loading} loading={loading}

View File

@ -4,12 +4,14 @@ import { resolvePublicUrl, skeleton } from '../../utils';
interface ResumeCardProps { interface ResumeCardProps {
loading: boolean; loading: boolean;
fileUrl: string; fileUrl: string;
sectionTitle: string;
previewLines: string[]; previewLines: string[];
} }
const ResumeCard = ({ const ResumeCard = ({
loading, loading,
fileUrl, fileUrl,
sectionTitle,
previewLines, previewLines,
}: ResumeCardProps) => { }: ResumeCardProps) => {
const pdfPath = resolvePublicUrl(fileUrl); const pdfPath = resolvePublicUrl(fileUrl);
@ -65,7 +67,7 @@ const ResumeCard = ({
{loading ? ( {loading ? (
skeleton({ widthCls: 'w-32', heightCls: 'h-8' }) 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> </h5>
</div> </div>

View File

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

View File

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