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:
parent
58f6088f66
commit
7bfd474b8c
@ -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
10
global.d.ts
vendored
@ -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.
42
src/components/about-card/index.tsx
Normal file
42
src/components/about-card/index.tsx
Normal 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;
|
||||
@ -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>
|
||||
{!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' })}
|
||||
|
||||
@ -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.skills.length !== 0 && (
|
||||
<SkillCard
|
||||
loading={loading}
|
||||
skills={sanitizedConfig.skills}
|
||||
/>
|
||||
)}
|
||||
{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.certifications.length !== 0 && (
|
||||
<CertificationCard
|
||||
loading={loading}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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 || [],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user