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: {
|
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
10
global.d.ts
vendored
@ -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.
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;
|
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' })}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -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 || [],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user