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 = {
|
||||
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
17
global.d.ts
vendored
@ -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
|
||||
*/
|
||||
|
||||
@ -77,14 +77,18 @@ const ListItem: React.FC<{
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex justify-start py-2 px-1 items-center"
|
||||
>
|
||||
{value}
|
||||
</a>
|
||||
{link ? (
|
||||
<a
|
||||
href={link}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
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) => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<ListItem
|
||||
icon={<AiFillGithub />}
|
||||
title="GitHub:"
|
||||
value={github.username}
|
||||
link={`https://github.com/${github.username}`}
|
||||
/>
|
||||
{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
|
||||
}
|
||||
/>
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -86,15 +86,25 @@ const ResumeCard = ({
|
||||
<p key={i}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
<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"
|
||||
onClick={openPdfInNewTab}
|
||||
>
|
||||
View Full Resume (PDF)
|
||||
</a>
|
||||
<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 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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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) =>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user