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 = {
github: {
username: 'IliaDobkin',
/** Keep username for avatar/name API; hide empty public GitHub in the contact card. */
showInDetails: false,
},
base: '/',
projects: {
@ -31,26 +33,28 @@ const CONFIG = {
{
title: 'atlas',
description:
'Python project for infrastructure and tooling.',
'Python tooling for infrastructure operations, glue scripts, and automation helpers.',
language: 'Python',
link: 'https://git.levkin.ca/ilia/atlas',
},
{
title: 'ansible',
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',
link: 'https://git.levkin.ca/ilia/ansible',
},
{
title: 'POTE',
description: 'Python project.',
description:
'Python utilities and experiments — small tools and libraries for day-to-day engineering work.',
language: 'Python',
link: 'https://git.levkin.ca/ilia/POTE',
},
{
title: 'mirror_match',
description: 'TypeScript application.',
description:
'TypeScript app — UI and logic for a focused domain problem; structured for testability.',
language: 'TypeScript',
link: 'https://git.levkin.ca/ilia/mirror_match',
},
@ -70,7 +74,8 @@ const CONFIG = {
},
{
title: 'crkl',
description: 'Kotlin application.',
description:
'Kotlin application — JVM/Android-oriented codebase and patterns.',
language: 'Kotlin',
link: 'https://git.levkin.ca/ilia/crkl',
},
@ -80,14 +85,15 @@ const CONFIG = {
seo: {
title: 'Ilia Dobkin — SDET & Test Automation Engineer',
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: '',
},
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.',
'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.',
'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.',
'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: {
linkedin: 'idobkin',
@ -108,6 +114,7 @@ const CONFIG = {
discord: '',
telegram: '',
website: 'https://git.levkin.ca',
websiteLabel: 'Git',
phone: '647 987 2792',
email: 'idobkin@gmail.com',
},
@ -148,7 +155,6 @@ const CONFIG = {
'Cross-browser testing',
'Mobile testing',
'GitHub Actions',
'GitHub',
'GitLab CI',
'Bitbucket',
'Jenkins',
@ -198,6 +204,7 @@ const CONFIG = {
'DNS',
'Local LLM / GPU',
],
skillsPreviewLimit: 16,
experiences: [],
certifications: [],
educations: [],
@ -212,7 +219,7 @@ const CONFIG = {
},
hotjar: { id: '', snippetVersion: 6 },
themeConfig: {
defaultTheme: 'light',
defaultTheme: 'dark',
disableSwitch: false,
@ -227,7 +234,7 @@ const CONFIG = {
class="text-primary" href="https://git.levkin.ca"
target="_blank"
rel="noreferrer"
>Gitea</a> · <a
>Git</a> · <a
class="text-primary" href="https://www.linkedin.com/in/idobkin/"
target="_blank"
rel="noreferrer"

17
global.d.ts vendored
View File

@ -1,8 +1,13 @@
interface Github {
/**
* GitHub org/user name
* GitHub org/user name (used for profile API: avatar, name)
*/
username: string;
/**
* Show GitHub row in the contact/details card (default true)
*/
showInDetails?: boolean;
}
interface GitHubProjects {
@ -190,6 +195,11 @@ interface Social {
*/
website?: string;
/**
* Label for the website row (default "Website"), e.g. "Git" for a Gitea host
*/
websiteLabel?: string;
/**
* Telegram username
*/
@ -365,6 +375,11 @@ interface Config {
*/
skills?: Array<string>;
/**
* How many skill badges to show before "Show all" (default 16)
*/
skillsPreviewLimit?: number;
/**
* Experience list
*/

View File

@ -77,14 +77,18 @@ const ListItem: React.FC<{
wordBreak: 'break-word',
}}
>
{link ? (
<a
href={link}
target="_blank"
rel="noreferrer"
className="flex justify-start py-2 px-1 items-center"
className="link link-primary link-hover inline-flex justify-end py-1"
>
{value}
</a>
) : (
<span className="inline-block py-1 text-right">{value}</span>
)}
</div>
</div>
);
@ -147,7 +151,7 @@ const OrganizationItem: React.FC<{
* @param {Object} profile - The profile object.
* @param {boolean} loading - Indicates whether the data is loading.
* @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.
*/
const DetailsCard = ({ profile, loading, social, github }: Props) => {
@ -195,12 +199,14 @@ const DetailsCard = ({ profile, loading, social, github }: Props) => {
}
/>
)}
{github.showInDetails && (
<ListItem
icon={<AiFillGithub />}
title="GitHub:"
value={github.username}
link={`https://github.com/${github.username}`}
/>
)}
{social?.researchGate && (
<ListItem
icon={<SiResearchgate />}
@ -324,13 +330,13 @@ const DetailsCard = ({ profile, loading, social, github }: Props) => {
{social?.website && (
<ListItem
icon={<FaGlobe />}
title="Website:"
title={`${social.websiteLabel}:`}
value={social.website
.replace('https://', '')
.replace('http://', '')}
link={
!social.website.startsWith('http')
? `http://${social.website}`
? `https://${social.website}`
: social.website
}
/>

View File

@ -201,12 +201,6 @@ const GitProfile = ({ config }: { config: Config }) => {
themeConfig={sanitizedConfig.themeConfig}
/>
)}
{sanitizedConfig.about.length !== 0 && (
<AboutCard
loading={loading}
paragraphs={sanitizedConfig.about}
/>
)}
<AvatarCard
profile={profile}
loading={loading}
@ -224,6 +218,12 @@ const GitProfile = ({ config }: { config: Config }) => {
github={sanitizedConfig.github}
social={sanitizedConfig.social}
/>
{sanitizedConfig.about.length !== 0 && (
<AboutCard
loading={loading}
paragraphs={sanitizedConfig.about}
/>
)}
{sanitizedConfig.resume.fileUrl &&
sanitizedConfig.resume.previewLines.length !== 0 && (
<ResumeCard
@ -237,6 +237,7 @@ const GitProfile = ({ config }: { config: Config }) => {
<SkillCard
loading={loading}
skills={sanitizedConfig.skills}
previewLimit={sanitizedConfig.skillsPreviewLimit}
/>
)}
{sanitizedConfig.certifications.length !== 0 && (

View File

@ -86,15 +86,25 @@ const ResumeCard = ({
<p key={i}>{line}</p>
))}
</div>
<div className="flex flex-wrap gap-2 mt-4">
<a
href={pdfAbsoluteUrl}
target="_blank"
rel="noopener noreferrer"
className="btn btn-outline btn-sm text-xs mt-4 opacity-70 hover:opacity-100 transition-opacity"
className="btn btn-outline btn-sm text-xs opacity-70 hover:opacity-100 transition-opacity"
onClick={openPdfInNewTab}
>
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>

View File

@ -1,12 +1,19 @@
import { useState } from 'react';
import { skeleton } from '../../utils';
const SkillCard = ({
loading,
skills,
previewLimit,
}: {
loading: boolean;
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 array = [];
for (let index = 0; index < 12; index++) {
@ -28,20 +35,38 @@ const SkillCard = ({
{loading ? (
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>
{!loading && hasMore && !expanded && (
<p className="text-xs text-base-content/50 mx-3 mt-1">
Showing {previewLimit} of {skills.length}.
</p>
)}
</div>
<div className="p-3 flow-root">
<div className="-m-1 flex flex-wrap justify-center gap-2">
{loading
? renderSkeleton()
: skills.map((skill, index) => (
: visible.map((skill, index) => (
<div key={index} className="badge badge-primary badge-sm">
{skill}
</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>

View File

@ -1,5 +1,6 @@
export interface SanitizedGithub {
username: string;
showInDetails: boolean;
}
export interface SanitizedGitHubProjects {
@ -60,6 +61,7 @@ export interface SanitizedSocial {
dev?: string;
stackoverflow?: string;
website?: string;
websiteLabel: string;
telegram?: string;
phone?: string;
email?: string;
@ -136,6 +138,7 @@ export interface SanitizedConfig {
social: SanitizedSocial;
resume: SanitizedResume;
skills: Array<string>;
skillsPreviewLimit: number;
experiences: Array<SanitizedExperience>;
educations: Array<SanitizedEducation>;
certifications: Array<SanitizedCertification>;

View File

@ -45,6 +45,7 @@ export const getSanitizedConfig = (
return {
github: {
username: config.github.username,
showInDetails: config.github.showInDetails !== false,
},
projects: {
github: {
@ -93,6 +94,7 @@ export const getSanitizedConfig = (
dev: config?.social?.dev,
stackoverflow: config?.social?.stackoverflow,
website: config?.social?.website,
websiteLabel: config?.social?.websiteLabel || 'Website',
phone: config?.social?.phone,
email: config?.social?.email,
telegram: config?.social?.telegram,
@ -105,6 +107,10 @@ export const getSanitizedConfig = (
previewLines: config?.resume?.previewLines || [],
},
skills: config?.skills || [],
skillsPreviewLimit: Math.max(
8,
Math.min(64, config?.skillsPreviewLimit ?? 16),
),
experiences:
config?.experiences?.filter(
(experience) =>