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:
ilia 2026-03-25 10:58:59 -04:00
parent 7bfd474b8c
commit 3b153fc63d
8 changed files with 120 additions and 47 deletions

View File

@ -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
View File

@ -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
*/ */

View File

@ -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
} }
/> />

View File

@ -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 && (

View File

@ -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>

View File

@ -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>

View File

@ -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>;

View File

@ -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) =>