UX: theme first, remove contact CTA buttons, Git label, skills preview
- Restore theme switcher to top of sidebar; drop LinkedIn/Email/Resume button row - github.showInDetails, websiteLabel Git, skillsPreviewLimit, shorter about - Details list links without bogus anchors; resume card view+download; footer Git Made-with: Cursor
This commit is contained in:
parent
7bfd474b8c
commit
3b153fc63d
@ -3,6 +3,8 @@
|
|||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
github: {
|
github: {
|
||||||
username: 'IliaDobkin',
|
username: 'IliaDobkin',
|
||||||
|
/** Keep username for avatar/name API; hide empty public GitHub in the contact card. */
|
||||||
|
showInDetails: false,
|
||||||
},
|
},
|
||||||
base: '/',
|
base: '/',
|
||||||
projects: {
|
projects: {
|
||||||
@ -31,26 +33,28 @@ const CONFIG = {
|
|||||||
{
|
{
|
||||||
title: 'atlas',
|
title: 'atlas',
|
||||||
description:
|
description:
|
||||||
'Python project for infrastructure and tooling.',
|
'Python tooling for infrastructure operations, glue scripts, and automation helpers.',
|
||||||
language: 'Python',
|
language: 'Python',
|
||||||
link: 'https://git.levkin.ca/ilia/atlas',
|
link: 'https://git.levkin.ca/ilia/atlas',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'ansible',
|
title: 'ansible',
|
||||||
description:
|
description:
|
||||||
'Infrastructure as code — Ansible playbooks and roles for provisioning and configuration.',
|
'Infrastructure as code — playbooks and roles for provisioning, configuration, and repeatable environments.',
|
||||||
language: 'Ansible',
|
language: 'Ansible',
|
||||||
link: 'https://git.levkin.ca/ilia/ansible',
|
link: 'https://git.levkin.ca/ilia/ansible',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'POTE',
|
title: 'POTE',
|
||||||
description: 'Python project.',
|
description:
|
||||||
|
'Python utilities and experiments — small tools and libraries for day-to-day engineering work.',
|
||||||
language: 'Python',
|
language: 'Python',
|
||||||
link: 'https://git.levkin.ca/ilia/POTE',
|
link: 'https://git.levkin.ca/ilia/POTE',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'mirror_match',
|
title: 'mirror_match',
|
||||||
description: 'TypeScript application.',
|
description:
|
||||||
|
'TypeScript app — UI and logic for a focused domain problem; structured for testability.',
|
||||||
language: 'TypeScript',
|
language: 'TypeScript',
|
||||||
link: 'https://git.levkin.ca/ilia/mirror_match',
|
link: 'https://git.levkin.ca/ilia/mirror_match',
|
||||||
},
|
},
|
||||||
@ -70,7 +74,8 @@ const CONFIG = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'crkl',
|
title: 'crkl',
|
||||||
description: 'Kotlin application.',
|
description:
|
||||||
|
'Kotlin application — JVM/Android-oriented codebase and patterns.',
|
||||||
language: 'Kotlin',
|
language: 'Kotlin',
|
||||||
link: 'https://git.levkin.ca/ilia/crkl',
|
link: 'https://git.levkin.ca/ilia/crkl',
|
||||||
},
|
},
|
||||||
@ -80,14 +85,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 20+ years across product, platform, and industrial test automation — Cypress, Playwright, Selenium, CI/CD, accessibility, and regulated domains.',
|
'Software Development Engineer in Test with 20+ years across product, platform, and industrial test automation — deep recent experience in regulated iGaming, Cypress, Playwright, Selenium, CI/CD, and accessibility.',
|
||||||
imageURL: '',
|
imageURL: '',
|
||||||
},
|
},
|
||||||
location: 'Thornhill, Ontario, Canada',
|
location: 'Thornhill, Ontario, Canada',
|
||||||
about: [
|
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.',
|
'20+ years in product, platform, and industrial test automation — financial and audit software (CaseWare, MNP, JazzIt), regulated iGaming, and modern web for startups and enterprises.',
|
||||||
'Builds and maintains a self-hosted infrastructure lab (Proxmox, Ansible, Caddy, CI runners) that mirrors production-grade DevOps practices.',
|
'Recent depth in iGaming: operator-facing casino platforms, payments and wallets, responsible gaming and compliance (geo/market rules, age gating, audit-friendly traceability). Automation with Playwright, API and integration tests, performance suites, and CI/CD on PostgreSQL and GCP.',
|
||||||
'Seeking roles where I can provide testing guidance, strengthen CI/CD operations, and collaborate with teams to optimize product delivery.',
|
'Self-hosted lab (Proxmox, Ansible, Caddy, CI runners) — same discipline I bring to delivery, observability, and reliability at work.',
|
||||||
|
'Looking for roles where I shape test strategy, strengthen CI/CD, and help teams ship in regulated, high-availability environments.',
|
||||||
],
|
],
|
||||||
social: {
|
social: {
|
||||||
linkedin: 'idobkin',
|
linkedin: 'idobkin',
|
||||||
@ -108,6 +114,7 @@ const CONFIG = {
|
|||||||
discord: '',
|
discord: '',
|
||||||
telegram: '',
|
telegram: '',
|
||||||
website: 'https://git.levkin.ca',
|
website: 'https://git.levkin.ca',
|
||||||
|
websiteLabel: 'Git',
|
||||||
phone: '647 987 2792',
|
phone: '647 987 2792',
|
||||||
email: 'idobkin@gmail.com',
|
email: 'idobkin@gmail.com',
|
||||||
},
|
},
|
||||||
@ -148,7 +155,6 @@ const CONFIG = {
|
|||||||
'Cross-browser testing',
|
'Cross-browser testing',
|
||||||
'Mobile testing',
|
'Mobile testing',
|
||||||
'GitHub Actions',
|
'GitHub Actions',
|
||||||
'GitHub',
|
|
||||||
'GitLab CI',
|
'GitLab CI',
|
||||||
'Bitbucket',
|
'Bitbucket',
|
||||||
'Jenkins',
|
'Jenkins',
|
||||||
@ -198,6 +204,7 @@ const CONFIG = {
|
|||||||
'DNS',
|
'DNS',
|
||||||
'Local LLM / GPU',
|
'Local LLM / GPU',
|
||||||
],
|
],
|
||||||
|
skillsPreviewLimit: 16,
|
||||||
experiences: [],
|
experiences: [],
|
||||||
certifications: [],
|
certifications: [],
|
||||||
educations: [],
|
educations: [],
|
||||||
@ -212,7 +219,7 @@ const CONFIG = {
|
|||||||
},
|
},
|
||||||
hotjar: { id: '', snippetVersion: 6 },
|
hotjar: { id: '', snippetVersion: 6 },
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
defaultTheme: 'light',
|
defaultTheme: 'dark',
|
||||||
|
|
||||||
disableSwitch: false,
|
disableSwitch: false,
|
||||||
|
|
||||||
@ -227,7 +234,7 @@ const CONFIG = {
|
|||||||
class="text-primary" href="https://git.levkin.ca"
|
class="text-primary" href="https://git.levkin.ca"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>Gitea</a> · <a
|
>Git</a> · <a
|
||||||
class="text-primary" href="https://www.linkedin.com/in/idobkin/"
|
class="text-primary" href="https://www.linkedin.com/in/idobkin/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
|
|||||||
17
global.d.ts
vendored
17
global.d.ts
vendored
@ -1,8 +1,13 @@
|
|||||||
interface Github {
|
interface Github {
|
||||||
/**
|
/**
|
||||||
* GitHub org/user name
|
* GitHub org/user name (used for profile API: avatar, name)
|
||||||
*/
|
*/
|
||||||
username: string;
|
username: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show GitHub row in the contact/details card (default true)
|
||||||
|
*/
|
||||||
|
showInDetails?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GitHubProjects {
|
interface GitHubProjects {
|
||||||
@ -190,6 +195,11 @@ interface Social {
|
|||||||
*/
|
*/
|
||||||
website?: string;
|
website?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Label for the website row (default "Website"), e.g. "Git" for a Gitea host
|
||||||
|
*/
|
||||||
|
websiteLabel?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Telegram username
|
* Telegram username
|
||||||
*/
|
*/
|
||||||
@ -365,6 +375,11 @@ interface Config {
|
|||||||
*/
|
*/
|
||||||
skills?: Array<string>;
|
skills?: Array<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How many skill badges to show before "Show all" (default 16)
|
||||||
|
*/
|
||||||
|
skillsPreviewLimit?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Experience list
|
* Experience list
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -77,14 +77,18 @@ const ListItem: React.FC<{
|
|||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<a
|
{link ? (
|
||||||
href={link}
|
<a
|
||||||
target="_blank"
|
href={link}
|
||||||
rel="noreferrer"
|
target="_blank"
|
||||||
className="flex justify-start py-2 px-1 items-center"
|
rel="noreferrer"
|
||||||
>
|
className="link link-primary link-hover inline-flex justify-end py-1"
|
||||||
{value}
|
>
|
||||||
</a>
|
{value}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className="inline-block py-1 text-right">{value}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -147,7 +151,7 @@ const OrganizationItem: React.FC<{
|
|||||||
* @param {Object} profile - The profile object.
|
* @param {Object} profile - The profile object.
|
||||||
* @param {boolean} loading - Indicates whether the data is loading.
|
* @param {boolean} loading - Indicates whether the data is loading.
|
||||||
* @param {Object} social - The social object.
|
* @param {Object} social - The social object.
|
||||||
* @param {Object} github - The GitHub object.
|
* @param {Object} github - Username API source; showInDetails controls GitHub row visibility.
|
||||||
* @return {JSX.Element} The details card component.
|
* @return {JSX.Element} The details card component.
|
||||||
*/
|
*/
|
||||||
const DetailsCard = ({ profile, loading, social, github }: Props) => {
|
const DetailsCard = ({ profile, loading, social, github }: Props) => {
|
||||||
@ -195,12 +199,14 @@ const DetailsCard = ({ profile, loading, social, github }: Props) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ListItem
|
{github.showInDetails && (
|
||||||
icon={<AiFillGithub />}
|
<ListItem
|
||||||
title="GitHub:"
|
icon={<AiFillGithub />}
|
||||||
value={github.username}
|
title="GitHub:"
|
||||||
link={`https://github.com/${github.username}`}
|
value={github.username}
|
||||||
/>
|
link={`https://github.com/${github.username}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{social?.researchGate && (
|
{social?.researchGate && (
|
||||||
<ListItem
|
<ListItem
|
||||||
icon={<SiResearchgate />}
|
icon={<SiResearchgate />}
|
||||||
@ -324,13 +330,13 @@ const DetailsCard = ({ profile, loading, social, github }: Props) => {
|
|||||||
{social?.website && (
|
{social?.website && (
|
||||||
<ListItem
|
<ListItem
|
||||||
icon={<FaGlobe />}
|
icon={<FaGlobe />}
|
||||||
title="Website:"
|
title={`${social.websiteLabel}:`}
|
||||||
value={social.website
|
value={social.website
|
||||||
.replace('https://', '')
|
.replace('https://', '')
|
||||||
.replace('http://', '')}
|
.replace('http://', '')}
|
||||||
link={
|
link={
|
||||||
!social.website.startsWith('http')
|
!social.website.startsWith('http')
|
||||||
? `http://${social.website}`
|
? `https://${social.website}`
|
||||||
: social.website
|
: social.website
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -201,12 +201,6 @@ 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}
|
||||||
@ -224,6 +218,12 @@ const GitProfile = ({ config }: { config: Config }) => {
|
|||||||
github={sanitizedConfig.github}
|
github={sanitizedConfig.github}
|
||||||
social={sanitizedConfig.social}
|
social={sanitizedConfig.social}
|
||||||
/>
|
/>
|
||||||
|
{sanitizedConfig.about.length !== 0 && (
|
||||||
|
<AboutCard
|
||||||
|
loading={loading}
|
||||||
|
paragraphs={sanitizedConfig.about}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{sanitizedConfig.resume.fileUrl &&
|
{sanitizedConfig.resume.fileUrl &&
|
||||||
sanitizedConfig.resume.previewLines.length !== 0 && (
|
sanitizedConfig.resume.previewLines.length !== 0 && (
|
||||||
<ResumeCard
|
<ResumeCard
|
||||||
@ -237,6 +237,7 @@ const GitProfile = ({ config }: { config: Config }) => {
|
|||||||
<SkillCard
|
<SkillCard
|
||||||
loading={loading}
|
loading={loading}
|
||||||
skills={sanitizedConfig.skills}
|
skills={sanitizedConfig.skills}
|
||||||
|
previewLimit={sanitizedConfig.skillsPreviewLimit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{sanitizedConfig.certifications.length !== 0 && (
|
{sanitizedConfig.certifications.length !== 0 && (
|
||||||
|
|||||||
@ -86,15 +86,25 @@ const ResumeCard = ({
|
|||||||
<p key={i}>{line}</p>
|
<p key={i}>{line}</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<a
|
<div className="flex flex-wrap gap-2 mt-4">
|
||||||
href={pdfAbsoluteUrl}
|
<a
|
||||||
target="_blank"
|
href={pdfAbsoluteUrl}
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
className="btn btn-outline btn-sm text-xs mt-4 opacity-70 hover:opacity-100 transition-opacity"
|
rel="noopener noreferrer"
|
||||||
onClick={openPdfInNewTab}
|
className="btn btn-outline btn-sm text-xs opacity-70 hover:opacity-100 transition-opacity"
|
||||||
>
|
onClick={openPdfInNewTab}
|
||||||
View Full Resume (PDF)
|
>
|
||||||
</a>
|
View Full Resume (PDF)
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={pdfAbsoluteUrl}
|
||||||
|
download
|
||||||
|
className="btn btn-outline btn-sm text-xs opacity-70 hover:opacity-100 transition-opacity"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Download Resume
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,12 +1,19 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { skeleton } from '../../utils';
|
import { skeleton } from '../../utils';
|
||||||
|
|
||||||
const SkillCard = ({
|
const SkillCard = ({
|
||||||
loading,
|
loading,
|
||||||
skills,
|
skills,
|
||||||
|
previewLimit,
|
||||||
}: {
|
}: {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
skills: string[];
|
skills: string[];
|
||||||
|
previewLimit: number;
|
||||||
}) => {
|
}) => {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const hasMore = skills.length > previewLimit;
|
||||||
|
const visible =
|
||||||
|
!hasMore || expanded ? skills : skills.slice(0, previewLimit);
|
||||||
const renderSkeleton = () => {
|
const renderSkeleton = () => {
|
||||||
const array = [];
|
const array = [];
|
||||||
for (let index = 0; index < 12; index++) {
|
for (let index = 0; index < 12; index++) {
|
||||||
@ -28,20 +35,38 @@ const SkillCard = ({
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
skeleton({ widthCls: 'w-32', heightCls: 'h-8' })
|
skeleton({ widthCls: 'w-32', heightCls: 'h-8' })
|
||||||
) : (
|
) : (
|
||||||
<span className="text-base-content opacity-70">Tech Stack</span>
|
<span className="text-base-content opacity-70">Tech stack</span>
|
||||||
)}
|
)}
|
||||||
</h5>
|
</h5>
|
||||||
|
{!loading && hasMore && !expanded && (
|
||||||
|
<p className="text-xs text-base-content/50 mx-3 mt-1">
|
||||||
|
Showing {previewLimit} of {skills.length}.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 flow-root">
|
<div className="p-3 flow-root">
|
||||||
<div className="-m-1 flex flex-wrap justify-center gap-2">
|
<div className="-m-1 flex flex-wrap justify-center gap-2">
|
||||||
{loading
|
{loading
|
||||||
? renderSkeleton()
|
? renderSkeleton()
|
||||||
: skills.map((skill, index) => (
|
: visible.map((skill, index) => (
|
||||||
<div key={index} className="badge badge-primary badge-sm">
|
<div key={index} className="badge badge-primary badge-sm">
|
||||||
{skill}
|
{skill}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{!loading && hasMore && (
|
||||||
|
<div className="flex justify-center mt-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost btn-sm text-xs"
|
||||||
|
onClick={() => setExpanded((e) => !e)}
|
||||||
|
>
|
||||||
|
{expanded
|
||||||
|
? 'Show fewer skills'
|
||||||
|
: `Show all ${skills.length} skills`}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
export interface SanitizedGithub {
|
export interface SanitizedGithub {
|
||||||
username: string;
|
username: string;
|
||||||
|
showInDetails: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SanitizedGitHubProjects {
|
export interface SanitizedGitHubProjects {
|
||||||
@ -60,6 +61,7 @@ export interface SanitizedSocial {
|
|||||||
dev?: string;
|
dev?: string;
|
||||||
stackoverflow?: string;
|
stackoverflow?: string;
|
||||||
website?: string;
|
website?: string;
|
||||||
|
websiteLabel: string;
|
||||||
telegram?: string;
|
telegram?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
@ -136,6 +138,7 @@ export interface SanitizedConfig {
|
|||||||
social: SanitizedSocial;
|
social: SanitizedSocial;
|
||||||
resume: SanitizedResume;
|
resume: SanitizedResume;
|
||||||
skills: Array<string>;
|
skills: Array<string>;
|
||||||
|
skillsPreviewLimit: number;
|
||||||
experiences: Array<SanitizedExperience>;
|
experiences: Array<SanitizedExperience>;
|
||||||
educations: Array<SanitizedEducation>;
|
educations: Array<SanitizedEducation>;
|
||||||
certifications: Array<SanitizedCertification>;
|
certifications: Array<SanitizedCertification>;
|
||||||
|
|||||||
@ -45,6 +45,7 @@ export const getSanitizedConfig = (
|
|||||||
return {
|
return {
|
||||||
github: {
|
github: {
|
||||||
username: config.github.username,
|
username: config.github.username,
|
||||||
|
showInDetails: config.github.showInDetails !== false,
|
||||||
},
|
},
|
||||||
projects: {
|
projects: {
|
||||||
github: {
|
github: {
|
||||||
@ -93,6 +94,7 @@ export const getSanitizedConfig = (
|
|||||||
dev: config?.social?.dev,
|
dev: config?.social?.dev,
|
||||||
stackoverflow: config?.social?.stackoverflow,
|
stackoverflow: config?.social?.stackoverflow,
|
||||||
website: config?.social?.website,
|
website: config?.social?.website,
|
||||||
|
websiteLabel: config?.social?.websiteLabel || 'Website',
|
||||||
phone: config?.social?.phone,
|
phone: config?.social?.phone,
|
||||||
email: config?.social?.email,
|
email: config?.social?.email,
|
||||||
telegram: config?.social?.telegram,
|
telegram: config?.social?.telegram,
|
||||||
@ -105,6 +107,10 @@ export const getSanitizedConfig = (
|
|||||||
previewLines: config?.resume?.previewLines || [],
|
previewLines: config?.resume?.previewLines || [],
|
||||||
},
|
},
|
||||||
skills: config?.skills || [],
|
skills: config?.skills || [],
|
||||||
|
skillsPreviewLimit: Math.max(
|
||||||
|
8,
|
||||||
|
Math.min(64, config?.skillsPreviewLimit ?? 16),
|
||||||
|
),
|
||||||
experiences:
|
experiences:
|
||||||
config?.experiences?.filter(
|
config?.experiences?.filter(
|
||||||
(experience) =>
|
(experience) =>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user