Compare commits

..

264 Commits

Author SHA1 Message Date
95081b8b77 Omit drafts from build 2025-11-11 09:28:59 -08:00
40b6359d8f Add public pgp key; update astro 2025-11-11 09:19:03 -08:00
d61080722d push latest 2025-09-15 10:38:26 -07:00
5117218a1a Fix code block formatting on mobile devices 2025-09-15 10:04:35 -07:00
f355373ba1 For now sunset resources work 2025-09-11 08:55:05 -07:00
384cb82efb create 0101-welcome-to-the-terminal 2025-08-27 12:46:17 -07:00
7ff6f6542b create video player 2025-08-27 11:19:18 -07:00
9ad08dc85d background animations: 2025-08-27 08:53:56 -07:00
12631dbd42 Animations 2025-08-27 08:27:22 -07:00
1758dc3153 Fixed 2025-08-22 23:08:39 -07:00
9496030d41 Broken 2025-08-21 22:53:37 -07:00
30f264a6bb Thinking of ways to build out a presentation system 2025-08-21 22:18:04 -07:00
7992fcbd49 Add a resources layout 2025-08-18 13:28:55 -07:00
60a9fb0339 back at it 2025-08-16 13:57:37 -07:00
6711de5eb6 Update mdx command component to conform to mobile container size 2025-04-23 12:26:34 -07:00
f2b4660300 keep working on coreboot guide 2025-04-23 12:00:44 -07:00
97608e983c update readme 2025-04-22 13:29:29 -07:00
ce812e8466 Add commands mdx component; continue work on coreboot post 2025-04-22 12:20:19 -07:00
d44988b39c Set comment feed to load eagerly 2025-04-22 09:26:43 -07:00
d885ea4e6b Fix requestAnimationFrame blurring background after switching views 2025-04-22 09:07:47 -07:00
6cfa4c5b7d Remove cursor; update background 2025-04-22 09:04:21 -07:00
f2e85dc6d8 Add cursor trail; fix giscuss 2025-04-21 14:45:44 -07:00
fce17d397e hide custom cursor om mobile devices 2025-04-21 14:26:17 -07:00
7cc954ae07 Add custom cursor; improve pointer events 2025-04-21 14:15:08 -07:00
c6aa014d29 Fix content typography sizes 2025-04-21 13:25:48 -07:00
a9cbbb7e8e Update dockerfile 2025-04-21 12:31:57 -07:00
788eb84488 Update entrypoint to support ssr 2025-04-21 12:26:19 -07:00
4fc5a07249 Update Caddyfile.release 2025-04-21 12:21:15 -07:00
aca5d53bd1 fix proxy issue 2025-04-21 12:17:57 -07:00
f1af80afaf Update compose 2025-04-21 12:14:40 -07:00
257000e81d Update container name 2025-04-21 12:13:32 -07:00
8b30228c4a update compose 2025-04-21 12:10:48 -07:00
99e1cd5639 Update dockerfile; update compose 2025-04-21 12:02:33 -07:00
7446d8296a update compose 2025-04-21 11:52:32 -07:00
6b424ae8e4 Update compose 2025-04-21 11:51:22 -07:00
04489a53d1 Update compose 2025-04-21 11:47:09 -07:00
b40134833b Update compose; remove prisma schema 2025-04-21 11:28:32 -07:00
e2bf036919 Update package ver 2025-04-21 11:10:04 -07:00
7443947131 Add a comments feed; update background 2025-04-21 11:09:23 -07:00
0589ff9c7c Add interactivity to the background animation: 2025-04-17 12:39:48 -07:00
0c2e7f505d Update background animation to be more fluid and natural 2025-04-17 12:28:25 -07:00
cfbe43ab8b Remove prisma db; update comments.css 2025-04-17 12:02:36 -07:00
b5120b60df Style giscuss comment feed 2025-04-17 11:48:25 -07:00
b6b98023da Add interactive script message to blog 2025-04-10 11:09:12 -07:00
37c63db863 Update dependencies; add scripts submodule 2025-04-10 10:38:44 -07:00
61cca45350 Update astro 2025-03-10 14:19:55 -07:00
4b37d29a43 Update tag-list 2025-01-30 15:03:46 -08:00
d4f51b121e Update pages/about.astro 2025-01-30 09:24:25 -08:00
2e088c5c9f Add wakatime stats; begin work on blog tags 2025-01-30 09:18:18 -08:00
6ef97bb5f7 Update post index, rss feed, 404 page 2025-01-28 10:17:01 -08:00
bc4ddb7eae Update css 2025-01-28 09:43:30 -08:00
d69d3a0249 Update code style; add selection style 2025-01-28 09:37:46 -08:00
ee3918f428 Add title and description to blog posts 2025-01-28 08:52:05 -08:00
c9ab7a37b9 Update rss feed path 2025-01-27 11:29:24 -08:00
935d2a9077 Update dependencies; add rss feed 2025-01-23 14:53:26 -08:00
Timothy Pidashev
3c067b6c49 Update dependencies 2025-01-23 09:18:41 -08:00
Timothy Pidashev
8bb28cffa6 Fix coerce.date to PST time 2025-01-14 14:41:07 -08:00
Timothy Pidashev
a24fea8f3b Ensure date is UTC 2025-01-14 14:30:22 -08:00
Timothy Pidashev
8e32f21462 Update date format in blog slug 2025-01-14 14:23:29 -08:00
Timothy Pidashev
de871e775e Update config.ts 2025-01-14 12:21:40 -08:00
Timothy Pidashev
c89318ddd8 Update date parsing on blog; fix Makefile 2025-01-14 12:19:13 -08:00
Timothy Pidashev
b14fd5d7e7 Add favicon 2025-01-14 08:54:31 -08:00
Timothy Pidashev
3d07b2b714 Bump footer version 2025-01-13 14:00:00 -08:00
Timothy Pidashev
3880e2ab7b Update blog image previews 2025-01-13 13:49:35 -08:00
Timothy Pidashev
72ee036c08 Woops; fix nested build config 2025-01-13 12:10:08 -08:00
Timothy Pidashev
d083ec090c Optimize build output 2025-01-13 12:08:36 -08:00
Timothy Pidashev
05844b2446 Update opengraph results and meta tags 2025-01-13 12:01:03 -08:00
Timothy Pidashev
844c8d49d4 Update astr.config.mjs 2025-01-13 10:53:45 -08:00
Timothy Pidashev
de1411b01a Update background 2025-01-13 08:53:31 -08:00
Timothy Pidashev
bfda37ee0b Update index layout: 2025-01-13 08:51:01 -08:00
Timothy Pidashev
2777d14007 Update blog title 2025-01-13 08:46:50 -08:00
Timothy Pidashev
f37688f2d1 hotfix 2025-01-13 08:43:07 -08:00
Timothy Pidashev
acad2cc0ca begin work on deployment process 2025-01-09 17:20:42 -08:00
Timothy Pidashev
6466602276 Rename README to README.md 2025-01-09 12:00:49 -08:00
Timothy Pidashev
d657951158 Update README 2025-01-09 12:00:17 -08:00
Timothy Pidashev
6a35f48097 Merge pull request #2 from timmypidashev/dependabot/npm_and_yarn/src/nanoid-3.3.8
Bump nanoid from 3.3.7 to 3.3.8 in /src
2025-01-09 11:57:16 -08:00
Timothy Pidashev
6805fe57d7 Write up my first post 2025-01-09 11:56:18 -08:00
Timothy Pidashev
133c0944bc Finish project content 2025-01-09 10:49:28 -08:00
Timothy Pidashev
02290388da Create resume page 2025-01-09 09:56:08 -08:00
Timothy Pidashev
b2455cb1e2 optimize view persistence & header 2025-01-08 17:26:05 -08:00
Timothy Pidashev
5f06079b5b Add background to content; update layouts; add more content: 2025-01-08 14:51:51 -08:00
Timothy Pidashev
5681e4b1ad Update header 2025-01-08 10:51:23 -08:00
Timothy Pidashev
2519182e86 Absolutely beatiful game of life hero background 2025-01-08 10:47:46 -08:00
Timothy Pidashev
42495f2316 Ensure vine branches dont generate on the tips of other branches 2025-01-07 12:09:37 -08:00
Timothy Pidashev
035944887b Fix dates in content projects; write up darkbox 2025-01-06 12:37:16 -08:00
Timothy Pidashev
21772ae6cb Add preliminary vines animation; more work on content 2025-01-06 11:55:34 -08:00
Timothy Pidashev
efe0b9713f Begin work on writing project mdx 2025-01-03 13:43:23 -08:00
Timothy Pidashev
f5211cc799 test 2024-12-21 09:54:36 -08:00
Timothy Pidashev
b618f6e807 Begin writing projects 2024-12-18 13:53:21 -08:00
Timothy Pidashev
d5cbe73c2d Add about components; implement projects 2024-12-18 10:22:48 -08:00
Timothy Pidashev
d96a27e612 Update intro component 2024-12-17 14:33:56 -08:00
Timothy Pidashev
b3da439864 Add intro component to about 2024-12-17 14:25:10 -08:00
dependabot[bot]
76ecd1a392 Bump nanoid from 3.3.7 to 3.3.8 in /src
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-17 20:43:52 +00:00
Timothy Pidashev
deeef2f8a0 Migrate to new astro version; small header/footer/hero fixes; continue work on blog implementation 2024-12-17 12:42:02 -08:00
Timothy Pidashev
26877cf18a Some styling touches 2024-11-05 14:24:42 -08:00
Timothy Pidashev
dfd5b15ed9 fix footer 2024-11-04 13:18:09 -08:00
Timothy Pidashev
22c9391c37 begin bringing back all the things 2024-11-04 12:37:25 -08:00
Timothy Pidashev
b90108e70f slight fixes; updated dependencies 2024-10-30 12:30:25 -07:00
Timothy Pidashev
0ff2116794 reconfigure docker/caddy/build 2024-09-06 20:52:04 -07:00
Timothy Pidashev
2fcdf6272e rename compose files 2024-09-06 19:46:16 -07:00
Timothy Pidashev
9d7414e0c9 Begin rewrite to astro 2024-09-06 19:44:15 -07:00
Timothy Pidashev
93d9b3e014 Slightly better styling: 2024-06-14 19:26:36 -07:00
Timothy Pidashev
c3bc253182 Code styles always worked, just weren't styled lol 2024-06-10 18:50:12 -07:00
Timothy Pidashev
aaf29f45a0 Remove broken inline code style 2024-06-10 18:29:40 -07:00
Timothy Pidashev
502b1a93e1 Fix navigation links 2024-06-10 09:59:42 -07:00
Timothy Pidashev
efa4be2fd9 Colors work on inline code lets go 2024-06-09 17:59:08 -07:00
Timothy Pidashev
6bd0616d54 Add mdx styling 2024-06-09 17:41:57 -07:00
Timothy Pidashev
73e6e2c354 Jumbo commit 2024-06-07 17:29:08 -07:00
Timothy Pidashev
f96629a6b4 Blog works 2024-06-05 18:41:29 -07:00
Timothy Pidashev
4c97f4f52d Actually fix broken nextjs 2024-06-05 18:09:14 -07:00
Timothy Pidashev
ef9522cf3e Move src dir 2024-06-05 09:11:18 -07:00
Timothy Pidashev
189774def8 Blog 2024-06-04 18:49:43 -07:00
Timothy Pidashev
6fd37f854d Blogs work 2024-06-04 18:35:15 -07:00
Timothy Pidashev
55f1ff96d4 Update 2024-06-04 17:57:00 -07:00
Timothy Pidashev
4b5de24ef6 Wrap up hero section for now 2024-05-11 22:55:17 -07:00
Timothy Pidashev
dc4cf3fbbc basic footer 2024-05-11 22:35:36 -07:00
Timothy Pidashev
0a08ef4b0c containerize app, several polishing things: 2024-05-11 21:59:40 -07:00
Timothy Pidashev
3e3bc486e2 back to fixed 2024-05-11 21:44:40 -07:00
Timothy Pidashev
df411e42ce fixed header 2024-05-11 21:31:47 -07:00
Timothy Pidashev
bc6e7a0278 Add bump to the makefile 2024-05-11 20:49:04 -07:00
Timothy Pidashev
1a72c07e82 small update 2024-05-02 09:50:08 -07:00
Timothy Pidashev
afa9013ff0 Switch to yarn 2024-04-29 18:14:58 -07:00
Timothy Pidashev
b459052b44 remove unneeded containers 2024-04-20 15:59:42 -07:00
Timothy Pidashev
21d1fc9f8c Containerize hero and header 2024-04-20 15:54:20 -07:00
Timothy Pidashev
068d2a5c7a Update lockfile, fix font sizes in not-found 2024-04-20 15:34:23 -07:00
Timothy Pidashev
e20fc0d197 add a profile pic, wip 2024-04-16 22:38:00 -07:00
Timothy Pidashev
bded192500 add margin and strong elements to hero content 2024-04-16 21:42:08 -07:00
Timothy Pidashev
d813123d95 Add grid element 2024-04-10 14:58:09 -07:00
Timothy Pidashev
b626ce3abb Expand hero content 2024-04-10 12:29:29 -07:00
Timothy Pidashev
242e4d8a7f Clean up components 2024-04-10 12:16:36 -07:00
Timothy Pidashev
c16f92e576 begin work on hero 2024-04-10 11:32:23 -07:00
Timothy Pidashev
8b33094cef Update hero sections 2024-03-23 04:02:55 +00:00
Timothy Pidashev
6b09180743 hero section work 2024-03-22 00:38:21 -07:00
Timothy Pidashev
4d205596fc Work on header, sidebar broken 2024-03-21 23:42:53 -07:00
Timothy Pidashev
4f0959f433 Add stagger effect 2024-03-19 23:31:46 -07:00
Timothy Pidashev
8bf39116e9 update hidden state 2024-03-19 22:56:37 -07:00
Timothy Pidashev
2879ab0563 Add tabs, though its more for readability... 2024-03-19 22:50:41 -07:00
Timothy Pidashev
3ba0a94793 Fix tab colors 2024-03-19 22:48:07 -07:00
Timothy Pidashev
a2555d1940 rework tabs 2024-03-19 22:31:41 -07:00
Timothy Pidashev
55391f7ee5 Work on collapsible menu 2024-03-19 21:52:12 -07:00
Timothy Pidashev
7c3bd72fa0 Split up header into multiple readable parts 2024-03-19 21:17:00 -07:00
Timothy Pidashev
1cf61969af Add all react-fiber stuff for more commits 2024-03-19 21:00:55 -07:00
Timothy Pidashev
8d23faf7ad react-fiber works! 2024-03-19 20:28:56 -07:00
Timothy Pidashev
4136bf2622 Fix stagger effect 2024-03-19 12:18:21 -07:00
Timothy Pidashev
2c9c0b08d0 remove stagger effect 2024-03-19 11:57:59 -07:00
Timothy Pidashev
9720e6faf4 Make the header responsive 2024-03-19 11:26:56 -07:00
Timothy Pidashev
2acb40c90c Add three.js dep, update page transition animation 2024-03-19 09:56:17 -07:00
Timothy Pidashev
b04cd10453 Add page transitions 2024-03-19 09:49:04 -07:00
Timothy Pidashev
b37f35350b prepare hero content 2024-03-18 23:18:38 -07:00
Timothy Pidashev
773085b2e0 Stagger tabs on animate 2024-03-18 22:56:35 -07:00
Timothy Pidashev
6c2b82086a minor fix 2024-03-18 22:47:44 -07:00
Timothy Pidashev
53a3832fc4 Change color for each tab in animation 2024-03-18 22:35:00 -07:00
Timothy Pidashev
62fdbf1fc1 Change header flair to underline from bubble 2024-03-18 22:12:39 -07:00
Timothy Pidashev
fbcc0385a4 center header: 2024-03-18 19:45:15 -07:00
Timothy Pidashev
d69b327bb6 Header placeholders for working ssr 2024-03-18 19:42:03 -07:00
Timothy Pidashev
93a2bf9caa Animate header 2024-03-18 19:40:39 -07:00
Timothy Pidashev
7b0d09f2c9 Create pages 2024-03-18 19:14:57 -07:00
Timothy Pidashev
385b237906 style not-found 2024-03-18 18:38:35 -07:00
Timothy Pidashev
70c7d03576 Add font 2024-03-18 18:15:33 -07:00
Timothy Pidashev
9f4c069f7f center theme-toggle 2024-03-18 12:36:40 -07:00
Timothy Pidashev
36112fe04e begin work on header 2024-03-18 12:19:11 -07:00
Timothy Pidashev
9422553c9c Add tailwind theme 2024-03-18 10:27:29 -07:00
Timothy Pidashev
ed1dc91bd7 Actually works! 2024-03-17 19:48:53 -07:00
Timothy Pidashev
998841e1e7 fix gitignore 2024-03-17 19:03:46 -07:00
Timothy Pidashev
e96e679a35 Theme toggle 2024-03-17 19:01:50 -07:00
Timothy Pidashev
73402aec0b update ignores 2024-03-17 16:54:01 -07:00
Timothy Pidashev
65a46162d7 Dockerfile, tailwind 2024-03-17 16:51:41 -07:00
Timothy Pidashev
6a6804f43a Caddy works 2024-03-16 23:08:46 -07:00
Timothy Pidashev
2fd5c7ec36 caddy update 2024-03-16 22:52:40 -07:00
Timothy Pidashev
b87d34410a begin next js version 2024-03-16 22:51:31 -07:00
Timothy Pidashev
bceec10c3f Work on hero sections 2024-03-13 23:57:11 -07:00
Timothy Pidashev
f23ddf6e5c content 2024-03-12 14:22:15 -07:00
Timothy Pidashev
c6c5f1c067 update style 2024-03-12 12:38:47 -07:00
Timothy Pidashev
09365d828a fix page reloads 2024-03-12 12:35:50 -07:00
Timothy Pidashev
d1684a1472 update 2024-03-12 10:10:44 -07:00
Timothy Pidashev
ec1a5103c3 Animations, kinda :D 2024-03-11 23:56:08 -07:00
Timothy Pidashev
e7f70b4c02 sometimes simpler is better; back to spa design 2024-03-11 22:19:02 -07:00
Timothy Pidashev
8b6a760d91 feat: shared components 2024-03-11 18:17:57 -07:00
Timothy Pidashev
56f799266b Shared dirs almost ready :D 2024-03-11 14:20:55 -07:00
Timothy Pidashev
893c59585e Add shared directory 2024-03-11 09:04:23 -07:00
Timothy Pidashev
1c255069e7 Were rolling with it 2024-03-11 08:54:21 -07:00
Timothy Pidashev
47bbbb01fa commit .web, though there's not really a point in tracking this directory 2024-03-11 08:35:42 -07:00
Timothy Pidashev
71b28b6059 Sunday commit 2024-03-10 23:12:54 -07:00
Timothy Pidashev
9483382799 Add css and fonts 2024-03-09 13:40:06 -08:00
Timothy Pidashev
42215fcad4 Add default webpage template 2024-03-09 05:34:48 -08:00
Timothy Pidashev
d3c260a0fa Begin work on landing 2024-03-09 03:37:15 -08:00
Timothy Pidashev
cb2ac819e0 Reverse proxy works locally :D 2024-03-09 03:15:53 -08:00
Timothy Pidashev
fde907781a update compose file 2024-03-07 15:07:08 -08:00
Timothy Pidashev
3b7fe795e8 now im getting a white page, better than an error i suppose... 2024-03-07 15:00:48 -08:00
Timothy Pidashev
1fee9df3a1 expose actually works lol 2024-03-07 14:52:30 -08:00
Timothy Pidashev
4f93517f9e ports are borked 2024-03-07 13:40:10 -08:00
Timothy Pidashev
9204d1c569 proxy updates 2024-03-06 14:55:36 -08:00
Timothy Pidashev
8f57e420b5 begin work on a new web 2024-03-06 10:11:13 -08:00
timmypidashev
0e534d670d container building, time to fix up the web file 2023-11-25 21:49:49 -08:00
timmypidashev
6799028dff Add build-time args 2023-11-12 00:00:43 -08:00
timmypidashev
219d891c23 update web script 2023-11-09 13:59:15 -08:00
timmypidashev
7820806c26 continue working on dev workflow: 2023-11-09 11:05:10 -08:00
timmypidashev
d666c62af1 rename project to webapp 2023-11-03 17:45:08 -07:00
timmypidashev
99d518f475 rename app to project 2023-11-03 17:44:30 -07:00
timmypidashev
4117802d8c move frontends to app dir 2023-11-03 17:38:44 -07:00
timmypidashev
a827dba86f create dns 2023-11-03 17:38:01 -07:00
timmypidashev
34dfde16d5 create blog 2023-11-03 17:37:08 -07:00
timmypidashev
bf43cbe9fd Create landing 2023-11-03 17:36:02 -07:00
timmypidashev
7561484d25 Upload AUTHORS,LICENSE,README,version.toml 2023-11-03 17:35:07 -07:00
timmypidashev
34aecb70d5 make a web script 2023-11-03 17:24:08 -07:00
timmypidashev
df1e0b5e00 begin rework of the site 2023-11-02 14:57:35 -07:00
timmypidashev
d7fdb4866a Cant forget tests 2023-03-11 20:54:59 -08:00
timmypidashev
6d7b58d2a9 Brainstorming design 2023-03-11 20:54:18 -08:00
timmypidashev
eeee364c93 Rust yew starter 2023-03-11 20:22:24 -08:00
timmypidashev
32c0b774a2 Initial commit for rust rewrite 2023-03-11 20:09:01 -08:00
timmypidashev
259d92dfe1 Remove .ruby-version 2022-06-14 15:21:07 -07:00
timmypidashev
974bb7956e Clean directory for site rebuild 2022-06-14 15:20:28 -07:00
timmypidashev
3c54f2470e Start ruby template 2022-05-08 16:37:52 -07:00
Timothy Pidashev
447098b7ef Update index.html 2022-01-17 09:08:10 -08:00
timmypidashev
30e6d32b09 update assignments 2022-01-17 05:46:30 +00:00
Timothy Pidashev
bba2c6fbb9 implement trinket.io for coding 2022-01-08 19:12:00 -08:00
Timothy Pidashev
41f9cd8fd3 plz work well 2022-01-08 18:36:52 -08:00
Timothy Pidashev
1723aae6fc minor fix 2022-01-08 18:24:01 -08:00
Timothy Pidashev
60b9e62881 more work 2022-01-08 18:21:20 -08:00
Timothy Pidashev
f134a3c3d5 begin work on assignments 2022-01-08 17:49:48 -08:00
Timothy Pidashev
3c371383d5 minor polish 2022-01-08 16:48:15 -08:00
Timothy Pidashev
074f13683a hopefully deployment is better than dev 2022-01-08 16:32:53 -08:00
Timothy Pidashev
c9385a50b8 push more code 2022-01-08 14:20:15 -08:00
Timothy Pidashev
76b2b20e2f little polish 2022-01-08 12:20:11 -08:00
Timothy Pidashev
e207049644 dont know what i did but i like it 2022-01-08 12:03:43 -08:00
Timothy Pidashev
64718e30e5 animations and more fun 2022-01-07 23:22:17 -08:00
Timothy Pidashev
b26b405eca just some more work done 2022-01-07 12:43:22 -08:00
Timothy Pidashev
78c96c0aff Add initial ace editor 2022-01-06 22:27:18 -08:00
Timothy Pidashev
73aecb076f Remove unneeded images 2021-11-25 14:44:14 -08:00
Timothy Pidashev
9114ee14af Update README.md 2021-10-14 09:41:28 -07:00
Timothy Pidashev
69f75857be Add files via upload 2021-10-14 09:40:34 -07:00
timmypidashev
8a2f8edb30 add blog__image style 2021-10-12 17:28:28 -07:00
timmypidashev
8cb801f51e Fix blog properties 2021-10-12 17:21:04 -07:00
timmypidashev
c66ee24dce Merge branch 'main' of https://github.com/timmypidashev/site 2021-10-12 17:06:25 -07:00
timmypidashev
441b26912f add smooth animations to navbar 2021-10-12 17:06:13 -07:00
Timothy Pidashev
4433a1523d Update styles.css 2021-10-11 12:53:09 -07:00
Timothy Pidashev
56934cf46a Update styles.css 2021-10-11 12:48:32 -07:00
Timothy Pidashev
a2cd51deae Update blog.html 2021-10-11 12:44:22 -07:00
Timothy Pidashev
638e90e858 Update blog.html 2021-10-11 12:42:31 -07:00
Timothy Pidashev
ace2695d7f Update styles.css 2021-10-11 12:38:27 -07:00
timmypidashev
dad9d4fd1b Merge branch 'main' of https://github.com/timmypidashev/site 2021-10-09 12:25:21 -07:00
timmypidashev
f39d8aa690 remove some links 2021-10-09 12:25:02 -07:00
Timothy Pidashev
98a7a589cc Update styles.css 2021-10-03 11:50:31 -07:00
Timothy Pidashev
ad75d90d9f Update styles.css 2021-10-03 11:48:07 -07:00
timmypidashev
8a9a1335f3 testing gpg 2021-10-02 20:53:35 -07:00
timmypidashev
bbe25f26e7 update rich embed site pointer 2021-10-02 20:44:42 -07:00
timmypidashev
3c979be288 add initial support for blog 2021-10-02 19:18:07 -07:00
timmypidashev
fbbca11032 minor cache fixing 2021-10-02 18:08:59 -07:00
timmypidashev
f4e630ee76 fix title for rich embed 2021-10-02 18:05:35 -07:00
timmypidashev
af2a4c104a more embed stuff 2021-10-02 18:04:01 -07:00
timmypidashev
8e86979394 try and add embeds 2021-10-02 17:58:10 -07:00
timmypidashev
7b875a85c1 update color-scheme 2021-10-02 17:34:59 -07:00
timmypidashev
1f820e4008 Merge branch 'main' of https://github.com/timmypidashev/site 2021-09-22 12:14:14 -07:00
timmypidashev
54361ac79b Clean up index.htl 2021-09-22 12:13:50 -07:00
Timothy Pidashev
e9ac5400b4 Update links 2021-09-22 07:28:17 -07:00
timmypidashev
d7130929d8 remove static from reference 2021-09-21 18:27:42 -07:00
timmypidashev
aca7c706ac Do a lot of work :) 2021-09-21 18:24:13 -07:00
timmypidashev
7672c75f29 start work on a yt download feature 2021-09-21 17:01:49 -07:00
timmypidashev
be9d5be429 remove scrollbar for all major web browsers 2021-09-21 17:01:30 -07:00
Timothy Pidashev
caca5b16c5 Update index.html 2021-09-14 10:53:24 -07:00
Timothy Pidashev
963e29e4f4 Update index.html 2021-09-14 10:26:48 -07:00
Timothy Pidashev
00614ebc7a Update index.html 2021-09-14 10:22:53 -07:00
Timothy Pidashev
2c8a235b4c Update index.html 2021-09-14 10:03:58 -07:00
Timothy Pidashev
d1aaebdefe Update index.html 2021-09-07 14:51:00 -07:00
115 changed files with 11403 additions and 4465 deletions

5
.caddy/Caddyfile.local Normal file
View File

@@ -0,0 +1,5 @@
timmypidashev.local {
tls internal
reverse_proxy timmypidashev.dev:4321
}

5
.caddy/Caddyfile.release Normal file
View File

@@ -0,0 +1,5 @@
timmypidashev.dev {
tls pidashev.tim@gmail.com
reverse_proxy timmypidashev.dev:3000
}

27
.docker/Dockerfile.local Normal file
View File

@@ -0,0 +1,27 @@
FROM node:22-alpine
ARG CONTAINER_WEB_VERSION
ARG ENVIRONMENT
ARG BUILD_DATE
ARG GIT_COMMIT
RUN set -eux \
& apk add \
--no-cache \
nodejs \
curl
RUN curl -L https://unpkg.com/@pnpm/self-installer | node
WORKDIR /app
COPY . .
RUN echo "PUBLIC_VERSION=${CONTAINER_WEB_VERSION}" > /app/.env && \
echo "PUBLIC_ENVIRONMENT=${ENVIRONMENT}" >> /app/.env && \
echo "PUBLIC_BUILD_DATE=${BUILD_DATE}" >> /app/.env && \
echo "PUBLIC_GIT_COMMIT=${GIT_COMMIT}" >> /app/.env
EXPOSE 3000
CMD ["pnpm", "run", "dev"]

View File

@@ -0,0 +1,47 @@
# Stage 1: Build and install dependencies
FROM node:22-alpine AS builder
WORKDIR /app
# Install necessary dependencies, including pnpm
RUN set -eux \
&& apk add --no-cache nodejs curl \
&& npm install -g pnpm
# Copy package files first (for better caching)
COPY package.json pnpm-lock.yaml ./
# Install dependencies
RUN pnpm install --frozen-lockfile
# Now copy the rest of your source code
COPY . .
# Set build arguments
ARG CONTAINER_WEB_VERSION
ARG ENVIRONMENT
ARG BUILD_DATE
ARG GIT_COMMIT
# Create .env file with build-time environment variables
RUN echo "PUBLIC_VERSION=${CONTAINER_WEB_VERSION}" > /app/.env && \
echo "PUBLIC_ENVIRONMENT=${ENVIRONMENT}" >> /app/.env && \
echo "PUBLIC_BUILD_DATE=${BUILD_DATE}" >> /app/.env && \
echo "PUBLIC_GIT_COMMIT=${GIT_COMMIT}" >> /app/.env
# Build the project
RUN pnpm run build
# Stage 2: Serve static files
FROM node:22-alpine
WORKDIR /app
# Copy built files
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
# Expose port 3000
EXPOSE 3000
# Deployment command
CMD ["node", "./dist/server/entry.mjs"]

BIN
.github/preview.jpeg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

83
.github/scripts/deploy_release.sh vendored Executable file
View File

@@ -0,0 +1,83 @@
#!/bin/sh
# Set variables
BRANCH_NAME="$1"
COMMIT_HASH="$2"
GHCR_USERNAME="$3"
GHCR_TOKEN="$4"
DEPLOY_TYPE="$5"
REPO_OWNER="$6"
COMPOSE_FILE="$7"
CADDYFILE="$8"
MAKEFILE="$9"
# Echo out variable names and their content on single lines
echo "BRANCH_NAME: $BRANCH_NAME"
echo "COMMIT_HASH: $COMMIT_HASH"
echo "GHCR_USERNAME: $GHCR_USERNAME"
echo "DEPLOY_TYPE: $DEPLOY_TYPE"
echo "REPO_OWNER: $REPO_OWNER"
echo "COMPOSE_FILE: $COMPOSE_FILE"
echo "CADDYFILE: $CADDYFILE"
echo "MAKEFILE: $MAKEFILE"
# Set the staging directory
STAGING_DIR="/root/deployments/.staging-${COMMIT_HASH}"
# Set the tmux session name for release
TMUX_SESSION="deployment-release"
# Function to cleanup existing release deployment
cleanup_release_deployment() {
echo "Cleaning up existing release deployment..."
# Stop and remove all release containers
docker-compose -f "/root/deployments/release/docker-compose.yml" down -v 2>/dev/null
# Remove release images
docker rmi $(docker images "ghcr.io/$REPO_OWNER/*:release" -q) 2>/dev/null
# Kill release tmux session if it exists
tmux kill-session -t "$TMUX_SESSION" 2>/dev/null
# Remove release deployment directory
rm -rf /root/deployments/release
}
# Function to create deployment directory
create_deployment_directory() {
echo "Creating deployment directory..."
mkdir -p /root/deployments/release
}
# Function to pull Docker images
pull_docker_images() {
echo "Pulling Docker images..."
docker pull ghcr.io/$REPO_OWNER/web:release
}
# Function to copy and prepare files
copy_and_prepare_files() {
echo "Copying and preparing files..."
# Copy files preserving names and locations
install -D "$STAGING_DIR/$COMPOSE_FILE" "/root/deployments/release/$COMPOSE_FILE"
install -D "$STAGING_DIR/$CADDYFILE" "/root/deployments/release/$CADDYFILE"
install -D "$STAGING_DIR/$MAKEFILE" "/root/deployments/release/$MAKEFILE"
# Replace {$COMMIT_HASH} with $COMMIT_HASH in $CADDYFILE
sed -i "s/{\$COMMIT_HASH}/$COMMIT_HASH/g" "/root/deployments/release/$CADDYFILE"
# Replace {commit_hash} with $COMMIT_HASH in $COMPOSE_FILE
sed -i "s/{commit_hash}/$COMMIT_HASH/g" "/root/deployments/release/$COMPOSE_FILE"
}
# Function to start the deployment
start_deployment() {
echo "Starting deployment..."
# Create new tmux session with specific name
tmux new-session -d -s "$TMUX_SESSION"
tmux send-keys -t "$TMUX_SESSION" "cd /root/deployments/release && make run release" Enter
}
# Main execution
cleanup_release_deployment
create_deployment_directory
copy_and_prepare_files
cd "/root/deployments/release"
pull_docker_images
start_deployment
echo "Release build $COMMIT_HASH deployed successfully!"

44
.gitignore vendored Normal file
View File

@@ -0,0 +1,44 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Env
.env
# astro
.astro/
# dependencies
node_modules/
/.pnp
.pnpm-store
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
.next/
/out/
# production
/build
dist/
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "src/public/scripts"]
path = src/public/scripts
url = https://github.com/timmypidashev/scripts

19
LICENSE Normal file
View File

@@ -0,0 +1,19 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

186
Makefile Normal file
View File

@@ -0,0 +1,186 @@
PROJECT_NAME := "timmypidashev.dev"
PROJECT_AUTHORS := "Timothy Pidashev (timmypidashev) <pidashev.tim@gmail.com>"
PROJECT_VERSION := "v2.1.1"
PROJECT_LICENSE := "MIT"
PROJECT_SOURCES := "https://github.com/timmypidashev/web"
PROJECT_REGISTRY := "ghcr.io/timmypidashev"
PROJECT_ORGANIZATION := "org.opencontainers"
CONTAINER_WEB_NAME := "timmypidashev.dev"
CONTAINER_WEB_VERSION := "v2.1.1"
CONTAINER_WEB_LOCATION := "src/"
CONTAINER_WEB_DESCRIPTION := "My portfolio website!"
.DEFAULT_GOAL := help
.PHONY: watch run build push prune bump exec
.SILENT: watch run build push prune bump exec
help:
@echo "Available targets:"
@echo " run - Runs the docker compose file with the specified environment (local or release)"
@echo " build - Builds the specified docker image with the appropriate environment"
@echo " push - Pushes the built docker image to the registry"
@echo " prune - Removes all built and cached docker images and containers"
@echo " bump - Bumps the project and container versions"
run:
# Arguments:
# [environment]: 'local' or 'release'
#
# Explanation:
# * Runs the docker compose file with the specified environment(compose.local.yml, or compose.release.yml)
# * Passes all generated arguments to the compose file.
# Make sure we have been given proper arguments.
@if [ "$(word 2,$(MAKECMDGOALS))" = "local" ]; then \
echo "Running in local environment"; \
elif [ "$(word 2,$(MAKECMDGOALS))" = "release" ]; then \
echo "Running in release environment"; \
else \
echo "Invalid usage. Please use 'make run local' or 'make run release'"; \
exit 1; \
fi
docker compose -f compose.$(word 2,$(MAKECMDGOALS)).yml up --remove-orphans
build:
# Arguments
# [container]: Build context(which container to build ['all' to build every container defined])
# [environment]: 'local' or 'release'
#
# Explanation:
# * Builds the specified docker image with the appropriate environment.
# * Passes all generated arguments to docker build-kit.
# Extract container and environment inputted.
$(eval INPUT_TARGET := $(word 2,$(MAKECMDGOALS)))
$(eval INPUT_CONTAINER := $(firstword $(subst :, ,$(INPUT_TARGET))))
$(eval INPUT_ENVIRONMENT := $(lastword $(subst :, ,$(INPUT_TARGET))))
# Call function container_build either through a for loop for each container
# if all is called, or singularly to build the container.
$(if $(filter $(strip $(INPUT_CONTAINER)),all), \
$(foreach container,$(containers),$(call container_build,$(container) $(INPUT_ENVIRONMENT))), \
$(call container_build,$(INPUT_CONTAINER) $(INPUT_ENVIRONMENT)))
push:
# Arguments
# [container]: Push context(which container to push to the registry)
# [version]: Container version to push.
#
# Explanation:
# * Pushes the specified container version to the registry defined in the user configuration.
# Extract container and version inputted.
$(eval INPUT_TARGET := $(word 2,$(MAKECMDGOALS)))
$(eval INPUT_CONTAINER := $(firstword $(subst :, ,$(INPUT_TARGET))))
$(eval INPUT_VERSION := $(lastword $(subst :, ,$(INPUT_TARGET))))
# Push the specified container version to the registry.
# NOTE: docker will complain if the container tag is invalid, no need to validate here.
@docker push $(PROJECT_REGISTRY)/$(INPUT_CONTAINER):$(INPUT_VERSION)
prune:
# Removes all built and cached docker images and containers.
bump:
# Arguments
# [container]: Bump context(which container version to bump)
# [semantic_type]: Semantic type context(major, minor, patch)
#
# Explanation:
# * Bumps the specified container version within the makefile config and the container's package.json.
# * Bumps the global project version in the makefile, and creates a new git tag with said version.
# Extract container and semantic_type inputted.
$(eval INPUT_TARGET := $(word 2,$(MAKECMDGOALS)))
$(eval INPUT_CONTAINER := $(firstword $(subst :, ,$(INPUT_TARGET))))
$(eval INPUT_SEMANTIC_TYPE := $(lastword $(subst :, ,$(INPUT_TARGET))))
# Extract old container and project versions.
$(eval OLD_CONTAINER_VERSION := $(subst v,,$(CONTAINER_$(shell echo $(INPUT_CONTAINER) | tr a-z A-Z)_VERSION)))
$(eval OLD_PROJECT_VERSION := $(subst v,,$(PROJECT_VERSION)))
# Pull docker semver becsause the normal command doesn't seem to work; also we don't need to worry about dependencies.
docker pull usvc/semver:latest
# Bump npm package.json file for selected container
cd $(call container_location,$(INPUT_CONTAINER)) && npm version $(shell docker run usvc/semver:latest bump $(INPUT_SEMANTIC_TYPE) $(OLD_CONTAINER_VERSION))
# Bump the git tag to match the new global version
git tag v$(shell docker run usvc/semver:latest bump $(INPUT_SEMANTIC_TYPE) $(OLD_PROJECT_VERSION))
# Bump the container version and global version in the Makefile
perl -pi -e 's/^PROJECT_VERSION\s*:=\s*\K.*/"v$(shell docker run usvc/semver:latest bump $(INPUT_SEMANTIC_TYPE) $(OLD_PROJECT_VERSION))"/ if /^PROJECT_VERSION\s*:=/' Makefile;
perl -pi -e 's/^CONTAINER_$(shell echo $(INPUT_CONTAINER) | tr a-z A-Z)_VERSION\s*:=\s*\K.*/"v$(shell docker run usvc/semver:latest bump $(INPUT_SEMANTIC_TYPE) $(OLD_CONTAINER_VERSION))"/ if /^CONTAINER_$(shell echo $(INPUT_CONTAINER) | tr a-z A-Z)_VERSION\s*:=/' Makefile;
# This function generates Docker build arguments based on variables defined in the Makefile.
# It extracts variable assignments, removes whitespace, and formats them as build arguments.
# Additionally, it appends any custom shell generated arguments defined below.
define args
$(shell \
grep -E '^[[:alnum:]_]+[[:space:]]*[:?]?[[:space:]]*=' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":="} { \
gsub(/^[[:space:]]+|[[:space:]]+$$/, "", $$2); \
gsub(/^/, "\x27", $$2); \
gsub(/$$/, "\x27", $$2); \
gsub(/[[:space:]]+/, "", $$1); \
gsub(":", "", $$1); \
printf "--build-arg %s=%s ", $$1, $$2 \
}') \
--build-arg BUILD_DATE='"$(shell date)"' \
--build-arg GIT_COMMIT='"$(shell git rev-parse HEAD)"'
endef
# This function generates labels based on variables defined in the Makefile.
# It extracts only the selected container variables and is used to echo this information
# to the docker buildx engine in the command line.
define labels
--label $(PROJECT_ORGANIZATION).image.title=$(CONTAINER_$(1)_NAME) \
--label $(PROJECT_ORGANIZATION).image.description=$(CONTAINER_$(1)_DESCRIPTION) \
--label $(PROJECT_ORGANIZATION).image.authors=$(PROJECT_AUTHORS) \
--label $(PROJECT_ORGANIZATION).image.url=$(PROJECT_SOURCES) \
--label $(PROJECT_ORGANIZATION).image.source=$(PROJECT_SOURCES)/$(CONTAINER_$(1)_LOCATION)
endef
# This function returns a list of container names defined in the Makefile.
# In order for this function to return a container, it needs to have this format: CONTAINER_%_NAME!
define containers
$(strip $(filter-out $(_NL),$(foreach var,$(.VARIABLES),$(if $(filter CONTAINER_%_NAME,$(var)),$(strip $($(var)))))))
endef
define container_build
$(eval CONTAINER := $(word 1,$1))
$(eval ENVIRONMENT := $(word 2,$1))
$(eval ARGS := $(shell echo $(args)))
$(eval VERSION := $(strip $(call container_version,$(CONTAINER))))
$(eval TAG := $(PROJECT_NAME):$(ENVIRONMENT))
@echo "Building container: $(CONTAINER)"
@echo "Environment: $(ENVIRONMENT)"
@echo "Version: $(VERSION)"
@if [ "$(strip $(ENVIRONMENT))" != "local" ] && [ "$(strip $(ENVIRONMENT))" != "release" ]; then \
echo "Invalid environment. Please specify 'local' or 'release'"; \
exit 1; \
fi
$(if $(filter $(strip $(ENVIRONMENT)),release), \
$(eval TAG := $(PROJECT_REGISTRY)/$(PROJECT_NAME):$(VERSION)), \
)
docker buildx build --load -t $(TAG) -f .docker/Dockerfile.$(ENVIRONMENT) ./$(strip $(subst $(SPACE),,$(call container_location,$(CONTAINER))))/. $(ARGS) $(call labels,$(shell echo $(CONTAINER_NAME) | tr '[:lower:]' '[:upper:]')) --no-cache
endef
define container_location
$(strip $(eval CONTAINER_NAME := $(shell echo $(1) | tr '[:lower:]' '[:upper:]'))) \
$(CONTAINER_$(CONTAINER_NAME)_LOCATION)
endef
define container_version
$(strip $(eval CONTAINER_NAME := $(shell echo $(1) | tr '[:lower:]' '[:upper:]'))) \
$(if $(CONTAINER_$(CONTAINER_NAME)_VERSION), \
$(shell echo $(strip $(strip $(CONTAINER_$(CONTAINER_NAME)_VERSION))) | tr -d '[:space:]'), \
$(error Version data for container $(1) not found))
endef

View File

@@ -1,2 +1,3 @@
# Portfolio
My portfolio website!
![Badge](https://hitscounter.dev/api/hit?url=https%3A%2F%2Ftimmypidashev.dev&label=Visits&icon=eye-fill&color=%23198754)
<img src=".github/preview.jpeg" title="Preview"/>

0
compose.local.yml Normal file
View File

32
compose.release.yml Normal file
View File

@@ -0,0 +1,32 @@
services:
caddy:
container_name: caddy
image: caddy:latest
ports:
- 80:80
- 443:443
volumes:
- ./.caddy/Caddyfile.release:/etc/caddy/Caddyfile:rw
networks:
- proxy_network
depends_on:
- timmypidashev.dev
watchtower:
container_name: updates
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- $HOME/.docker/config.json:/config.json
command: --interval 120 --cleanup --label-enable
timmypidashev.dev:
container_name: timmypidashev.dev
image: ghcr.io/timmypidashev/timmypidashev.dev:latest
networks:
- proxy_network
networks:
proxy_network:
name: proxy_network
external: true

View File

@@ -1,81 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Timothy Pidashev</title>
<link rel="shortcut icon" type="image/png" href="static/images/elements/png/timmy.png"/>
<link rel="stylesheet" href="static/styles.css">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/aos@next/dist/aos.css" />
</head>
<body>
<section class="centered">
<div>
<h1 class="hero__header pink__colored" data-aos="fade-right">Hello,</h1>
<h1 class="hero__header" data-aos="fade-left">I'm Tim.</h1>
</div>
<img src="static/images/elements/png/line_short.png" alt="line" class="divider__line" data-aos="flip-left">
</section>
<section class="centered">
<div>
<h1 class="about__header" data-aos="zoom-out">
I'm a 17-year-old on an
<span class="orange__highlight">epic journey</span>
</h1>
<h1 class="about__header" data-aos="flip-down">
to become a
<span class="orange__highlight">software developer!</span>
</h1>
</div>
<img src="static/images/elements/png/line_short.png" alt="line" class="divider__line" data-aos="flip-left">
</section>
<div id="end__of__page" class="centered">
<div class="row">
<div class="logo">
<a href="https://www.youtube.com/channel/UCEpaCDz-wZ21kR8nA8xDSzg" target="_blank">
<img src="static/images/elements/png/youtube.png" alt="youtube logo" data-aos="fade-left" class="logo__image">
</a>
</div>
<div class="logo">
<a href="https://twitter.com/Timothy89184676" target="_blank">
<img src="static/images/elements/png/twitter.png" alt="twitter logo" data-aos="zoom-in" class="logo__image">
</a>
</div>
<div class="logo">
<a href="https://discord.gg/34RqygKbtX" target="_blank">
<img src="static/images/elements/png/discord.png" alt="discord logo" data-aos="fade-right" class="logo__image">
</a>
</div>
</div>
<div class="row">
<div class="logo">
<a href="https://timmyverybored.itch.io/" target="_blank">
<img src="static/images/elements/png/itch.png" alt="itch logo" data-aos="fade-right" class="logo__image">
</a>
</div>
<div class="logo">
<a href="https://github.com/timmypidashev.com" target="_blank">
<img src="static/images/elements/png/github.png" alt="github logo" data-aos="zoom-out" class="logo__image">
</a>
</div>
<div class="logo">
<a href="mailto: pidashev.tim@gmail.com" target="_blank">
<img src="static/images/elements/png/gmail.png" alt="gmail logo" data-aos="fade-left" class="logo__image">
</a>
</div>
</div>
</div>
<script src="https://unpkg.com/aos@next/dist/aos.js"></script>
<script>AOS.init(
{
once: false,
mirror: true,
anchorPlacement: 'top-bottom',
offset: 0,
duration: 800
});</script>
</body>
</html>

6
src/.stackblitzrc Normal file
View File

@@ -0,0 +1,6 @@
{
"startCommand": "npm start",
"env": {
"ENABLE_CJS_IMPORTS": true
}
}

11
src/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Astro with Tailwind
```
npm init astro -- --template with-tailwindcss
```
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/with-tailwindcss)
Astro comes with [Tailwind](https://tailwindcss.com) support out of the box. This example showcases how to style your Astro project with Tailwind.
For complete setup instructions, please see our [Styling Guide](https://docs.astro.build/guides/styling#-tailwind).

188
src/astro.config.mjs Normal file
View File

@@ -0,0 +1,188 @@
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
import tailwind from "@astrojs/tailwind";
import react from "@astrojs/react";
import mdx from "@astrojs/mdx";
import rehypePrettyCode from "rehype-pretty-code";
import rehypeSlug from "rehype-slug";
import sitemap from "@astrojs/sitemap";
// https://astro.build/config
export default defineConfig({
output: "server",
server: {
host: true,
port: 3000,
},
adapter: node({
mode: "standalone",
}),
site: "https://timmypidashev.dev",
build: {
// Enable build-time optimizations
inlineStylesheets: "auto",
// Split large components into smaller chunks
splitComponents: true,
},
integrations: [
tailwind(),
react(),
mdx({
syntaxHighlight: false,
rehypePlugins: [
/**
* Adds ids to headings
*/
rehypeSlug,
[
/**
* Enhances code blocks with syntax highlighting, line numbers,
* titles, and allows highlighting specific lines and words
*/
rehypePrettyCode,
{
theme: {
"name": "Darkbox",
"type": "dark",
"colors": {
"editor.background": "#000000",
"editor.foreground": "#ebdbb2"
},
"tokenColors": [
{
"scope": ["comment", "punctuation.definition.comment"],
"settings": {
"foreground": "#928374",
"fontStyle": "italic"
}
},
{
"scope": ["constant", "variable.other.constant"],
"settings": {
"foreground": "#d3869b"
}
},
{
"scope": "variable",
"settings": {
"foreground": "#ebdbb2"
}
},
{
"scope": ["keyword", "storage.type", "storage.modifier"],
"settings": {
"foreground": "#fb4934"
}
},
{
"scope": ["string", "punctuation.definition.string"],
"settings": {
"foreground": "#b8bb26"
}
},
{
"scope": ["entity.name.function", "support.function"],
"settings": {
"foreground": "#b8bb26"
}
},
{
"scope": "entity.name.type",
"settings": {
"foreground": "#fabd2f"
}
},
{
"scope": ["entity.name.tag", "punctuation.definition.tag"],
"settings": {
"foreground": "#83a598"
}
},
{
"scope": ["entity.other.attribute-name"],
"settings": {
"foreground": "#8ec07c"
}
},
{
"scope": ["punctuation", "meta.brace"],
"settings": {
"foreground": "#ebdbb2"
}
},
{
"scope": "markup.inline.raw",
"settings": {
"foreground": "#fe8019"
}
},
{
"scope": ["markup.heading"],
"settings": {
"foreground": "#b8bb26",
"fontStyle": "bold"
}
},
{
"scope": ["markup.bold"],
"settings": {
"foreground": "#fe8019",
"fontStyle": "bold"
}
},
{
"scope": ["markup.italic"],
"settings": {
"foreground": "#fe8019",
"fontStyle": "italic"
}
},
{
"scope": ["markup.list"],
"settings": {
"foreground": "#83a598"
}
},
{
"scope": ["markup.quote"],
"settings": {
"foreground": "#928374",
"fontStyle": "italic"
}
},
{
"scope": ["markup.link"],
"settings": {
"foreground": "#8ec07c"
}
},
{
"scope": "support.class",
"settings": {
"foreground": "#fabd2f"
}
},
{
"scope": "number",
"settings": {
"foreground": "#d3869b"
}
}
],
},
keepBackground: true,
},
],
],
}),
sitemap({
filter: (page) => {
return !page.includes("/drafts/") && !page.includes("/private/");
},
changefreq: "weekly",
priority: 0.7,
lastmod: new Date(),
}),
],
});

43
src/package.json Normal file
View File

@@ -0,0 +1,43 @@
{
"name": "src",
"version": "2.1.1",
"private": true,
"scripts": {
"dev": "astro dev --host",
"build": "astro build",
"preview": "astro preview"
},
"devDependencies": {
"@astrojs/react": "^4.4.0",
"@astrojs/tailwind": "^6.0.2",
"@tailwindcss/typography": "^0.5.16",
"@types/react": "^18.3.20",
"@types/react-dom": "^18.3.6",
"astro": "^5.14.1",
"tailwindcss": "^3.4.17"
},
"dependencies": {
"@astrojs/mdx": "^4.3.6",
"@astrojs/node": "^9.4.4",
"@astrojs/rss": "^4.0.12",
"@astrojs/sitemap": "^3.6.0",
"@giscus/react": "^3.1.0",
"@pilcrowjs/object-parser": "^0.0.4",
"@react-hook/intersection-observer": "^3.1.2",
"@rehype-pretty/transformers": "^0.13.2",
"arctic": "^3.6.0",
"lucide-react": "^0.468.0",
"marked": "^15.0.8",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.5.0",
"react-responsive": "^10.0.1",
"reading-time": "^1.5.0",
"rehype-pretty-code": "^0.14.1",
"rehype-slug": "^6.0.0",
"schema-dts": "^1.1.5",
"shiki": "^3.12.2",
"typewriter-effect": "^2.21.0",
"unist-util-visit": "^5.0.0"
}
}

5412
src/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

3
src/pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,3 @@
onlyBuiltDependencies:
- esbuild
- sharp

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

14
src/public/favicon.svg Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="16.000000pt" height="16.000000pt" viewBox="0 0 16.000000 16.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,16.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M0 80 l0 -80 30 32 c17 17 30 35 30 39 0 5 12 9 26 9 14 0 22 -4 19
-10 -6 -10 33 -70 47 -70 4 0 8 36 8 80 l0 80 -80 0 -80 0 0 -80z m105 20 c-3
-5 -16 -10 -28 -10 -18 0 -19 2 -7 10 20 13 43 13 35 0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 642 B

BIN
src/public/me.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
src/public/og-image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

65
src/public/pgp.asc Normal file
View File

@@ -0,0 +1,65 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGTwzLsBEADehIeiC1fV/GiRBclTmM9e6rmG29/YQbrRfJZ+sa1gWlAws/yR
sXbrCDh1S/JU85lirhc0A2N+OZSqCSkGUtvDhCttxLi5VUyVxgqshJWN/mU5eZEN
x4/5mpAApV6K2WGjAggJjoHecr/+sRpV3Vq/5ypdp6RLt7zeJolJSzKWpFrGeYTd
CFaZyZbQVw2NeFe9NBoQHDk0mMS+9bVyXQ2oi4fteKCe205cJkEc4Z8q4NzOlquL
BbowQwxKoAosCwz19Em5yeND34WUHJPlgoYTf9HRgsQcmI17hn9Q00gzwm7gotbF
bqWKEMduOzGBRyEEWeZ+l8CcWHAo2NbxZx01L4UsrZ6+MkXWv+2NQBFhPL14UcDg
uuBVvc5rsgeu7Rm8DE2EXTyA3Bszz1r75TbsQwUianma38kxSuiKArUDE+v5X0vv
F6e2z6hkDkKCe53EM9yF4lvVHNsQwxT8RBXPj+sc6Sqv3nIkLnan3++aMhW3up9s
YXKouadmvgB4uGqsBjWOme4ViW46C7rNVRtUKQTdx89cFG1c+GuaI203RbHjAZVJ
M9fBH5Ycdl0ieSCwmerbHfTRudHqPdNLQMvFYGPEj5pXalwtd3j4CEEC/HqOyklz
IN9ReIYIYF0pSC5OmWD3c70vj0INsbWp96eB8skjONjHYL3Y2CXIZ0AzRQARAQAB
tClUaW1vdGh5IFBpZGFzaGV2IDxwaWRhc2hldi50aW1AZ21haWwuY29tPokCTgQT
AQgAOBYhBGjE1l58MsXUX+ieufe9h7j9i0NOBQJk8My7AhsDBQsJCAcCBhUKCQgL
AgQWAgMBAh4BAheAAAoJEPe9h7j9i0NOsxwQALfdoZJAdkBpM2AmsVdx6JqvA08I
p/Xr1YgjwJvziq8fnWpu/AGz9VevFVgAt1h1Dsr4XAolEtQM6+aiNX7HGqyLKqT7
kum/dpnjw0/tiKvv/P2TRc+YZZLOfb+TYa1bZYGVDzGAHAm17yMJTV3rH8tIKNee
VaqWmxMuwmUQXutvF9P2bhaJLOTjGCIVxuAMfLIhRGKz8q8+I5g5aLm/JrpHC0OY
ACGSSj1vP0b5m5BLqqv67GueDHTX6w/7U1LAEspIcs+/GxoA5G9WzZFn4qNdq98h
RPixuY5y5FYKV6FAVGm5Yu3FSPvKpXAfWZIKM3WzKf7BNhUVaB6HNGbCFAMGDcHz
dZ9xXRohWlHidft55qBUpTXAjy3vb2k5eMXGPNMCwQvMyzDZzLkYbN3apuWbjzlQ
ARdlGKpRRzmeAHEmAybX1Fel8dT2DWjP05t2z2BRQAFK9sGE7WzYhvllMp31C7SA
uJZNzgjjs/aI8oiNc1qpeiQxsEGws3OakHRxd1rnM+d4icwTx13u8lMqHnvORgIe
rAa6qKIUnfZLo0ut0X5s8iPjoLZr/qjDGRbVmHH5K/D5Ci6VlsEkmcQ0REE1FWPA
hrlSNfKbeaHWAJ7MFKvNxViy8n7MeoR6Nn7EGoxCfekgGQLWuRNanChJ8dm7HZpq
o/vu4JH94Ui9xDwBtDlUaW1vdGh5IFBpZGFzaGV2ICh0aW1teXBpZGFzaGV2KSA8
bWFpbEB0aW1teXBpZGFzaGV2LmRldj6JAlQEEwEIAD4CGwMFCwkIBwICIgIGFQoJ
CAsCBBYCAwECHgcCF4AWIQRoxNZefDLF1F/onrn3vYe4/YtDTgUCZ5J4fQIZAQAK
CRD3vYe4/YtDTsGjD/4s/pBI8s+zoV+aBBKi9qmIqFAlZ8+JyY4TzAlIa1qZg/Xk
GVEN1+Lwa9m4eI1SFZUOprLPiqqFJ+DSHjrua5FGo56uhYGBEbPBlzIJx0XtXclS
1FmpoDOjY6FFsvrkv19jPYB2oXnPjok/nkRLdNWp1BVqisqFq8f//iynMu6GTndF
cNHf7iwZ+IHytFTiKFCgMg7jPeXofAkpnFXoOB33wn7ED2I26zhMx9wJE1cKApxv
ZmxGtkPk0rv6kiQqGE9zTg+AKSy1+jkXp8eFrnA4P9bIDXxcnDyVs/63X6X/qEK5
Yg1a3Xq1U4d6aoQUqAShpAQGbTmEvKusYXzdd2fFJEd0OExXkG2mWndhkF/9+8Rh
SroaqS+0G3nG38KTvK7OKnyHhuDVjcvJ5QiWVd1T7M3SBDAZwcOmpkw3SN26b8iS
i8iHAUQGjKftG+PDrXvRhMn7lpIshJBXopCGJvzPwLIoMvVzgq0gxAeCUHqI0wr2
EXEgboPW18zcAagI2r4B7p0xVtpJ9qPamYcCPqPMfxA8YYKhyzb8owkFUBboSF8j
ihBz1NN3ph8ZEa1YbxrdJVMkbOlE/O+DDaegxkXG9gSts/nYZXQx5GZ2LyVlHC4z
yVUpGwsRLfSejubhBnkRrXzOn1dFhAL4kIXFvFp4t4ZkYssdWzLVx71UVTyGsrkC
DQRk8My7ARAA1NRA04/vS9Cww0MMFQwaBztEb4INAT3dVxybyPZEIiNGttqGzEc9
EV+5NlcLwygDraXqw+k5GekIE7Mqf3YukeIqA+4TDVpFv26QbtBnLQ01YM7Z0tU4
R/X6IJmn/Uudc5hKLOLms3BH4x6O/XQJERJIALOfMWRfsmcUXw8a05HF5OuNVClT
w8FHVawN7frCJdBsh9g2bGJArwQFCxaLcDgpydUTMxNgxQMLgcAuIk3GiFwwWC4e
HzrAmp1yHn/iDh+UN8zQBjoi/5Ac4uXJlHKGAiakw/NqYlFccno1vUg5kuW+9QN4
ch2fa4zAosd7ObR5uZjNn6sggnq4ejA98vtg5DssCSQpTiFqNu3pBLroh4LsRh3r
THuWnXj4HWKDPZ3odlPVy2sIswtMXO3uygyWLJbPuT824iFwD9imshqsnoMxazb1
W/GuBFyI7ZM8tzCMVNtZExEBqnOwQdjlSgpla6L3UVWs4KL1UEVWm3doFCGgzQbK
JVVH3Uk0Z+w+jylZqXdmSSrB/wkg+j9QK2VxewEP0onS4FBhoJsaezLL5fTYpOy5
yAx2k3lqa3YF51ulPoGGg3u75R/37zt8VT3rfEXuCjtHd/H4fieluAOW2w4phXrC
u8iMq0eChaedVZsAAsy+DW9Ighf8zy8x/HQ0MKaFGI59B6BOsL6f/2MAEQEAAYkC
NgQYAQgAIBYhBGjE1l58MsXUX+ieufe9h7j9i0NOBQJk8My7AhsMAAoJEPe9h7j9
i0NOWvEQAJl5UqnzxM9+aYQcCw1qxGYTxdui7mE8QHDU9L7sz5vPeLVXrkxZfFsR
+2Y6S92ySk+pTZv8/+TztxYczr3tJ/TUpj6jM7jJROo2BUcAph4wSBD/vxqCN40g
XYcvbzbFvmbXJL+I2h/C3Ja7O0DqlDqRB2icaCYqVK0aDFfe5ldHbfs+w4Ox/zh6
7kkchA+WauRadwyaqWK3XGdusTw3ZcaFRSGfTRCQfM2U1761mRBppeDhOPoZ6Ymy
rWUpiOE7SJNe+gxyX0wVGv/CuUr5RaDEwvl7A5fUA9Sdvtxdr3Eixd6lsCRWoAtq
1woDoqpDZpS0wlb28r7/ZtvAUR1EovAOwy41GNri9AwyMeRSg3NLqb21c9yhEV5a
cWdGzioSjk89dd1c/pzuHPZ1cTRiizH8SRQjn/rLqJTnH1vVhLFmLv/Ywr9LOw2s
RcKxrByu0O+j96zR+6dqsiCo4i0CUS7P1shQlzhRuz6o3eZ1IlepAlif2LDW5waO
KdFJe2hD0oe4JKO9TbzJOeupFW1C/TBzdLif3K7VsKVdfPBL1YWinf7V7gryxJdc
pAE4764aIhfCkLa85tt7Tn/ii4ZLLMQG+Ww7LJ7BRarMlcyrw3nf0dICLxFgAnMS
vclOqTbTH082uTM/wa3dhxByz0rKZEz7xXAjPiZfK+2nncOdQhAm
=T4Wx
-----END PGP PUBLIC KEY BLOCK-----

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

1
src/public/scripts Submodule

Submodule src/public/scripts added at b555dc1e10

11
src/sandbox.config.json Normal file
View File

@@ -0,0 +1,11 @@
{
"infiniteLoopProtection": true,
"hardReloadOnChange": false,
"view": "browser",
"template": "node",
"container": {
"port": 3000,
"startScript": "start",
"node": "14"
}
}

View File

@@ -0,0 +1,56 @@
import React, { useState, useEffect } from "react";
const GlitchText = () => {
const originalText = 'Error 404';
const [characters, setCharacters] = useState(
originalText.split("").map(char => ({ char, isGlitched: false }))
);
const glitchChars = "!<>-_\\/[]{}—=+*^?#________";
useEffect(() => {
const glitchInterval = setInterval(() => {
if (Math.random() < 0.2) { // 20% chance to trigger glitch
setCharacters(prev => {
return originalText.split('').map((originalChar, index) => {
if (Math.random() < 0.3) { // 30% chance to glitch each character
return {
char: glitchChars[Math.floor(Math.random() * glitchChars.length)],
isGlitched: true
};
}
return { char: originalChar, isGlitched: false };
});
});
// Reset after short delay
setTimeout(() => {
setCharacters(originalText.split('').map(char => ({
char,
isGlitched: false
})));
}, 100);
}
}, 50);
return () => clearInterval(glitchInterval);
}, []);
return (
<div className="relative">
<h1 className="text-6xl font-bold mb-4 relative">
<span className="relative inline-block">
{characters.map((charObj, index) => (
<span
key={index}
className={charObj.isGlitched ? "text-red" : "text-purple"}
>
{charObj.char}
</span>
))}
</span>
</h1>
</div>
);
};
export default GlitchText;

View File

@@ -0,0 +1,134 @@
import React from 'react';
import { Code2, BookOpen, RocketIcon, Compass } from 'lucide-react';
export default function CurrentFocus() {
const recentProjects = [
{
title: "Darkbox",
description: "My gruvbox theme, with a pure black background",
href: "/projects/darkbox",
tech: ["Neovim", "Lua"]
},
{
title: "Revive Auto Parts",
description: "A car parts listing site built for a client",
href: "/projects/reviveauto",
tech: ["Tanstack", "React Query", "Fastapi"]
},
{
title: "Fhccenter",
description: "Website made for a private school",
href: "/projects/fhccenter",
tech: ["Nextjs", "Typescript", "Prisma"]
}
];
return (
<div className="flex justify-center items-center w-full">
<div className="w-full max-w-6xl p-4 sm:px-6 py-6 sm:py-8">
<h2 className="text-3xl sm:text-4xl font-bold text-center text-yellow-bright mb-8 sm:mb-12">
Current Focus
</h2>
{/* Recent Projects Section */}
<div className="mb-8 sm:mb-16">
<div className="flex items-center justify-center gap-2 mb-6">
<Code2 className="text-yellow-bright" size={24} />
<h3 className="text-xl font-bold text-foreground/90">Recent Projects</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 max-w-5xl mx-auto">
{recentProjects.map((project) => (
<a
href={project.href}
key={project.title}
className="p-4 sm:p-6 rounded-lg border border-foreground/10 hover:border-yellow-bright/50
transition-all duration-300 group bg-background/50"
>
<h4 className="font-bold text-lg group-hover:text-yellow-bright transition-colors">
{project.title}
</h4>
<p className="text-foreground/70 mt-2 text-sm sm:text-base">{project.description}</p>
<div className="flex flex-wrap gap-2 mt-3">
{project.tech.map((tech) => (
<span key={tech} className="text-xs px-2 py-1 rounded-full bg-foreground/5 text-foreground/60">
{tech}
</span>
))}
</div>
</a>
))}
</div>
</div>
{/* Current Learning & Interests */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8 max-w-5xl mx-auto">
{/* What I'm Learning */}
<div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50">
<div className="flex items-center justify-center gap-2">
<BookOpen className="text-green-bright" size={24} />
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Currently Learning</h3>
</div>
<ul className="space-y-3 text-sm sm:text-base text-foreground/70">
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-green-bright flex-shrink-0" />
<span>Rust Programming</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-green-bright flex-shrink-0" />
<span>WebAssembly with Rust</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-green-bright flex-shrink-0" />
<span>HTTP/3 & WebTransport</span>
</li>
</ul>
</div>
{/* Project Interests */}
<div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50">
<div className="flex items-center justify-center gap-2">
<RocketIcon className="text-blue-bright" size={24} />
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Project Interests</h3>
</div>
<ul className="space-y-3 text-sm sm:text-base text-foreground/70">
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-bright flex-shrink-0" />
<span>AI Model Integration</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-bright flex-shrink-0" />
<span>Rust Systems Programming</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-bright flex-shrink-0" />
<span>Cross-platform WASM Apps</span>
</li>
</ul>
</div>
{/* Areas to Explore */}
<div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50">
<div className="flex items-center justify-center gap-2">
<Compass className="text-purple-bright" size={24} />
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Want to Explore</h3>
</div>
<ul className="space-y-3 text-sm sm:text-base text-foreground/70">
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
<span>LLM Fine-tuning</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
<span>Rust 2024 Edition</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
<span>Real-time Web Transport</span>
</li>
</ul>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,65 @@
import React from "react";
import { ChevronDownIcon } from "@/components/icons";
export default function Intro() {
const scrollToNext = () => {
const nextSection = document.querySelector("section")?.nextElementSibling;
if (nextSection) {
const offset = nextSection.offsetTop - (window.innerHeight - nextSection.offsetHeight) / 2; // Center the section
window.scrollTo({
top: offset,
behavior: "smooth"
});
}
};
return (
<div className="w-full max-w-4xl px-4">
<div className="space-y-8 md:space-y-12">
<div className="flex flex-col sm:flex-row items-center sm:items-center justify-center gap-8 sm:gap-16">
<div className="w-32 h-32 sm:w-48 sm:h-48 shrink-0">
<img
src="/me.jpeg"
alt="Timothy Pidashev"
className="rounded-lg object-cover w-full h-full ring-2 ring-yellow-bright hover:ring-orange-bright transition-all duration-300"
/>
</div>
<div className="text-center sm:text-left space-y-4 sm:space-y-6">
<h2 className="text-xl sm:text-5xl font-bold text-yellow-bright">
Timothy Pidashev
</h2>
<div className="text-sm sm:text-xl text-foreground/70 space-y-2 sm:space-y-3">
<p className="flex items-center justify-center font-bold sm:justify-start gap-2">
<span className="text-blue">Software Systems Engineer</span>
</p>
<p className="flex items-center justify-center font-bold sm:justify-start gap-2">
<span className="text-green">Open Source Enthusiast</span>
</p>
<p className="flex items-center justify-center font-bold sm:justify-start gap-2">
<span className="text-yellow">Coffee Connoisseur</span>
</p>
</div>
</div>
</div>
<div className="space-y-8">
<p className="text-foreground/80 text-center text-base sm:text-2xl italic max-w-3xl mx-auto font-medium">
"Turning coffee into code" isn't just a clever phrase
<span className="text-aqua-bright"> it's how I approach each project:</span>
<span className="text-purple-bright"> methodically,</span>
<span className="text-blue-bright"> with attention to detail,</span>
<span className="text-green-bright"> and a refined process.</span>
</p>
<div className="flex justify-center">
<button
onClick={scrollToNext}
className="text-foreground/50 hover:text-yellow-bright transition-colors duration-300"
aria-label="Scroll to next section"
>
<ChevronDownIcon size={40} className="animate-bounce" />
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,65 @@
import React from 'react';
import { Fish, Mountain, Book, Car } from 'lucide-react';
export default function OutsideCoding() {
const interests = [
{
icon: <Fish className="text-blue-bright" size={20} />,
title: "Fishing",
description: "Finding peace and adventure on the water, always looking for the next great fishing spot"
},
{
icon: <Mountain className="text-green-bright" size={20} />,
title: "Hiking",
description: "Exploring trails with friends and seeking out scenic viewpoints in nature"
},
{
icon: <Book className="text-purple-bright" size={20} />,
title: "Reading",
description: "Deep diving into novels & technical books that expand my horizons & captivate my mind"
},
{
icon: <Car className="text-yellow-bright" size={20} />,
title: "Project Cars",
description: "Working on automotive projects, modifying & restoring sporty sedans"
}
];
return (
<div className="flex justify-center items-center w-full">
<div className="w-full max-w-4xl px-4 py-8">
<h2 className="text-2xl md:text-4xl font-bold text-center text-yellow-bright mb-8">
Outside of Programming
</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{interests.map((interest) => (
<div
key={interest.title}
className="flex flex-col items-center text-center p-4 rounded-lg border border-foreground/10
hover:border-yellow-bright/50 transition-all duration-300 bg-background/50"
>
<div className="mb-3">
{interest.icon}
</div>
<h3 className="font-bold text-foreground/90 mb-2">
{interest.title}
</h3>
<p className="text-sm text-foreground/70">
{interest.description}
</p>
</div>
))}
</div>
<p className="text-center text-foreground/80 mt-8 max-w-2xl mx-auto text-sm md:text-base italic">
When I'm not writing code, you'll find me
<span className="text-blue-bright"> out on the water,</span>
<span className="text-green-bright"> hiking trails,</span>
<span className="text-purple-bright"> reading books,</span>
<span className="text-yellow-bright"> or modifying my current ride.</span>
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,150 @@
import React, { useState, useEffect } from 'react';
export const ActivityGrid = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('/api/wakatime');
if (!response.ok) {
throw new Error('Failed to fetch data');
}
const result = await response.json();
setData(result.data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
// Get intensity based on coding hours (0-4 for different shades)
const getIntensity = (hours) => {
if (hours === 0) return 0;
if (hours < 2) return 1;
if (hours < 4) return 2;
if (hours < 6) return 3;
return 4;
};
// Get color class based on intensity
const getColorClass = (intensity) => {
if (intensity === 0) return 'bg-foreground/5';
if (intensity === 1) return 'bg-green-DEFAULT/30';
if (intensity === 2) return 'bg-green-DEFAULT/60';
if (intensity === 3) return 'bg-green-DEFAULT/80';
return 'bg-green-bright';
};
// Group data by week
const weeks = [];
let currentWeek = [];
if (data.length > 0) {
data.forEach((day, index) => {
currentWeek.push(day);
if (currentWeek.length === 7 || index === data.length - 1) {
weeks.push(currentWeek);
currentWeek = [];
}
});
}
if (loading) {
return (
<div className="bg-background border border-foreground/10 rounded-lg p-6">
<div className="text-lg text-aqua-bright mb-6">Loading activity data...</div>
</div>
);
}
if (error) {
return (
<div className="bg-background border border-foreground/10 rounded-lg p-6">
<div className="text-lg text-red-bright mb-6">Error loading activity: {error}</div>
</div>
);
}
return (
<div className="bg-background border border-foreground/10 rounded-lg p-6 hover:border-foreground/20 transition-colors">
<div className="text-lg text-aqua-bright mb-6">Activity</div>
<div className="flex gap-4">
{/* Days labels */}
<div className="flex flex-col gap-2 pt-6 text-xs">
{days.map((day, i) => (
<div key={day} className="h-3 text-foreground/60">{i % 2 === 0 ? day : ''}</div>
))}
</div>
{/* Grid */}
<div className="flex-grow overflow-x-auto">
<div className="flex gap-2">
{weeks.map((week, weekIndex) => (
<div key={weekIndex} className="flex flex-col gap-2">
{week.map((day, dayIndex) => {
const hours = day.grand_total.total_seconds / 3600;
const intensity = getIntensity(hours);
return (
<div
key={dayIndex}
className={`w-3 h-3 rounded-sm ${getColorClass(intensity)}
hover:ring-1 hover:ring-foreground/30 transition-all cursor-pointer
group relative`}
>
{/* Tooltip */}
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 p-2
bg-background border border-foreground/10 rounded-md opacity-0
group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap text-xs">
{hours.toFixed(1)} hours on {day.date}
</div>
</div>
);
})}
</div>
))}
</div>
{/* Months labels */}
<div className="flex text-xs text-foreground/60 mt-2">
{weeks.map((week, i) => {
const date = new Date(week[0].date);
const isFirstOfMonth = date.getDate() <= 7;
return (
<div
key={i}
className="w-3 mx-1"
style={{ marginLeft: i === 0 ? '0' : undefined }}
>
{isFirstOfMonth && months[date.getMonth()]}
</div>
);
})}
</div>
</div>
</div>
{/* Legend */}
<div className="flex items-center gap-2 mt-4 text-xs text-foreground/60">
<span>Less</span>
{[0, 1, 2, 3, 4].map((intensity) => (
<div
key={intensity}
className={`w-3 h-3 rounded-sm ${getColorClass(intensity)}`}
/>
))}
<span>More</span>
</div>
</div>
);
};
export default ActivityGrid;

View File

@@ -0,0 +1,213 @@
import { useState, useEffect } from "react";
const Stats = () => {
const [stats, setStats] = useState<any>(null);
const [count, setCount] = useState(0);
const [isFinished, setIsFinished] = useState(false);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
setIsVisible(true);
const fetchStats = async () => {
try {
const res = await fetch("/api/wakatime/alltime");
const data = await res.json();
setStats(data.data);
startCounting(data.data.total_seconds);
} catch (error) {
console.error("Error fetching stats:", error);
}
};
fetchStats();
}, []);
const startCounting = (totalSeconds: number) => {
const duration = 2000;
const steps = 60;
let currentStep = 0;
const timer = setInterval(() => {
currentStep += 1;
if (currentStep >= steps) {
setCount(totalSeconds);
setIsFinished(true);
clearInterval(timer);
return;
}
const progress = 1 - Math.pow(1 - currentStep / steps, 4);
setCount(Math.floor(totalSeconds * progress));
}, duration / steps);
return () => clearInterval(timer);
};
if (!stats) return null;
const hours = Math.floor(count / 3600);
const formattedHours = hours.toLocaleString("en-US", {
minimumIntegerDigits: 4,
useGrouping: true
});
return (
<div className="flex flex-col items-center justify-center min-h-[50vh] gap-6">
<div className={`
text-2xl opacity-0
${isVisible ? "animate-fade-in-first" : ""}
`}>
I've spent
</div>
<div className="relative">
<div className="text-8xl text-center relative z-10">
<span className="font-bold relative">
<span className={`
bg-gradient-text opacity-0
${isVisible ? "animate-fade-in-second" : ""}
`}>
{formattedHours}
</span>
</span>
<span className={`
text-4xl opacity-0
${isVisible ? "animate-slide-in-hours" : ""}
`}>
hours
</span>
</div>
</div>
<div className="flex flex-col items-center gap-3 text-center">
<div className={`
text-xl opacity-0
${isVisible ? "animate-fade-in-third" : ""}
`}>
writing code & building apps
</div>
<div className={`
flex items-center gap-3 text-lg opacity-0
${isVisible ? "animate-fade-in-fourth" : ""}
`}>
<span>since</span>
<span className="text-green-bright font-bold">{stats.range.start_text}</span>
</div>
</div>
<style jsx>{`
.bg-gradient-text {
background: linear-gradient(
90deg,
#fbbf24,
#f59e0b,
#d97706,
#b45309,
#f59e0b,
#fbbf24
);
background-size: 200% auto;
color: transparent;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.animate-gradient {
animation: gradient 4s linear infinite;
}
@keyframes gradient {
0% { background-position: 0% 50%; }
100% { background-position: 200% 50%; }
}
@keyframes fadeInFirst {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 0.8;
transform: translateY(0);
}
}
@keyframes fadeInSecond {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideInHours {
0% {
opacity: 0;
transform: translateX(20px);
margin-left: 0;
}
100% {
opacity: 0.6;
transform: translateX(0);
margin-left: 1rem;
}
}
@keyframes fadeInThird {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 0.8;
transform: translateY(0);
}
}
@keyframes fadeInFourth {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 0.6;
transform: translateY(0);
}
}
.animate-fade-in-first {
animation: fadeInFirst 0.7s ease-out forwards;
}
.animate-fade-in-second {
animation: fadeInSecond 0.7s ease-out forwards;
animation-delay: 0.4s;
}
.animate-slide-in-hours {
animation: slideInHours 0.7s ease-out forwards;
animation-delay: 0.6s;
}
.animate-fade-in-third {
animation: fadeInThird 0.7s ease-out forwards;
animation-delay: 0.8s;
}
.animate-fade-in-fourth {
animation: fadeInFourth 0.7s ease-out forwards;
animation-delay: 1s;
}
`}</style>
</div>
);
};
export default Stats;

View File

@@ -0,0 +1,175 @@
import { useState, useEffect } from "react";
import { Clock, CalendarClock, CodeXml, Computer } from "lucide-react";
import { ActivityGrid } from "@/components/about/stats-activity";
const DetailedStats = () => {
const [stats, setStats] = useState(null);
const [activity, setActivity] = useState(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
fetch("/api/wakatime/detailed")
.then(res => res.json())
.then(data => {
setStats(data.data);
setIsVisible(true);
})
.catch(error => {
console.error("Error fetching stats:", error);
});
fetch("/api/wakatime/activity")
.then(res => res.json())
.then(data => {
setActivity(data.data);
})
.catch(error => {
console.error("Error fetching activity:", error);
});
}, []);
if (!stats) return null;
const progressColors = [
"bg-red-bright",
"bg-orange-bright",
"bg-yellow-bright",
"bg-green-bright",
"bg-blue-bright",
"bg-purple-bright",
"bg-aqua-bright"
];
return (
<div className="flex flex-col gap-10 md:py-32 w-full max-w-[1200px] mx-auto px-4">
<h2 className="text-2xl md:text-4xl font-bold text-center text-yellow-bright">
Weekly Statistics
</h2>
{/* Top Stats Grid */}
<div className={`
grid grid-cols-1 md:grid-cols-2 gap-8
transition-all duration-700 transform
${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-4 opacity-0'}
`}>
{/* Total Time */}
<StatsCard
title="Total Time"
value={`${Math.round(stats.total_seconds / 3600 * 10) / 10}`}
unit="hours"
subtitle="this week"
color="text-yellow-bright"
icon={Clock}
iconColor="stroke-yellow-bright"
/>
{/* Daily Average */}
<StatsCard
title="Daily Average"
value={`${Math.round(stats.daily_average / 3600 * 10) / 10}`}
unit="hours"
subtitle="per day"
color="text-orange-bright"
icon={CalendarClock}
iconColor="stroke-orange-bright"
/>
{/* Editors */}
<StatsCard
title="Primary Editor"
value={stats.editors?.[0]?.name || "None"}
unit={`${Math.round(stats.editors?.[0]?.percent || 0)}%`}
subtitle="of the time"
color="text-blue-bright"
icon={CodeXml}
iconColor="stroke-blue-bright"
/>
{/* OS */}
<StatsCard
title="Operating System"
value={stats.operating_systems?.[0]?.name || "None"}
unit={`${Math.round(stats.operating_systems?.[0]?.percent || 0)}%`}
subtitle="of the time"
color="text-green-bright"
icon={Computer}
iconColor="stroke-green-bright"
/>
</div>
{/* Languages */}
<div className={`
transition-all duration-700 delay-200 transform
${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-4 opacity-0'}
`}>
<DetailCard
title="Languages"
items={stats.languages?.slice(0, 7).map((lang, index) => ({
name: lang.name,
value: Math.round(lang.percent) + '%',
time: Math.round(lang.total_seconds / 3600 * 10) / 10 + ' hrs',
color: progressColors[index % progressColors.length]
})) || []}
titleColor="text-purple-bright"
/>
{/* Activity Grid */}
{activity && (
<div className={`
transition-all duration-700 delay-300 transform
${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-4 opacity-0'}
`}>
<ActivityGrid data={activity} />
</div>
)}
</div>
</div>
);
};
const StatsCard = ({ title, value, unit, subtitle, color, icon: Icon, iconColor }) => (
<div className="bg-background border border-foreground/10 rounded-lg p-6 hover:border-yellow transition-colors flex items-center justify-center">
<div className="flex gap-3 items-center">
<Icon className={`w-6 h-6 ${iconColor}`} strokeWidth={1.5} />
<div className="flex flex-col items-center">
<div className={`${color} text-lg mb-1`}>{title}</div>
<div className="flex items-baseline gap-2">
<div className="text-2xl font-bold">{value}</div>
<div className="text-lg opacity-80">{unit}</div>
</div>
<div className="text-sm opacity-60 mt-1">{subtitle}</div>
</div>
</div>
</div>
);
const DetailCard = ({ title, items, titleColor }) => (
<div className="bg-background border border-foreground/10 rounded-lg p-6 hover:border-yellow transition-colors">
<div className={`${titleColor} mb-6 text-lg`}>{title}</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-6">
{items.map((item) => (
<div key={item.name} className="flex flex-col gap-2">
<div className="flex justify-between items-center">
<span className="text-base font-medium">{item.name}</span>
<span className="text-base opacity-80">{item.value}</span>
</div>
<div className="flex gap-3 items-center">
<div className="flex-grow h-2 bg-foreground/5 rounded-full overflow-hidden">
<div
className={`h-full ${item.color} rounded-full transition-all duration-1000`}
style={{
width: item.value,
opacity: '0.8'
}}
/>
</div>
<span className="text-sm text-foreground/60 min-w-[70px] text-right">{item.time}</span>
</div>
</div>
))}
</div>
</div>
);
export default DetailedStats;

View File

@@ -0,0 +1,81 @@
import React from "react";
import { Check, Code, GitBranch, Star } from "lucide-react";
export default function Timeline() {
const timelineItems = [
{
year: "2024",
title: "Present",
description: "The wisdom of past ventures now flows through my work, whether crafting elegant CRUD applications or embarking on bold projects that expand my limits.",
technologies: ["Rust", "Typescript", "Go", "Postgres"],
icon: <Code className="text-yellow-bright" size={20} />
},
{
year: "2022",
title: "Diving Deeper",
description: "The worlds of systems programming and scalable infrastructure collided as I explored low-level C++ graphics programming and containerization with Docker.",
technologies: ["C++", "Cmake", "Docker", "Docker Compose"],
icon: <GitBranch className="text-green-bright" size={20} />
},
{
year: "2020",
title: "Exploring the Stack",
description: "Starting with pure HTML and CSS, I explored the foundations of web development, gradually venturing into JavaScript and React to bring my static pages to life.",
technologies: ["Javascript", "Tailwind", "React", "Express"],
icon: <Star className="text-blue-bright" size={20} />
},
{
year: "2018",
title: "Starting the Journey",
description: "An elective Python class in 8th grade transformed my keen interest in programming into a relentless obsession, one that drove me to constantly explore new depths.",
technologies: ["Python", "Discord.py", "Asyncio", "Sqlite"],
icon: <Check className="text-purple-bright" size={20} />
}
];
return (
<div className="w-full max-w-6xl px-4 py-8 relative z-0">
<h2 className="text-2xl md:text-4xl font-bold text-center text-yellow-bright mb-8 md:mb-12">
Journey Through Code
</h2>
<div className="relative">
<div className="absolute left-4 sm:left-1/2 h-full w-0.5 bg-foreground/10 -translate-x-1/2" />
<div className="ml-8 sm:ml-0">
{timelineItems.map((item, index) => (
<div key={item.year} className="relative mb-8 md:mb-12 last:mb-0">
<div className={`flex flex-col sm:flex-row items-start ${
index % 2 === 0 ? 'sm:flex-row-reverse' : ''
}`}>
<div className="absolute -left-8 sm:left-1/2 w-6 h-6 sm:w-8 sm:h-8 bg-background
rounded-full border-2 border-yellow-bright sm:-translate-x-1/2
flex items-center justify-center z-10">
{item.icon}
</div>
<div className={`w-full sm:w-[calc(50%-32px)] ${
index % 2 === 0 ? 'sm:pr-8 md:pr-12' : 'sm:pl-8 md:pl-12'
}`}>
<div className="p-4 sm:p-6 bg-background/50 rounded-lg border border-foreground/10
hover:border-yellow-bright/50 transition-colors duration-300">
<span className="text-xs sm:text-sm font-mono text-yellow-bright">{item.year}</span>
<h3 className="text-lg sm:text-xl font-bold text-foreground/90 mt-2">{item.title}</h3>
<p className="text-sm sm:text-base text-foreground/70 mt-2">{item.description}</p>
<div className="flex flex-wrap gap-2 mt-3">
{item.technologies.map((tech) => (
<span key={tech}
className="px-2 py-1 text-xs sm:text-sm rounded-full bg-foreground/5
text-foreground/60 hover:text-yellow-bright transition-colors duration-300">
{tech}
</span>
))}
</div>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,706 @@
import { useEffect, useRef } from "react";
interface Cell {
alive: boolean;
next: boolean;
color: [number, number, number];
baseColor: [number, number, number]; // Original color
currentX: number;
currentY: number;
targetX: number;
targetY: number;
opacity: number;
targetOpacity: number;
scale: number;
targetScale: number;
elevation: number; // For 3D effect
targetElevation: number;
transitioning: boolean;
transitionComplete: boolean;
rippleEffect: number; // For ripple animation
rippleStartTime: number; // When ripple started
rippleDistance: number; // Distance from ripple center
}
interface Grid {
cells: Cell[][];
cols: number;
rows: number;
offsetX: number;
offsetY: number;
}
interface MousePosition {
x: number;
y: number;
isDown: boolean;
lastClickTime: number;
cellX: number;
cellY: number;
}
interface BackgroundProps {
layout?: 'index' | 'sidebar';
position?: 'left' | 'right';
}
const CELL_SIZE_MOBILE = 15;
const CELL_SIZE_DESKTOP = 25;
const TARGET_FPS = 60; // Target frame rate
const CYCLE_TIME = 3000; // 3 seconds per full cycle, regardless of FPS
const TRANSITION_SPEED = 0.05;
const SCALE_SPEED = 0.05;
const INITIAL_DENSITY = 0.15;
const SIDEBAR_WIDTH = 240;
const MOUSE_INFLUENCE_RADIUS = 150; // Radius of mouse influence in pixels
const COLOR_SHIFT_AMOUNT = 30; // Maximum color shift amount
const RIPPLE_SPEED = 0.02; // Speed of ripple propagation
const RIPPLE_ELEVATION_FACTOR = 4; // Height of ripple wave
const ELEVATION_FACTOR = 8; // Max height for 3D effect - reduced for more subtle effect
const Background: React.FC<BackgroundProps> = ({
layout = 'index',
position = 'left'
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const gridRef = useRef<Grid>();
const animationFrameRef = useRef<number>();
const lastUpdateTimeRef = useRef<number>(0);
const lastCycleTimeRef = useRef<number>(0);
const resizeTimeoutRef = useRef<NodeJS.Timeout>();
const mouseRef = useRef<MousePosition>({
x: -1000,
y: -1000,
isDown: false,
lastClickTime: 0,
cellX: -1,
cellY: -1
});
const randomColor = (): [number, number, number] => {
const colors = [
[204, 36, 29], // red
[152, 151, 26], // green
[215, 153, 33], // yellow
[69, 133, 136], // blue
[177, 98, 134], // purple
[104, 157, 106] // aqua
];
return colors[Math.floor(Math.random() * colors.length)];
};
const getCellSize = () => {
// Check if we're on mobile based on screen width
const isMobile = window.innerWidth <= 768;
return isMobile ? CELL_SIZE_MOBILE : CELL_SIZE_DESKTOP;
};
const calculateGridDimensions = (width: number, height: number) => {
const cellSize = getCellSize();
const cols = Math.floor(width / cellSize);
const rows = Math.floor(height / cellSize);
const offsetX = Math.floor((width - (cols * cellSize)) / 2);
const offsetY = Math.floor((height - (rows * cellSize)) / 2);
return { cols, rows, offsetX, offsetY };
};
const initGrid = (width: number, height: number): Grid => {
const { cols, rows, offsetX, offsetY } = calculateGridDimensions(width, height);
const cells = Array(cols).fill(0).map((_, i) =>
Array(rows).fill(0).map((_, j) => {
const baseColor = randomColor();
return {
alive: Math.random() < INITIAL_DENSITY,
next: false,
color: [...baseColor] as [number, number, number],
baseColor: baseColor,
currentX: i,
currentY: j,
targetX: i,
targetY: j,
opacity: 0,
targetOpacity: 0,
scale: 0,
targetScale: 0,
elevation: 0,
targetElevation: 0,
transitioning: false,
transitionComplete: false,
rippleEffect: 0,
rippleStartTime: 0,
rippleDistance: 0
};
})
);
const grid = { cells, cols, rows, offsetX, offsetY };
computeNextState(grid);
// Initialize cells with staggered animation
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const cell = cells[i][j];
if (cell.next) {
cell.alive = true;
setTimeout(() => {
cell.targetOpacity = 1;
cell.targetScale = 1;
}, Math.random() * 1000);
} else {
cell.alive = false;
}
}
}
return grid;
};
const countNeighbors = (grid: Grid, x: number, y: number): { count: number, colors: [number, number, number][] } => {
const neighbors = { count: 0, colors: [] as [number, number, number][] };
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
if (i === 0 && j === 0) continue;
const col = (x + i + grid.cols) % grid.cols;
const row = (y + j + grid.rows) % grid.rows;
if (grid.cells[col][row].alive) {
neighbors.count++;
neighbors.colors.push(grid.cells[col][row].baseColor);
}
}
}
return neighbors;
};
const averageColors = (colors: [number, number, number][]): [number, number, number] => {
if (colors.length === 0) return [0, 0, 0];
const sum = colors.reduce((acc, color) => [
acc[0] + color[0],
acc[1] + color[1],
acc[2] + color[2]
], [0, 0, 0]);
return [
Math.round(sum[0] / colors.length),
Math.round(sum[1] / colors.length),
Math.round(sum[2] / colors.length)
];
};
const computeNextState = (grid: Grid) => {
// First, calculate the next state for all cells based on standard rules
for (let i = 0; i < grid.cols; i++) {
for (let j = 0; j < grid.rows; j++) {
const cell = grid.cells[i][j];
const { count, colors } = countNeighbors(grid, i, j);
// Standard Conway's Game of Life rules
if (cell.alive) {
cell.next = count === 2 || count === 3;
} else {
cell.next = count === 3;
if (cell.next) {
cell.baseColor = averageColors(colors);
cell.color = [...cell.baseColor];
}
}
}
}
// Then, set up animations for cells that need to change state
for (let i = 0; i < grid.cols; i++) {
for (let j = 0; j < grid.rows; j++) {
const cell = grid.cells[i][j];
if (cell.alive !== cell.next && !cell.transitioning) {
cell.transitioning = true;
cell.transitionComplete = false;
// Random delay for staggered animation effect
const delay = Math.random() * 800;
setTimeout(() => {
if (!cell.next) {
cell.targetScale = 0;
cell.targetOpacity = 0;
cell.targetElevation = 0;
} else {
cell.targetScale = 1;
cell.targetOpacity = 1;
cell.targetElevation = 0;
}
}, delay);
}
}
}
};
const createRippleEffect = (grid: Grid, centerX: number, centerY: number) => {
const currentTime = Date.now();
for (let i = 0; i < grid.cols; i++) {
for (let j = 0; j < grid.rows; j++) {
const cell = grid.cells[i][j];
// Calculate distance from cell to ripple center
const dx = i - centerX;
const dy = j - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
// Only apply ripple to visible cells
if (cell.opacity > 0.1) {
cell.rippleStartTime = currentTime + distance * 100; // Delayed start based on distance
cell.rippleDistance = distance;
cell.rippleEffect = 0;
}
}
}
};
const spawnCellAtPosition = (grid: Grid, x: number, y: number) => {
if (x >= 0 && x < grid.cols && y >= 0 && y < grid.rows) {
const cell = grid.cells[x][y];
if (!cell.alive && !cell.transitioning) {
cell.alive = true;
cell.next = true;
cell.transitioning = true;
cell.transitionComplete = false;
cell.baseColor = randomColor();
cell.color = [...cell.baseColor];
cell.targetScale = 1;
cell.targetOpacity = 1;
cell.targetElevation = 0;
// Create a small ripple from the new cell
createRippleEffect(grid, x, y);
}
}
};
const updateCellAnimations = (grid: Grid, deltaTime: number) => {
const mouseX = mouseRef.current.x;
const mouseY = mouseRef.current.y;
const cellSize = getCellSize();
// Adjust transition speeds based on time
const transitionFactor = TRANSITION_SPEED * (deltaTime / (1000 / TARGET_FPS));
const scaleFactor = SCALE_SPEED * (deltaTime / (1000 / TARGET_FPS));
for (let i = 0; i < grid.cols; i++) {
for (let j = 0; j < grid.rows; j++) {
const cell = grid.cells[i][j];
// Smooth transitions
cell.opacity += (cell.targetOpacity - cell.opacity) * transitionFactor;
cell.scale += (cell.targetScale - cell.scale) * scaleFactor;
cell.elevation += (cell.targetElevation - cell.elevation) * scaleFactor;
// Apply mouse interaction
const cellCenterX = grid.offsetX + i * cellSize + cellSize / 2;
const cellCenterY = grid.offsetY + j * cellSize + cellSize / 2;
const dx = cellCenterX - mouseX;
const dy = cellCenterY - mouseY;
const distanceToMouse = Math.sqrt(dx * dx + dy * dy);
// 3D hill effect based on mouse position
if (distanceToMouse < MOUSE_INFLUENCE_RADIUS && cell.opacity > 0.1) {
// Calculate height based on distance - peak at center, gradually decreasing
const influenceFactor = Math.cos((distanceToMouse / MOUSE_INFLUENCE_RADIUS) * Math.PI / 2);
// Only positive elevation (growing upward)
cell.targetElevation = ELEVATION_FACTOR * influenceFactor * influenceFactor; // squared for more pronounced effect
// Slight color shift as cells rise
const colorShift = influenceFactor * COLOR_SHIFT_AMOUNT * 0.5;
cell.color = [
Math.min(255, Math.max(0, cell.baseColor[0] + colorShift)),
Math.min(255, Math.max(0, cell.baseColor[1] + colorShift)),
Math.min(255, Math.max(0, cell.baseColor[2] + colorShift))
] as [number, number, number];
} else {
// Gradually return to base color and zero elevation when mouse is away
cell.color[0] += (cell.baseColor[0] - cell.color[0]) * 0.1;
cell.color[1] += (cell.baseColor[1] - cell.color[1]) * 0.1;
cell.color[2] += (cell.baseColor[2] - cell.color[2]) * 0.1;
cell.targetElevation = 0;
}
// Handle cell state transitions
if (cell.transitioning) {
// When a cell is completely faded out, update its alive state
if (!cell.next && cell.opacity < 0.01 && cell.scale < 0.01) {
cell.alive = false;
cell.transitioning = false;
cell.transitionComplete = true;
cell.opacity = 0;
cell.scale = 0;
cell.elevation = 0;
}
// When a new cell is born
else if (cell.next && !cell.alive && !cell.transitionComplete) {
cell.alive = true;
cell.transitioning = false;
cell.transitionComplete = true;
}
}
// Handle ripple animation
if (cell.rippleStartTime > 0) {
const elapsedTime = Date.now() - cell.rippleStartTime;
if (elapsedTime > 0) {
// Calculate ripple progress (0 to 1)
const rippleProgress = elapsedTime / 1000; // 1 second for full animation
if (rippleProgress < 1) {
// Create a smooth wave effect
const wavePhase = rippleProgress * Math.PI * 2;
const waveHeight = Math.sin(wavePhase) * Math.exp(-rippleProgress * 4);
// Apply wave height to cell elevation only if it's not being overridden by mouse
if (distanceToMouse >= MOUSE_INFLUENCE_RADIUS) {
cell.rippleEffect = waveHeight;
cell.targetElevation = RIPPLE_ELEVATION_FACTOR * waveHeight;
} else {
cell.rippleEffect = waveHeight * 0.3; // Reduced effect when mouse is influencing
}
} else {
// Reset ripple effects
cell.rippleEffect = 0;
cell.rippleStartTime = 0;
if (distanceToMouse >= MOUSE_INFLUENCE_RADIUS) {
cell.targetElevation = 0;
}
}
}
}
}
}
};
const handleMouseDown = (e: MouseEvent) => {
if (!gridRef.current || !canvasRef.current) return;
const canvas = canvasRef.current;
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const cellSize = getCellSize();
mouseRef.current.isDown = true;
mouseRef.current.lastClickTime = Date.now();
const grid = gridRef.current;
// Calculate which cell was clicked
const cellX = Math.floor((mouseX - grid.offsetX) / cellSize);
const cellY = Math.floor((mouseY - grid.offsetY) / cellSize);
if (cellX >= 0 && cellX < grid.cols && cellY >= 0 && cellY < grid.rows) {
mouseRef.current.cellX = cellX;
mouseRef.current.cellY = cellY;
const cell = grid.cells[cellX][cellY];
if (cell.alive) {
// Create ripple effect from existing cell
createRippleEffect(grid, cellX, cellY);
} else {
// Spawn new cell at empty position
spawnCellAtPosition(grid, cellX, cellY);
}
}
};
const handleMouseMove = (e: MouseEvent) => {
if (!canvasRef.current || !gridRef.current) return;
const canvas = canvasRef.current;
const rect = canvas.getBoundingClientRect();
const cellSize = getCellSize();
mouseRef.current.x = e.clientX - rect.left;
mouseRef.current.y = e.clientY - rect.top;
// Drawing functionality - place cells while dragging
if (mouseRef.current.isDown) {
const grid = gridRef.current;
// Calculate which cell the mouse is over
const cellX = Math.floor((mouseRef.current.x - grid.offsetX) / cellSize);
const cellY = Math.floor((mouseRef.current.y - grid.offsetY) / cellSize);
// Only draw if we're on a new cell
if (cellX !== mouseRef.current.cellX || cellY !== mouseRef.current.cellY) {
mouseRef.current.cellX = cellX;
mouseRef.current.cellY = cellY;
// Spawn cell at this position if it's empty
if (cellX >= 0 && cellX < grid.cols && cellY >= 0 && cellY < grid.rows) {
const cell = grid.cells[cellX][cellY];
if (!cell.alive && !cell.transitioning) {
spawnCellAtPosition(grid, cellX, cellY);
}
}
}
}
};
const handleMouseUp = () => {
mouseRef.current.isDown = false;
};
const handleMouseLeave = () => {
mouseRef.current.isDown = false;
mouseRef.current.x = -1000;
mouseRef.current.y = -1000;
};
const setupCanvas = (canvas: HTMLCanvasElement, width: number, height: number) => {
const ctx = canvas.getContext('2d');
if (!ctx) return;
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
ctx.scale(dpr, dpr);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
return ctx;
};
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
// Create an AbortController for cleanup
const controller = new AbortController();
const signal = controller.signal;
const handleResize = () => {
if (signal.aborted) return;
if (resizeTimeoutRef.current) {
clearTimeout(resizeTimeoutRef.current);
}
resizeTimeoutRef.current = setTimeout(() => {
if (signal.aborted) return;
const displayWidth = layout === 'index' ? window.innerWidth : SIDEBAR_WIDTH;
const displayHeight = window.innerHeight;
const ctx = setupCanvas(canvas, displayWidth, displayHeight);
if (!ctx) return;
lastUpdateTimeRef.current = 0;
lastCycleTimeRef.current = 0;
const cellSize = getCellSize();
// Only initialize new grid if one doesn't exist or dimensions changed
if (!gridRef.current ||
gridRef.current.cols !== Math.floor(displayWidth / cellSize) ||
gridRef.current.rows !== Math.floor(displayHeight / cellSize)) {
gridRef.current = initGrid(displayWidth, displayHeight);
}
}, 250);
};
const displayWidth = layout === 'index' ? window.innerWidth : SIDEBAR_WIDTH;
const displayHeight = window.innerHeight;
const ctx = setupCanvas(canvas, displayWidth, displayHeight);
if (!ctx) return;
// Only initialize grid if it doesn't exist
if (!gridRef.current) {
gridRef.current = initGrid(displayWidth, displayHeight);
}
// Add mouse event listeners
canvas.addEventListener('mousedown', handleMouseDown, { signal });
canvas.addEventListener('mousemove', handleMouseMove, { signal });
canvas.addEventListener('mouseup', handleMouseUp, { signal });
canvas.addEventListener('mouseleave', handleMouseLeave, { signal });
const handleVisibilityChange = () => {
if (document.hidden) {
// Tab is hidden
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = undefined;
}
} else {
// Tab is visible again
if (!animationFrameRef.current) {
// Reset timing references to prevent catching up
lastUpdateTimeRef.current = performance.now();
lastCycleTimeRef.current = performance.now();
animationFrameRef.current = requestAnimationFrame(animate);
}
}
};
const animate = (currentTime: number) => {
if (signal.aborted) return;
// Initialize timing if first frame
if (!lastUpdateTimeRef.current) {
lastUpdateTimeRef.current = currentTime;
lastCycleTimeRef.current = currentTime;
}
// Calculate time since last frame
const deltaTime = currentTime - lastUpdateTimeRef.current;
// Limit delta time to prevent large jumps when tab becomes active again
const clampedDeltaTime = Math.min(deltaTime, 100);
lastUpdateTimeRef.current = currentTime;
// Calculate time since last cycle update
const cycleElapsed = currentTime - lastCycleTimeRef.current;
if (gridRef.current) {
// Check if it's time for the next life cycle
if (cycleElapsed >= CYCLE_TIME) {
computeNextState(gridRef.current);
lastCycleTimeRef.current = currentTime;
}
updateCellAnimations(gridRef.current, clampedDeltaTime);
}
// Draw frame
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (gridRef.current) {
const grid = gridRef.current;
const cellSize = getCellSize();
const displayCellSize = cellSize * 0.8;
const roundness = displayCellSize * 0.2;
for (let i = 0; i < grid.cols; i++) {
for (let j = 0; j < grid.rows; j++) {
const cell = grid.cells[i][j];
// Draw all transitioning cells, even if they're fading out
if ((cell.alive || cell.targetOpacity > 0) && cell.opacity > 0.01) {
const [r, g, b] = cell.color;
// Base opacity
ctx.globalAlpha = cell.opacity * 0.9;
const scaledSize = displayCellSize * cell.scale;
const xOffset = (displayCellSize - scaledSize) / 2;
const yOffset = (displayCellSize - scaledSize) / 2;
// Apply 3D elevation effect
const elevationOffset = cell.elevation;
const x = grid.offsetX + i * cellSize + (cellSize - displayCellSize) / 2 + xOffset;
const y = grid.offsetY + j * cellSize + (cellSize - displayCellSize) / 2 + yOffset - elevationOffset;
const scaledRoundness = roundness * cell.scale;
// Draw shadow for 3D effect when cell is elevated
if (elevationOffset > 0.5) {
ctx.fillStyle = `rgba(0, 0, 0, ${0.2 * (elevationOffset / ELEVATION_FACTOR)})`;
ctx.beginPath();
ctx.moveTo(x + scaledRoundness, y + elevationOffset * 1.1);
ctx.lineTo(x + scaledSize - scaledRoundness, y + elevationOffset * 1.1);
ctx.quadraticCurveTo(x + scaledSize, y + elevationOffset * 1.1, x + scaledSize, y + elevationOffset * 1.1 + scaledRoundness);
ctx.lineTo(x + scaledSize, y + elevationOffset * 1.1 + scaledSize - scaledRoundness);
ctx.quadraticCurveTo(x + scaledSize, y + elevationOffset * 1.1 + scaledSize, x + scaledSize - scaledRoundness, y + elevationOffset * 1.1 + scaledSize);
ctx.lineTo(x + scaledRoundness, y + elevationOffset * 1.1 + scaledSize);
ctx.quadraticCurveTo(x, y + elevationOffset * 1.1 + scaledSize, x, y + elevationOffset * 1.1 + scaledSize - scaledRoundness);
ctx.lineTo(x, y + elevationOffset * 1.1 + scaledRoundness);
ctx.quadraticCurveTo(x, y + elevationOffset * 1.1, x + scaledRoundness, y + elevationOffset * 1.1);
ctx.fill();
}
// Draw main cell
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.beginPath();
ctx.moveTo(x + scaledRoundness, y);
ctx.lineTo(x + scaledSize - scaledRoundness, y);
ctx.quadraticCurveTo(x + scaledSize, y, x + scaledSize, y + scaledRoundness);
ctx.lineTo(x + scaledSize, y + scaledSize - scaledRoundness);
ctx.quadraticCurveTo(x + scaledSize, y + scaledSize, x + scaledSize - scaledRoundness, y + scaledSize);
ctx.lineTo(x + scaledRoundness, y + scaledSize);
ctx.quadraticCurveTo(x, y + scaledSize, x, y + scaledSize - scaledRoundness);
ctx.lineTo(x, y + scaledRoundness);
ctx.quadraticCurveTo(x, y, x + scaledRoundness, y);
ctx.fill();
// Draw highlight on elevated cells
if (elevationOffset > 0.5) {
ctx.fillStyle = `rgba(255, 255, 255, ${0.1 * (elevationOffset / ELEVATION_FACTOR)})`;
ctx.beginPath();
ctx.moveTo(x + scaledRoundness, y);
ctx.lineTo(x + scaledSize - scaledRoundness, y);
ctx.quadraticCurveTo(x + scaledSize, y, x + scaledSize, y + scaledRoundness);
ctx.lineTo(x + scaledSize, y + scaledSize/3);
ctx.lineTo(x, y + scaledSize/3);
ctx.lineTo(x, y + scaledRoundness);
ctx.quadraticCurveTo(x, y, x + scaledRoundness, y);
ctx.fill();
}
// No need for separate ripple drawing since the elevation handles the 3D ripple effect
}
}
}
ctx.globalAlpha = 1;
}
animationFrameRef.current = requestAnimationFrame(animate);
};
document.addEventListener('visibilitychange', handleVisibilityChange, { signal });
window.addEventListener('resize', handleResize, { signal });
animate(performance.now());
return () => {
controller.abort();
document.removeEventListener('visibilitychange', handleVisibilityChange);
window.removeEventListener('resize', handleResize);
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
if (resizeTimeoutRef.current) {
clearTimeout(resizeTimeoutRef.current);
}
};
}, [layout]); // Added layout as a dependency since it's used in the effect
const getContainerClasses = () => {
if (layout === 'index') {
return 'fixed inset-0 -z-10';
}
const baseClasses = 'fixed top-0 bottom-0 hidden lg:block -z-10';
return position === 'left'
? `${baseClasses} left-0`
: `${baseClasses} right-0`;
};
return (
<div className={getContainerClasses()}>
<canvas
ref={canvasRef}
className="w-full h-full bg-black"
style={{ cursor: 'default' }} // Changed from cursor-pointer to default
/>
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm pointer-events-none" />
</div>
);
};
export default Background;

View File

@@ -0,0 +1,34 @@
import * as React from "react";
import Giscus from "@giscus/react";
const id = "inject-comments";
export const Comments = () => {
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
return (
<div id={id}>
{mounted ? (
<Giscus
id={id}
repo="timmypidashev/web"
repoId="MDEwOlJlcG9zaXRvcnkzODYxMjk5Mjk="
category="Blog & Project Comments"
categoryId="DIC_kwDOFwPgCc4CpKtV"
theme="https://timmypidashev.us-sea-1.linodeobjects.com/comments.css"
mapping="pathname"
strict="0"
reactionsEnabled="1"
emitMetadata="0"
inputPosition="bottom"
lang="en"
loading="eager"
/>
) : null}
</div>
);
};

View File

@@ -0,0 +1,38 @@
import React from "react";
import { RssIcon, TagIcon, TrendingUpIcon } from "lucide-react";
export const BlogHeader = () => {
return (
<div className="w-full max-w-6xl mx-auto px-4 pt-24 sm:pt-24">
<h1 className="text-2xl sm:text-3xl font-bold text-purple mb-3 text-center px-4 leading-relaxed">
Latest Thoughts <br className="sm:hidden" />
& Writings
</h1>
<div className="flex flex-wrap justify-center gap-4 mb-12 text-sm sm:text-base">
<a
href="/rss"
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-background border border-foreground/20 text-orange hover:text-orange-bright hover:border-orange/50 transition-colors duration-200"
>
<RssIcon className="w-4 h-4" />
<span>RSS Feed</span>
</a>
<a
href="/blog/tags"
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-background border border-foreground/20 text-aqua hover:text-aqua-bright hover:border-aqua/50 transition-colors duration-200"
>
<TagIcon className="w-4 h-4" />
<span>Browse Tags</span>
</a>
<a
href="/blog/popular"
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-background border border-foreground/20 text-blue hover:text-blue-bright hover:border-blue/50 transition-colors duration-200"
>
<TrendingUpIcon className="w-4 h-4" />
<span>Most Popular</span>
</a>
</div>
</div>
);
};

View File

@@ -0,0 +1,99 @@
import React from "react";
type BlogPost = {
slug: string;
data: {
title: string;
author: string;
date: string;
tags: string[];
description: string;
image?: string;
imagePosition?: string;
};
};
interface BlogPostListProps {
posts: BlogPost[];
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric"
});
};
export const BlogPostList = ({ posts }: BlogPostListProps) => {
return (
<div className="w-full max-w-6xl mx-auto">
<ul className="space-y-6 md:space-y-10">
{posts.map((post) => (
<li key={post.slug} className="group px-4 md:px-0">
<a
href={`/blog/${post.slug}`}
className="block"
>
<article className="flex flex-col md:flex-row gap-4 md:gap-8 pb-6 md:pb-10 border-b border-foreground/20 last:border-b-0 p-2 md:p-4 rounded-lg group-hover:outline group-hover:outline-2 group-hover:outline-purple transition-all duration-200">
{/* Image container with fixed aspect ratio */}
<div className="w-full md:w-1/3 aspect-[16/9] overflow-hidden rounded-lg bg-background">
<img
src={post.data.image || "/blog/placeholder.png"}
alt={post.data.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
style={{ objectPosition: post.data.imagePosition || "center center" }}
/>
</div>
{/* Content container */}
<div className="w-full md:w-2/3 flex flex-col gap-2 md:gap-4 py-1 md:py-2">
{/* Title and meta info */}
<div className="space-y-1.5 md:space-y-3">
<h2 className="text-lg md:text-2xl font-semibold text-yellow group-hover:text-purple transition-colors duration-200 line-clamp-2">
{post.data.title}
</h2>
<div className="flex flex-wrap items-center gap-2 md:gap-3 text-sm md:text-base text-foreground/80">
<span className="text-orange">{post.data.author}</span>
<span className="text-foreground/50"></span>
<time dateTime={post.data.date} className="text-blue">
{formatDate(post.data.date)}
</time>
</div>
</div>
{/* Description */}
<p className="text-foreground/90 text-sm md:text-lg leading-relaxed line-clamp-2 mt-0.5 md:mt-0">
{post.data.description}
</p>
{/* Tags */}
<div className="flex flex-wrap gap-1.5 md:gap-3 mt-1 md:mt-2">
{post.data.tags.slice(0, 3).map((tag) => (
<span
key={tag}
className="text-xs md:text-base text-aqua hover:text-aqua-bright transition-colors duration-200"
onClick={(e) => {
e.preventDefault();
window.location.href = `/blog/tag/${tag}`;
}}
>
#{tag}
</span>
))}
{post.data.tags.length > 3 && (
<span className="text-xs md:text-base text-foreground/60">
+{post.data.tags.length - 3}
</span>
)}
</div>
</div>
</article>
</a>
</li>
))}
</ul>
</div>
);
};

View File

@@ -0,0 +1,114 @@
import React, { useMemo } from 'react';
interface BlogPost {
title: string;
data: {
tags: string[];
};
}
interface TagListProps {
posts: BlogPost[];
}
const TagList: React.FC<TagListProps> = ({ posts }) => {
const spectrumColors = [
'text-red-bright',
'text-orange-bright',
'text-yellow-bright',
'text-green-bright',
'text-aqua-bright',
'text-blue-bright',
'text-purple-bright'
];
const tagData = useMemo(() => {
if (!Array.isArray(posts)) return [];
const tagMap = new Map();
posts.forEach(post => {
if (post?.data?.tags && Array.isArray(post.data.tags)) {
post.data.tags.forEach(tag => {
if (!tagMap.has(tag)) {
tagMap.set(tag, {
name: tag,
count: 1
});
} else {
const data = tagMap.get(tag);
data.count++;
}
});
}
});
const tagArray = Array.from(tagMap.values());
const maxCount = Math.max(...tagArray.map(t => t.count));
return tagArray
.sort((a, b) => b.count - a.count)
.map((tag, index) => ({
...tag,
color: spectrumColors[index % spectrumColors.length],
frequency: tag.count / maxCount
}));
}, [posts]);
if (tagData.length === 0) {
return (
<div className="flex-1 w-full min-h-[16rem] flex items-center justify-center text-foreground opacity-60">
No tags available
</div>
);
}
return (
<div className="flex-1 w-full bg-background p-4">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{tagData.map(({ name, count, color, frequency }) => (
<a
key={name}
href={`/blog/tags/${encodeURIComponent(name)}`}
className={`
group relative
flex flex-col items-center justify-center
min-h-[5rem]
px-6 py-4 rounded-lg
text-xl
transition-all duration-300 ease-in-out
hover:scale-105
hover:bg-foreground/5
${color}
`}
>
{/* Main tag display */}
<div className="font-medium text-center">
#{name}
</div>
{/* Post count */}
<div className="mt-2 text-base opacity-60">
{count} post{count !== 1 ? 's' : ''}
</div>
{/* Background gradient */}
<div
className="absolute inset-0 -z-10 rounded-lg opacity-10"
style={{
background: `
linear-gradient(
45deg,
currentColor ${frequency * 100}%,
transparent
)
`
}}
/>
</a>
))}
</div>
</div>
);
};
export default TagList;

View File

@@ -0,0 +1,21 @@
import React from "react";
import { Links } from "@/components/footer/links";
export default function Footer({ fixed = false }) {
const footerLinks = Links.map((link) => (
<div
key={link.id}
className={`inline-block ${link.color}`}
>
<a href={link.href} target="_blank" rel="noopener noreferrer">{link.label}</a>
</div>
));
return (
<footer className={`w-full font-bold pointer-events-none ${fixed ? "fixed bottom-0 left-0 right-0" : ""}`}>
<div className="flex flex-row px-2 py-1 text-lg lg:px-6 lg:py-1.5 lg:text-3xl md:text-2xl justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20 pointer-events-none [&_a]:pointer-events-auto">
{footerLinks}
</div>
</footer>
);
}

View File

@@ -0,0 +1,14 @@
interface FooterLink {
id: number;
href: string;
label: string;
color: string;
}
export const Links: FooterLink[] = [
{ id: 0, href: "mailto:contact@timmypidashev.dev", label: "Contact", color: "text-green" },
{ id: 1, href: "https://github.com/timmypidashev", label: "Github", color: "text-yellow" },
{ id: 3, href: "https://www.linkedin.com/in/timothy-pidashev-4353812b8", label: "Linkedin", color: "text-blue" },
{ id: 4, href: "https://github.com/timmypidashev/web", label: "Source", color: "text-purple" },
{ id: 5, href: "https://github.com/timmypidashev/web/releases", label: "v2", color: "text-aqua" }
];

View File

@@ -0,0 +1,110 @@
import React, { useState, useEffect } from "react";
import { Links } from "@/components/header/links";
export default function Header() {
const [isClient, setIsClient] = useState(false);
const [visible, setVisible] = useState(true);
const [lastScrollY, setLastScrollY] = useState(0);
const [currentPath, setCurrentPath] = useState("");
const [shouldAnimate, setShouldAnimate] = useState(false);
useEffect(() => {
setIsClient(true);
setCurrentPath(document.location.pathname);
setTimeout(() => setShouldAnimate(true), 50);
}, []);
useEffect(() => {
if (!isClient) return;
const handleScroll = () => {
const currentScrollY = document.documentElement.scrollTop;
setVisible(currentScrollY < lastScrollY || currentScrollY < 10);
setLastScrollY(currentScrollY);
};
document.addEventListener("scroll", handleScroll);
return () => document.removeEventListener("scroll", handleScroll);
}, [lastScrollY, isClient]);
const checkIsActive = (linkHref: string): boolean => {
if (!isClient) return false;
const path = document.location.pathname;
if (linkHref === "/") return path === "/";
return linkHref !== "/" && path.startsWith(linkHref);
};
const isIndexPage = checkIsActive("/");
const headerLinks = Links.map((link) => {
const isActive = checkIsActive(link.href);
return (
<div
key={link.id}
className={`
relative inline-block
${link.color}
${!isIndexPage ? 'bg-black' : ''}
`}
>
<a
href={link.href}
className="relative inline-block"
>
{link.label}
<div className="absolute -bottom-1 left-0 w-full overflow-hidden">
{isClient && isActive && (
<svg
className="w-full h-3 opacity-100"
preserveAspectRatio="none"
viewBox="0 0 100 12"
xmlns="http://www.w3.org/2000/svg"
>
<path
className="stroke-current transition-[stroke-dashoffset] duration-[600ms] ease-out"
d="M0,6
C25,4 35,8 50,6
S75,4 100,6"
fill="none"
strokeWidth="2.5"
strokeLinecap="round"
style={{
strokeDasharray: 100,
strokeDashoffset: shouldAnimate ? 0 : 100
}}
/>
</svg>
)}
</div>
</a>
</div>
);
});
return (
<header
className={`
fixed z-50 top-0 left-0 right-0
font-bold
transition-transform duration-300
pointer-events-none
${visible ? "translate-y-0" : "-translate-y-full"}
`}
>
<div className={`
w-full flex flex-row items-center justify-center
pointer-events-none
${!isIndexPage ? 'bg-black md:bg-transparent' : ''}
`}>
<div className={`
w-full md:w-auto flex flex-row pt-1 px-2 text-lg lg:text-3xl md:text-2xl
items-center justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20 md:py-2
pointer-events-none [&_a]:pointer-events-auto
${!isIndexPage ? 'bg-black md:px-20' : ''}
`}>
{headerLinks}
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,14 @@
interface HeaderLink {
id: number;
href: string;
label: string;
color: string;
}
export const Links: HeaderLink[] = [
{ id: 0, href: "/", label: "Home", color: "text-green" },
{ id: 1, href: "/about", label: "About", color: "text-yellow" },
{ id: 2, href: "/projects", label: "Projects", color: "text-blue" },
{ id: 3, href: "/blog", label: "Blog", color: "text-purple" },
{ id: 4, href: "/resume", label: "Resume", color: "text-aqua" }
];

View File

@@ -0,0 +1,84 @@
import React from "react";
import Typewriter from "typewriter-effect";
const html = (strings: TemplateStringsArray, ...values: any[]) => {
let result = strings[0];
for (let i = 0; i < values.length; i++) {
result += values[i] + strings[i + 1];
}
return result.replace(/\n\s*/g, "").replace(/\s+/g, " ").trim();
};
interface TypewriterOptions {
autoStart: boolean;
loop: boolean;
delay: number;
deleteSpeed: number;
cursor: string;
}
interface TypewriterInstance {
typeString: (str: string) => TypewriterInstance;
pauseFor: (ms: number) => TypewriterInstance;
deleteAll: () => TypewriterInstance;
start: () => TypewriterInstance;
}
export default function Hero() {
const SECTION_1 = html`
<span>Hello, I'm</span>
<br><div class="mb-4"></div>
<span><a href="/about" class="text-aqua hover:underline"><strong>Timothy Pidashev</strong></a></span>
`;
const SECTION_2 = html`
<span>I've been turning</span>
<br><div class="mb-4"></div>
<span><a href="/projects" class="text-green hover:underline"><strong>coffee</strong></a> into
<a href="/projects" class="text-yellow hover:underline"><strong>code</strong></a></span>
<br><div class="mb-4"></div>
<span>since <a href="/about" class="text-blue hover:underline"><strong>2018</strong></a>!</span>
`;
const SECTION_3 = html`
<span>Check out my</span>
<br><div class="mb-4"></div>
<span><a href="/blog" class="text-purple hover:underline"><strong>blog</strong></a>/
<a href="/projects" class="text-blue hover:underline"><strong>projects</strong></a> or</span>
<br><div class="mb-4"></div>
<span><a href="/contact" class="text-green hover:underline"><strong>contact</strong></a> me below!</span>
`;
const handleInit = (typewriter: TypewriterInstance): void => {
typewriter
.typeString(SECTION_1)
.pauseFor(2000)
.deleteAll()
.typeString(SECTION_2)
.pauseFor(2000)
.deleteAll()
.typeString(SECTION_3)
.pauseFor(2000)
.deleteAll()
.start();
};
const typewriterOptions: TypewriterOptions = {
autoStart: true,
loop: true,
delay: 50,
deleteSpeed: 800,
cursor: '|'
};
return (
<div className="flex justify-center items-center min-h-screen pointer-events-none">
<div className="text-4xl font-bold text-center pointer-events-none [&_a]:pointer-events-auto">
<Typewriter
options={typewriterOptions}
onInit={handleInit}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,7 @@
import React from "react";
import { ChevronDown } from "lucide-react";
export const ChevronDownIcon = (props: React.ComponentProps<typeof ChevronDown>) => {
return <ChevronDown {...props} />;
};

View File

@@ -0,0 +1 @@
export { ChevronDownIcon } from "@/components/icons/chevron-down";

View File

@@ -0,0 +1,264 @@
import React, { useState } from 'react';
import { Terminal, Copy, Check } from 'lucide-react';
// Import all required icons from react-icons
import { FaDebian, FaFedora } from 'react-icons/fa6';
import { SiGentoo, SiNixos, SiArchlinux } from 'react-icons/si';
// Component for multi-line command sequences
const CommandSequence = ({
commands,
description,
shell = "bash"
}) => {
const [copied, setCopied] = useState(false);
// Join the commands with newlines for copying
const fullCommandText = Array.isArray(commands)
? commands.join('\n')
: commands;
const copyToClipboard = () => {
navigator.clipboard.writeText(fullCommandText)
.then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
})
.catch(err => {
console.error('Failed to copy: ', err);
});
};
return (
<div className="w-full rounded-md overflow-hidden border border-foreground/20 bg-background my-4" style={{ maxWidth: '95vw' }}>
{/* Header with Terminal Icon and Copy Button */}
<div className="bg-background border-b border-foreground/20 text-foreground p-2 flex items-center justify-between">
<div className="flex items-center">
<Terminal size={20} className="mr-2 text-yellow-bright" />
<div className="text-sm font-comic-code">
{description || "Terminal Commands"}
</div>
</div>
<button
onClick={copyToClipboard}
className="bg-background hover:bg-foreground/10 text-foreground text-xs px-2 py-1 rounded flex items-center"
>
{copied ? (
<>
<Check size={14} className="mr-1 text-green-bright" />
</>
) : (
<>
<Copy size={14} className="mr-1 text-foreground/70" />
</>
)}
</button>
</div>
{/* Command Display */}
<div className="text-foreground p-3 overflow-x-auto">
<div className="font-comic-code text-sm">
{Array.isArray(commands)
? commands.map((cmd, index) => (
<div key={index} className="flex items-start mb-2 last:mb-0">
<span className="text-orange-bright mr-2 flex-shrink-0">$</span>
<span className="text-purple-bright overflow-x-auto whitespace-nowrap">
{cmd}
</span>
</div>
))
: (
<div className="flex items-start">
<span className="text-orange-bright mr-2 flex-shrink-0">$</span>
<span className="text-purple-bright overflow-x-auto whitespace-nowrap">
{commands}
</span>
</div>
)
}
</div>
</div>
</div>
);
};
// Original Commands component with tabs for different distros
const Commands = ({
commandId,
description,
archCommand,
debianCommand,
fedoraCommand,
gentooCommand,
nixCommand
}) => {
const [activeTab, setActiveTab] = useState('arch');
const [copied, setCopied] = useState(false);
const distros = [
{
id: 'arch',
name: 'Arch',
icon: SiArchlinux,
command: archCommand || 'echo "No command specified for Arch"'
},
{
id: 'debian',
name: 'Debian/Ubuntu',
icon: FaDebian,
command: debianCommand || 'echo "No command specified for Debian/Ubuntu"'
},
{
id: 'fedora',
name: 'Fedora',
icon: FaFedora,
command: fedoraCommand || 'echo "No command specified for Fedora"'
},
{
id: 'gentoo',
name: 'Gentoo',
icon: SiGentoo,
command: gentooCommand || 'echo "No command specified for Gentoo"'
},
{
id: 'nix',
name: 'NixOS',
icon: SiNixos,
command: nixCommand || 'echo "No command specified for NixOS"'
}
];
const copyToClipboard = (text) => {
navigator.clipboard.writeText(text)
.then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
})
.catch(err => {
console.error('Failed to copy: ', err);
});
};
return (
<div className="w-full rounded-md overflow-hidden border border-foreground/20 bg-background my-4" style={{ maxWidth: '95vw' }}>
{/* Header with Terminal Icon and Copy Button */}
<div className="bg-background border-b border-foreground/20 text-foreground p-2 flex items-center justify-between">
<div className="flex items-center">
<Terminal size={20} className="mr-2 text-yellow-bright" />
<div className="text-sm font-comic-code">
{description || "Terminal Command"}
</div>
</div>
<button
onClick={() => copyToClipboard(distros.find(d => d.id === activeTab).command)}
className="bg-background hover:bg-foreground/10 text-foreground text-xs px-2 py-1 rounded flex items-center"
>
{copied ? (
<>
<Check size={14} className="mr-1 text-green-bright" />
<span>Copied</span>
</>
) : (
<>
<Copy size={14} className="mr-1 text-foreground/70" />
<span>Copy</span>
</>
)}
</button>
</div>
{/* Tabs */}
<div className="flex flex-wrap border-b border-foreground/20 bg-background">
{distros.map((distro) => {
const IconComponent = distro.icon;
return (
<button
key={distro.id}
className={`px-3 py-2 text-sm font-medium flex items-center ${
activeTab === distro.id
? 'bg-background border-b-2 border-blue-bright text-blue-bright'
: 'text-foreground/80 hover:text-foreground hover:bg-foreground/5'
}`}
onClick={() => setActiveTab(distro.id)}
>
<span className="mr-1 inline-flex items-center">
<IconComponent size={16} />
</span>
{distro.name}
</button>
);
})}
</div>
{/* Command Display with Horizontal Scrolling */}
<div className="text-foreground p-3 overflow-x-auto">
<div className="flex items-center font-comic-code text-sm whitespace-nowrap">
<span className="text-orange-bright mr-2">$</span>
<span className="text-purple-bright">
{distros.find(d => d.id === activeTab).command}
</span>
</div>
</div>
</div>
);
};
// Single command component
const Command = ({
command,
description,
shell = "bash"
}) => {
const [copied, setCopied] = useState(false);
const copyToClipboard = () => {
navigator.clipboard.writeText(command)
.then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
})
.catch(err => {
console.error('Failed to copy: ', err);
});
};
return (
<div className="w-full rounded-md overflow-hidden border border-foreground/20 bg-background my-4" style={{ maxWidth: '95vw' }}>
{/* Header with Terminal Icon and Copy Button */}
<div className="bg-background border-b border-foreground/20 text-foreground p-2 flex items-center justify-between">
<div className="flex items-center">
<Terminal size={20} className="mr-2 text-yellow-bright" />
<div className="text-sm font-comic-code">
{description || "Terminal Command"}
</div>
</div>
<button
onClick={copyToClipboard}
className="bg-background hover:bg-foreground/10 text-foreground text-xs px-2 py-1 rounded flex items-center"
>
{copied ? (
<>
<Check size={14} className="mr-1 text-green-bright" />
</>
) : (
<>
<Copy size={14} className="mr-1 text-foreground/70" />
</>
)}
</button>
</div>
{/* Command Display with Horizontal Scrolling */}
<div className="text-foreground p-3 overflow-x-auto">
<div className="flex items-center font-comic-code text-sm whitespace-nowrap">
<span className="text-orange-bright mr-2">$</span>
<span className="text-purple-bright">
{command}
</span>
</div>
</div>
</div>
);
};
export { Commands, Command, CommandSequence };

View File

@@ -0,0 +1,68 @@
// src/components/mdx/Video.tsx
import React, { useRef } from "react";
import { Play } from "lucide-react";
type VideoProps = {
url: string;
title: string;
attribution?: string;
className?: string;
};
export function Video({ url, title, attribution, className }: VideoProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const overlayRef = useRef<HTMLButtonElement>(null);
const handlePlay = () => {
if (!videoRef.current || !overlayRef.current) return;
// Show browser native controls on play
videoRef.current.controls = true;
videoRef.current.play();
// Hide the overlay
overlayRef.current.style.display = "none";
};
return (
<figure className={`w-full ${className ?? ""}`}>
<div className="relative w-full bg-background rounded-lg overflow-hidden">
<video
ref={videoRef}
className="w-full h-auto bg-black cursor-pointer rounded-lg block"
preload="metadata"
playsInline
title={title}
>
<source src={url} type="video/mp4" />
Your browser does not support the video tag.
</video>
{/* Big overlay play button */}
<button
ref={overlayRef}
onClick={handlePlay}
className="absolute inset-0 flex items-center justify-center bg-background/90 text-foreground hover:text-yellow-bright transition"
aria-label={`Play ${title}`}
>
<Play size={64} strokeWidth={1.5} />
</button>
</div>
{/* Title + attribution */}
<figcaption className="mt-2 text-xs text-foreground flex justify-between items-center">
<span>{title}</span>
{attribution && (
<a
href={attribution}
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-blue-bright"
>
{attribution}
</a>
)}
</figcaption>
</figure>
);
}

View File

@@ -0,0 +1,78 @@
import React from "react"
import type { CollectionEntry } from "astro:content";
interface ProjectCardProps {
project: CollectionEntry<"projects">;
}
export function ProjectCard({ project }: ProjectCardProps) {
const hasLinks = project.data.githubUrl || project.data.demoUrl;
return (
<article className="group relative h-full">
<a
href={`/projects/${project.slug}`}
className="block rounded-lg border-2 border-foreground/20
hover:border-blue transition-all duration-300
bg-background overflow-hidden h-full flex flex-col"
>
<div className="aspect-video w-full border-b border-foreground/20 bg-foreground/5 overflow-hidden flex-shrink-0">
{project.data.image ? (
<img
src={project.data.image}
alt={`${project.data.title} preview`}
className="w-full h-full object-cover object-center group-hover:scale-105 transition-transform duration-300"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-foreground/30">
<span className="text-sm">No preview available</span>
</div>
)}
</div>
<div className="p-4 sm:p-6 space-y-3 flex flex-col flex-grow">
<h3 className="text-lg sm:text-xl font-bold group-hover:text-blue transition-colors">
{project.data.title}
</h3>
<div className="flex flex-wrap gap-2">
{project.data.techStack.map(tech => (
<span key={tech} className="text-xs px-2 py-1 rounded-full bg-purple-bright/10 text-purple-bright">
{tech}
</span>
))}
</div>
<p className="text-foreground/70 text-sm sm:text-base flex-grow">
{project.data.description}
</p>
{hasLinks && (
<div className="flex gap-4 pt-3 border-t border-foreground/10 mt-auto">
{project.data.githubUrl && (
<a
href={project.data.githubUrl}
className="text-sm text-blue hover:text-blue-bright
transition-colors z-10"
onClick={(e) => e.stopPropagation()}
>
View Source
</a>
)}
{project.data.demoUrl && (
<a
href={project.data.demoUrl}
className="text-sm text-green hover:text-green-bright
transition-colors z-10"
onClick={(e) => e.stopPropagation()}
>
Live Link
</a>
)}
</div>
)}
</div>
</a>
</article>
);
}

View File

@@ -0,0 +1,49 @@
import React from "react";
import type { CollectionEntry } from "astro:content";
import { ProjectCard } from "@/components/projects/project-card";
interface ProjectListProps {
projects: CollectionEntry<"projects">[];
}
export function ProjectList({ projects }: ProjectListProps) {
const latestProjects = projects.slice(0, 3);
const otherProjects = projects.slice(3);
return (
<div className="w-full max-w-6xl mx-auto pt-24 sm:pt-32">
<h1 className="text-2xl sm:text-3xl font-bold text-blue mb-12 text-center px-4 leading-relaxed">
Here's what I've been <br className="sm:hidden" />
building lately
</h1>
<div className="px-4 mb-16">
<h2 className="text-xl font-bold text-foreground/90 mb-6">
Featured Projects
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 auto-rows-fr justify-items-center">
{latestProjects.map(project => (
<div key={project.slug} className="w-full max-w-md">
<ProjectCard project={project} />
</div>
))}
</div>
</div>
{otherProjects.length > 0 && (
<div className="px-4 pb-8">
<h2 className="text-xl font-bold text-foreground/90 mb-6">
All Projects
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 auto-rows-fr justify-items-center">
{otherProjects.map(project => (
<div key={project.slug} className="w-full max-w-md">
<ProjectCard project={project} />
</div>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,270 @@
import React from "react";
import {
FileDown,
Github,
Linkedin,
Globe
} from "lucide-react";
const resumeData = {
name: "Timothy Pidashev",
title: "Software Engineer",
contact: {
email: "contact@timmypidashev.dev",
phone: "+1 (360) 409-0357",
location: "Camas, WA",
linkedin: "linkedin.com/in/timothy-pidashev-4353812b8/",
github: "github.com/timmypidashev"
},
summary: "Experienced software engineer with a passion for building scalable web applications and solving complex problems. Specialized in React, TypeScript, and modern web technologies.",
experience: [
{
title: "Office Manager & Tutor",
company: "FHCC",
location: "Ridgefield, WA",
period: "2020 - Present",
achievements: [
"Tutored Python, JavaScript, and HTML to students in grades 4-10, successfully fostering early programming skills",
"Designed and deployed a full-stack CRUD application to manage organizational operations",
"Engineered and implemented building-wide networking infrastructure and managed multiple service deployments",
"Maintained student records and administrative paperwork."
]
}
],
contractWork: [
{
title: "Revive Auto Parts",
type: "Full-Stack Development & Maintenance",
startDate: "2024",
url: "https://reviveauto.parts",
responsibilities: [
"Maintain and optimize website performance and security",
"Implement new features and functionality as needed",
"Provide 24/7 monitoring and emergency support"
],
achievements: [
"Designed and built the entire application from the ground up, including auth",
"Engineered a tagging system to optimize search results by keywords and relativity",
"Implemented a filter provider to further narrow down search results and enchance the user experience",
"Created a smooth and responsive infinitely scrollable listings page",
"Automated deployment & testing processes reducing downtime by 60%"
]
}
],
education: [
{
degree: "B.S. Computer Science",
school: "Clark College",
location: "Vancouver, WA",
period: "Graduating 2026",
achievements: []
}
],
skills: {
technical: ["JavaScript", "TypeScript", "React", "Node.js", "Python", "SQL", "AWS", "Docker", "C", "Go", "Rust"],
soft: ["Leadership", "Problem Solving", "Communication", "Agile Methodologies"]
},
certifications: [
{
name: "AWS Certified Solutions Architect",
issuer: "Amazon Web Services",
date: "2022"
}
]
};
const Resume = () => {
const handleDownloadPDF = () => {
window.open("/timothy-pidashev-resume.pdf", "_blank");
};
return (
<div className="max-w-4xl mx-auto px-6 md:px-8 pt-24 pb-16">
<div className="space-y-16">
{/* Header */}
<header className="text-center space-y-6">
<h1 className="text-6xl font-bold text-aqua-bright">{resumeData.name}</h1>
<h2 className="text-3xl text-foreground/80">{resumeData.title}</h2>
<div className="flex flex-col md:flex-row justify-center gap-4 md:gap-6 text-foreground/60 text-lg">
<a href={`mailto:${resumeData.contact.email}`} className="hover:text-foreground transition-colors duration-200">
{resumeData.contact.email}
</a>
<span className="hidden md:inline"></span>
<a href={`tel:${resumeData.contact.phone}`} className="hover:text-foreground transition-colors duration-200">
{resumeData.contact.phone}
</a>
<span className="hidden md:inline"></span>
<span>{resumeData.contact.location}</span>
</div>
<div className="flex justify-center items-center gap-6 text-lg">
<a href={`https://${resumeData.contact.github}`}
target="_blank"
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
>
<Github size={18} />
GitHub
</a>
<a href={`https://${resumeData.contact.linkedin}`}
target="_blank"
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
>
<Linkedin size={18} />
LinkedIn
</a>
<button
onClick={handleDownloadPDF}
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
>
<FileDown size={18} />
Resume
</button>
</div>
</header>
{/* Summary */}
<section className="space-y-4">
<h3 className="text-3xl font-bold text-yellow-bright">Professional Summary</h3>
<p className="text-xl leading-relaxed">{resumeData.summary}</p>
</section>
{/* Experience */}
<section className="space-y-8">
<h3 className="text-3xl font-bold text-yellow-bright">Experience</h3>
{resumeData.experience.map((exp, index) => (
<div key={index} className="space-y-4">
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
<div>
<h4 className="text-2xl font-semibold text-green-bright">{exp.title}</h4>
<div className="text-foreground/60 text-lg">{exp.company} - {exp.location}</div>
</div>
<div className="text-foreground/60 text-lg font-medium">{exp.period}</div>
</div>
<ul className="list-disc pl-6 space-y-3">
{exp.achievements.map((achievement, i) => (
<li key={i} className="text-lg leading-relaxed">{achievement}</li>
))}
</ul>
</div>
))}
</section>
{/* Contract Work */}
<section className="space-y-8">
<h3 className="text-3xl font-bold text-yellow-bright">Contract Work</h3>
{resumeData.contractWork.map((project, index) => (
<div key={index} className="space-y-4">
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
<div>
<div className="flex items-center gap-3">
<h4 className="text-2xl font-semibold text-green-bright">{project.title}</h4>
{project.url && (
<a
href={project.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-bright hover:text-blue transition-colors duration-200 opacity-60 hover:opacity-100"
aria-label={`Visit ${project.title}`}
>
<Globe size={16} strokeWidth={1.5} />
</a>
)}
</div>
<div className="text-foreground/60 text-lg">{project.type}</div>
</div>
<div className="text-foreground/60 text-lg font-medium">Since {project.startDate}</div>
</div>
<div className="space-y-4">
{project.responsibilities && (
<div>
<h5 className="text-lg text-aqua font-semibold mb-2">Responsibilities</h5>
<ul className="list-disc pl-6 space-y-3">
{project.responsibilities.map((responsibility, i) => (
<li key={i} className="text-lg leading-relaxed">{responsibility}</li>
))}
</ul>
</div>
)}
{project.achievements && (
<div>
<h5 className="text-lg text-aqua font-semibold mb-2">Key Achievements</h5>
<ul className="list-disc pl-6 space-y-3">
{project.achievements.map((achievement, i) => (
<li key={i} className="text-lg leading-relaxed">{achievement}</li>
))}
</ul>
</div>
)}
</div>
</div>
))}
</section>
{/* Education */}
<section className="space-y-8">
<h3 className="text-3xl font-bold text-yellow-bright">Education</h3>
{resumeData.education.map((edu, index) => (
<div key={index} className="space-y-4">
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
<div>
<h4 className="text-2xl font-semibold text-green-bright">{edu.degree}</h4>
<div className="text-foreground/60 text-lg">{edu.school} - {edu.location}</div>
</div>
<div className="text-foreground/60 text-lg font-medium">{edu.period}</div>
</div>
<ul className="list-disc pl-6 space-y-3">
{edu.achievements.map((achievement, i) => (
<li key={i} className="text-lg leading-relaxed">{achievement}</li>
))}
</ul>
</div>
))}
</section>
{/* Skills */}
<section className="space-y-8">
<h3 className="text-3xl font-bold text-yellow-bright">Skills</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-4">
<h4 className="text-2xl font-semibold text-green-bright">Technical Skills</h4>
<div className="flex flex-wrap gap-3">
{resumeData.skills.technical.map((skill, index) => (
<span key={index}
className="border border-foreground/20 px-4 py-2 rounded-lg hover:border-foreground/40 transition-colors duration-200">
{skill}
</span>
))}
</div>
</div>
<div className="space-y-4">
<h4 className="text-2xl font-semibold text-green-bright">Soft Skills</h4>
<div className="flex flex-wrap gap-3">
{resumeData.skills.soft.map((skill, index) => (
<span key={index}
className="border border-foreground/20 px-4 py-2 rounded-lg hover:border-foreground/40 transition-colors duration-200">
{skill}
</span>
))}
</div>
</div>
</div>
</section>
{/* Certifications */}
{/* Temporarily Hidden
<section className="space-y-6 mb-16">
<h3 className="text-3xl font-bold text-yellow-bright">Certifications</h3>
{resumeData.certifications.map((cert, index) => (
<div key={index} className="space-y-2">
<h4 className="text-2xl font-semibold text-green-bright">{cert.name}</h4>
<div className="text-foreground/60 text-lg">{cert.issuer} - {cert.date}</div>
</div>
))}
</section>
*/}
</div>
</div>
);
};
export default Resume;

View File

@@ -0,0 +1,9 @@
---
title: Breaking the Chromebook Cage
description: From breaking Chromebooks as a student to breaking Chromebooks to stop students from breaking Chromebooks
author: Timothy Pidashev
tags: ["uefi", "coreboot", "firmware", "chromebooks"]
date: 2025-09-15
image: "/blog/breaking-the-chromebook-cage/thumbnail.png"
isDraft: true
---

View File

@@ -0,0 +1,59 @@
import { useState } from "react";
const Cookie = () => {
const [hasBite, setHasBite] = useState(false);
const handleClick = () => {
setHasBite((prev) => !prev);
};
return (
<button
onClick={handleClick}
aria-label="Click for a cookie bite"
className="inline-flex items-center justify-center p-0 bg-transparent hover:scale-110 transition-transform"
style={{
verticalAlign: "middle",
lineHeight: 0, // Ensure the button doesn't affect the text spacing
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 64 64"
className="w-6 h-6" // Adjust size to align better with text
fill="none"
>
{/* Base cookie */}
<circle cx="32" cy="32" r="30" fill="#D2691E" />
{/* Bite area: uses a mask */}
<mask id="biteMask">
{/* Full circle mask */}
<rect width="64" height="64" fill="white" />
{hasBite && <circle cx="48" cy="16" r="12" fill="black" />}
</mask>
{/* Cookie with bite mask applied */}
<circle cx="32" cy="32" r="30" fill="#D2691E" mask="url(#biteMask)" />
{/* Chocolate chips */}
<circle cx="20" cy="24" r="3" fill="#3E2723" />
<circle cx="40" cy="18" r="3" fill="#3E2723" />
<circle cx="28" cy="40" r="3" fill="#3E2723" />
<circle cx="44" cy="36" r="3" fill="#3E2723" />
<circle cx="24" cy="48" r="3" fill="#3E2723" />
</svg>
</button>
);
};
const App = () => {
return (
<p className="text-lg">
On your way out, don't forget to grab a cookie!{" "}
<Cookie />
</p>
);
};
export default App;

View File

@@ -0,0 +1,104 @@
import React from 'react';
interface T440pAdProps {
className?: string;
}
const Advertisement: React.FC<T440pAdProps> = ({ className = '' }) => {
return (
<div className={`bg-gradient-to-br from-blue-50 to-indigo-100 border-2 border-blue-200 rounded-lg p-6 my-8 shadow-lg ${className}`}>
<div className="flex items-start gap-4">
{/* Icon/Logo placeholder */}
<div className="flex-shrink-0 w-12 h-12 bg-blue-600 rounded-lg flex items-center justify-center">
<svg
className="w-8 h-8 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
</div>
<div className="flex-1">
<div className="flex items-center gap-3 mb-3">
<h3 className="text-xl font-bold text-gray-900">
Custom Corebooted ThinkPad T440p
</h3>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
Available Now
</span>
</div>
<p className="text-gray-700 mb-4">
Skip the technical complexity and get a professionally corebooted ThinkPad T440p
built to your specifications. Each laptop is carefully modified and tested to ensure
optimal performance and reliability.
</p>
<div className="grid md:grid-cols-2 gap-4 mb-4">
<div>
<h4 className="font-semibold text-gray-900 mb-2"> What's Included:</h4>
<ul className="text-sm text-gray-700 space-y-1">
<li> Coreboot firmware pre-installed</li>
<li> IPS 1080p display upgrade</li>
<li> RAM options: 4GB, 8GB, or 16GB</li>
<li> CPU choice available</li>
<li> Battery upgrade option</li>
<li> Thorough testing & quality assurance</li>
</ul>
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-2">🔧 Benefits:</h4>
<ul className="text-sm text-gray-700 space-y-1">
<li> Faster boot times</li>
<li> Open-source BIOS</li>
<li> Enhanced security</li>
<li> No proprietary firmware</li>
<li> Full hardware control</li>
<li> Professional installation</li>
</ul>
</div>
</div>
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 pt-4 border-t border-blue-200">
<div className="flex items-baseline gap-2">
<span className="text-3xl font-bold text-blue-600">$500</span>
<span className="text-sm text-gray-600">USD (base configuration)</span>
</div>
<div className="flex gap-3">
<a
href="https://ebay.com"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors"
>
<svg className="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M8.5 2A1.5 1.5 0 007 3.5v1A1.5 1.5 0 008.5 6h7A1.5 1.5 0 0017 4.5v-1A1.5 1.5 0 0015.5 2h-7zM10 3h4v1h-4V3z"/>
<path d="M6 7.5A1.5 1.5 0 017.5 6h9A1.5 1.5 0 0118 7.5v9a1.5 1.5 0 01-1.5 1.5h-9A1.5 1.5 0 016 16.5v-9z"/>
</svg>
Order on eBay
</a>
<button className="inline-flex items-center px-4 py-2 border border-blue-600 text-blue-600 font-medium rounded-lg hover:bg-blue-50 transition-colors">
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Ask Questions
</button>
</div>
</div>
</div>
</div>
</div>
);
};
export default Advertisement;

View File

@@ -0,0 +1,25 @@
---
title: My First Post!
description: A quick introduction
author: Timothy Pidashev
tags: [greeting]
date: 2025-01-09
image: "/blog/my-first-post/thumbnail.png"
imagePosition: "center 30%"
---
import Cookie from "@/content/blog/components/my-first-post/cookie";
Hello, my name is Timothy Pidashev! Im an aspiring software engineer currently expanding my technical horizons!
Wait… that was a bit too formal. This is, after all, a blog post. Let me try that again.
Hey 👋, my name is Timothy! Im a software nerd who loves building cool tech! Phew, that was rough. Jokes aside,
Im happy you're here, dear reader. I have a lot planned for my little blog, and I hope the next few posts will
be useful to many nerds out there!
As a little spoiler of whats to come, the next post will be a complete dive into modifying and corebooting a
Thinkpad T440p for the ultimate secure dev laptop! Ive been personally daily-driving mine for the last two
years and cant wait to show it off, so stay tuned!
<Cookie />

View File

@@ -0,0 +1,308 @@
---
title: Thinkpad T440p Coreboot Guide
description: The definitive guide on corebooting a Thinkpad T440p
author: Timothy Pidashev
tags: [t440p, coreboot, thinkpad]
date: 2025-01-15
image: "/blog/thinkpad-t440p-coreboot-guide/thumbnail.png"
isDraft: true
---
import { Commands, Command, CommandSequence } from "@/components/mdx/command";
import Advertisement from '@/content/blog/components/thinkpad-t440p-coreboot-guide/advertisement';
> **Interactive Script Available!**
> Want to skip the manual steps in this guide?
> I've created an interactive script that can automate the entire process step by step as you follow along.
> This script supports Arch, Debian, Fedora, Gentoo, and Nix!
<Command
description="Interactive script"
command="curl -fsSL https://timmypidashev.dev/scripts/run.sh | sh -s -- -t coreboot-t440p"
client:load
/>
Don't pipe anyone's scripts to **sh** blindly, including mine - <a href="https://github.com/timmypidashev/scripts" target="_blank" rel="noopener noreferrer">audit the source</a>.
## Getting Started
The Thinkpad T440p is a powerful and versatile laptop that can be further enhanced by installing coreboot,
an open-source BIOS replacement. This guide will walk you through the process of corebooting your T440p,
including flashing the BIOS chip and installing the necessary software.
## What You'll Need
Before getting started corebooting your T440p, make sure you have the following:
- **Thinkpad T440p**: This guide is specifically for the T440p model.
- **CH341A Programmer**: This is a USB device used to flash the BIOS chip.
- **Screwdriver**: A torx screwdriver is needed to open the laptop.
## Installing Dependencies
Install the following programs. These will be needed to compile coreboot and flash the BIOS.
<Commands
description="Install prerequisite packages"
archCommand="sudo pacman -S base-devel curl git gcc-ada ncurses zlib nasm sharutils unzip flashrom"
debianCommand="sudo apt install build-essential curl git gnat libncurses-dev zlib1g-dev nasm sharutils unzip flashrom"
fedoraCommand="sudo dnf install @development-tools curl git gcc-gnat ncurses-devel zlib-devel nasm sharutils unzip flashrom"
gentooCommand="sudo emerge --ask sys-devel/base-devel net-misc/curl dev-vcs/git sys-devel/gcc ncurses dev-libs/zlib dev-lang/nasm app-arch/sharutils app-arch/unzip sys-apps/flashrom"
nixCommand="nix-env -i stdenv curl git gcc gnat ncurses zlib nasm sharutils unzip flashrom"
client:load
/>
## Disassembling the Laptop
1. **Power off your laptop**: Make sure your T440p is completely powered off and unplugged from any power source.
2. **Remove the battery**: Flip the laptop over and remove the battery by sliding the latch to the unlock position and lifting it out.
3. **Unscrew the back panel**: Use a torx screwdriver to remove the screws securing the back panel.
## Locating the EEPROM Chips
In order to flash the laptop, you will need to have access to two EEPROM chips located next to the sodimm RAM.
![EEPROM Chips Location](/blog/thinkpad-t440p-coreboot-guide/eeprom_chips_location.png)
## Assembling the SPI Flasher
Place the SPI flasher ribbon cable into the correct slot and make sure its the 3.3v variant
![SPI Flasher Assembly](/blog/thinkpad-t440p-coreboot-guide/spi_flasher_assembly.png)
After the flasher is ready, connect it to your machine and ensure its ready to use:
<Command
description="Ensure the CH341A flasher is being detected"
command="flashrom --programmer ch341a_spi"
/>
Flashrom should report that programmer initialization was a success.
## Extracting Original BIOS
To begin, first create a clean directory where all work to coreboot
the T440p will be done.
<Command
description="Create a directory where all work will be done"
command="mkdir ~/t440p-coreboot"
client:load
/>
Next, extract the original rom from both EEPROM chips. This is
done by attaching the programmer to the correct chip and running
the subsequent commands. It may take longer than expected, and
ensuring the bios was properly extracted is important before proceeding
further.
<CommandSequence
commands={[
"sudo flashrom --programmer ch341a_spi -r 4mb_backup1.bin",
"sudo flashrom --programmer ch341a_spi -r 4mb_backup2.bin",
"diff 4mb_backup1.bin 4mb_backup2.bin"
]}
description="Backup and verify 4MB chip"
client:load
/>
<CommandSequence
commands={[
"sudo flashrom --programmer ch341a_spi -r 8mb_backup1.bin",
"sudo flashrom --programmer ch341a_spi -r 8mb_backup2.bin",
"diff 8mb_backup1.bin 8mb_backup2.bin"
]}
description="Backup and verify 8MB chip"
client:load
/>
If the diff checks pass, combine both files into one ROM.
<Command
description="Combine 4MB & 8MB into one ROM"
command="cat 8mb_backup_1.bin 4mb_backup1.bin > t440p-original.rom"
client:load
/>
## Building Required Tools
Now that the original bios has been successfuly extracted, it is time
to clone the coreboot repository and build every tool needed to build
a new bios image.
<CommandSequence
commands={[
"git clone https://review.coreboot.org/coreboot ~/t440p-coreboot/coreboot",
"cd ~/t440p-coreboot/coreboot",
"git checkout e1e762716cf925c621d58163133ed1c3e006a903",
"git submodule update --init --checkout"
]}
description="Clone coreboot and checkout to the correct commit"
client:load
/>
We will need to build `idftool`, which will be used to export all necessary blobs
from our original bios, and `cbfstool`, which will be used to extract __mrc.bin__(a blob
from a haswell chromebook peppy image).
<Command
description="Build util/ifdtool"
command="cd ~/t440p-coreboot/coreboot/util/ifdtool && make"
client:load
/>
<Command
description="Build util/cbfstool"
command="cd ~/t440p-coreboot/coreboot/ && make -C util/cbfstool"
client:load
/>
## Exporting Firmware Blobs
Once the necessary tools have been built, we can export the
3 flash regions from our original bios image.
<CommandSequence
commands={[
"cd ~/t440p-coreboot/coreboot/util/ifdtool",
"./ifdtool -x ~/t440p-coreboot/t440p-original.rom",
"mv flashregion_0_flashdescriptor.bin ~/t440p-coreboot/ifd.bin",
"mv flashregion_2_intel_me.bin ~/t440p-coreboot/me.bin",
"mv flashregion_3_gbe.bin ~/t440p-coreboot/gbe.bin"
]}
description="Export firmware blobs"
client:load
/>
## Obtaining mrc.bin
In order to obtain __mrc.bin__, we need the chromeos peppy image.
This can be pulled by running the `crosfirmware.sh` script found in util/chromeos.
<Command
description="Download peppy chromeos image"
command="cd ~/t440p-coreboot/coreboot/util/chromeos && ./crosfirmware.sh peppy"
client:load
/>
We can now obtain __mrc.bin__ using cbfstool to extract the blob from the image.
<CommandSequence
commands={[
"cd ~/t440p-coreboot/coreboot/util/chromeos",
"../cbfstool/cbfstool coreboot-*.bin extract -f mrc.bin -n mrc.bin -r RO_SECTION",
"mv mrc.bin ~/t440p-coreboot/mrc.bin"
]}
description="Extract mrc.bin using cbfstool"
client:load
/>
## Configuring Coreboot
Configuring coreboot is really where most of your time will be spent. To help out,
I've created several handy configs that should suit most use cases, and can be easily
tweaked to your liking. Here is a list of whats available:
1. GRUB2
This configuration features GRUB2 as the bootloader, and contains 3 secondary payloads,
which the user can opt in/out of:
* memtest built in
* nvramcui built in
* coreinfo built in
This configuration also includes the dGPU option rom as well for T440p's featuring the gt730m on board.
2. SeaBIOS
3. edk2
> NOTE: Show the user how to choose the appropriate config, as well as building a custom config below.
## Building and Flashing
After configuring coreboot, it is time to build and flash it onto your unsuspecting T440p :D
<CommandSequence
commands={[
"cd ~/t440p-coreboot/coreboot",
"make crossgcc-i386 CPUS=$(nproc)",
"make"
]}
description="Build coreboot"
client:load
/>
Once the coreboot build has completed, split the built ROM for the 8MB(bottom) chip & 4MB(top) chip.
<CommandSequence
commands={[
"cd ~/t440p-coreboot/coreboot/build",
"dd if=coreboot.rom of=bottom.rom bs=1M count=8",
"dd if=coreboot.rom of=top.rom bs=1M skin=8"
]}
description="Split the built ROM for both EEPROM chips"
client:load
/>
Now flash the new bios onto your thinkpad!
<Command
description="Flash the 4MB chip"
command="sudo flashrom --programmer ch341a_spi -w top.rom"
/>
<Command
description="Flash the 8MB chip"
command="sudo flashrom --programmer ch341a_spi -w bottom.rom"
/>
Thats it! If done properly, your thinkpad should now boot!
## Reverting to Original
If for some reason you feel the need to revert back, or your T440p can't boot,
here are the steps needed to flash the original image back.
### Can't Boot
<CommandSequence
commands={[
"cd ~/t440p-coreboot/",
"dd if=t440p-original.rom of=bottom.rom bs=1M count=8",
"dd if=t440p-original.rom of=top.rom bs=1M skip=8"
]}
description="Split original bios image for both EEPROM chips"
client:load
/>
<Command
description="Flash the 4MB chip"
command="sudo flashrom --programmer ch341a_spi -w top.rom"
/>
<Command
description="Flash the 8MB chip"
command="sudo flashrom --programmer ch341a_spi -w bottom.rom"
/>
### Can Boot
<CommandSequence
commands={[
"sudo sed -i '/GRUB_CMDLINE_LINUX_DEFAULT/ s/\"/ iomem=relaxed\"/2' /etc/default/grub",
"sudo grub-mkconfig -o /boot/grub/grub.cfg",
]}
description="Set kernel flag iomem=relaxed and update grub config"
client:load
/>
Reboot to apply `iomem=relaxed`
<Command
description="Flash the original bios"
command="sudo flashrom -p internal:laptop=force_I_want_a_brick -r ~/t440p-coreboot/t440p-original.rom"
/>
And that about wraps it up! If you liked the guide, leave a reaction or comment any changes or fixes
I should make below. Your feedback is greatly appreciated!

29
src/src/content/config.ts Normal file
View File

@@ -0,0 +1,29 @@
import { defineCollection, z } from "astro:content";
export const collections = {
blog: defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
author: z.string(),
tags: z.array(z.string()),
date: z.coerce.date().transform((date) => {
return new Date(date.setUTCHours(12, 0, 0, 0));
}),
image: z.string().optional(),
imagePosition: z.string().optional(),
isDraft: z.boolean().optional()
}),
}),
projects: defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
githubUrl: z.string().url().optional(),
demoUrl: z.string().url().optional(),
techStack: z.array(z.string()),
date: z.string(),
image: z.string().optional()
}),
})
};

View File

@@ -0,0 +1,42 @@
---
title: "Darkbox"
description: "My gruvbox theme, with a pure black background"
githubUrl: "https://github.com/timmypidashev/darkbox.nvim"
techStack: ["Neovim", "Lua"]
date: "2025-01-05"
image: "/projects/darkbox/thumbnail.jpeg"
---
## Overview
My gruvbox theme, with a pure black background
## Key Features
* **Pure Black Background**: Preventing eyestrain during prolonged screen time.
* **Gruvbox Inspired**: Only the very best warm colorscheme, revamped for contrast.
* **Neovim First**: First class support for neovim, with treesitter support built in.
## Development Highlights
First, this project could not have been possible without the help of previous theme projects
such as [gruvbox.nvim](https://github.com/ellisonleao/gruvbox.nvim). Major kudos to everyone
who helped write that plugin. Using existing code as reference from this project dramatically
sped up development and made matters easy.
On top of that, working with lua was delightful, and there were no roadblocks or glaring
issues which prevented me from completion. Neovim's tight integration with lua is a game
changer compared to something like [VimScript](https://vimdoc.sourceforge.net/htmldoc/usr_41.html).
Overall, I found the whole experience simply perfect.
## Challenges & Roadblocks
One thing I am not proud of, is how contrast has turned out. At the moment, the gruvbox dark
colorscheme that inspired darkbox, isn't taking into account the contrast change from the
background `#282828` to `#000000`. This, can show up as a bit washed out to the careful eye in certain
code blocks where multiple colors clash together. I intend to work on this in a future release.
The fix will be to darken the color contrast to more closely match to the pure black background.
## Summary
This is it! This is the theme I am going to use for most likely the rest of my programming life.
I have always had a gruvbox themed desktop, but no background image. This resulted in an interesting
combo, and where the spark for this idea came from. I'm a bit biased, but I love it and hope
someone else does too. The github link can be found [here](https://github.com/timmypidashev/darkbox.nvim) :D

View File

@@ -0,0 +1,49 @@
---
title: "Fhccenter"
description: "Website made for a private school"
demoUrl: "https://fhccenter.org"
techStack: ["Nextjs", "Typescript", "Prisma"]
date: "2024-10-03"
image: "/projects/fhccenter/thumbnail.jpeg"
---
## Overview
A private learning center website that provides information about the organization,
student registrations, a newsletter, and full admin login with administrative tools.
## Key Features
* **Server Components**: React Server Components power efficient data flow and server-side
rendering.
* **JWT Authentication**: Built using a custom authentication provider, the entire site is
secured with a jwt token schema, including refresh tokens.
* **Elegant Components**: Constructed using Shadcn and Framer motion, component animations
and feel are seamless.
## Development Highlights
Building this site was quite an interesting feat. This was my first foray into react server
components, and while they aren't my preferred method of building applications, It was a great
experience to fully dive in and make use of them. This was also my first time using Nextjs's
new app router, and coming from the page router I really liked the experience. Building out
new pages and components was just a pure flow of thought, and I found myself trying to keep up
with the ideas coming out of my head rather than coming upon development barriers or softblocks.
## Challenges & Roadblocks
One of the only real challenges I didn't anticipate was caching. In Nextjs 14, caching is handled
very poorly, and I found myself struggling to figure out workarounds when handling dynamic data
such as the newsletter on the site. Full cache routes were extremely tricky to understand, but
after reading some troubleshooting guides it was full sailing ahead.
Another tricky but fun challenge was implementing authentication using react server components.
Since I had never used them up to this point, going about setting up a jwt schema while also trying
to ensure api routes don't accidentally leak was tricky, and something I still to this day don't really
like when it comes to server components. The fact that a single line of code is responsible for separating
server from client with nothing preventing a dev from putting all those components wherever they please can
cause quite a mess, so following good file organization conventions was instrumental.
## Summary
Overall, I had a pleasant time working on [fhccenter](https://fhccenter.org). After learning all the new
features like server components and the app router, I spent more time messing around with css styling and
getting the site to look exactly the way I wanted it rather than debugging silly mistakes, which was
refreshing coming from other frameworks.

View File

@@ -0,0 +1,58 @@
---
title: "Iridescent"
description: "An open-source graphics engine"
githubUrl: "https://github.com/timmypidashev/iridescent"
techStack: ["Cmake", "Glad", "Imgui"]
date: "2024-05-03"
image: "/projects/iridescent/thumbnail.jpeg"
---
## Overview
A open-source graphics engine concept created for my highschool senior project.
Built to expand my understanding of the low level programming world, and further
my reach of graphics and c++ programming.
## Key Features
* **Dockable Windows**: A quality of life feature utilizing imgui which allows
to efficiently use monitor space, moving logging and certain tools into their own
windows outside of the engines viewspace.
* **Layer Stack**: Deterministic and fast stack which determines the order in which
all layers of the application are drawn, from the ui all the way to the polygons making
up the scene.
* **Input Polling**: A realtime implementation polling all input events on the keyboard
and mouse for an extremely low latency between input events and scene events.
* **Detailed Logging**: A detailed logging system which logs events from the scene all the
way down to the system.
* **Shading**: The crème de la crème of the renderer, adding shading to the entire scene.
## Development Highlights
Some of the best highlights for me during this project was quite literally every time I managed
to fix a compilation issue. The exhileration after each successful step forward was just mesmerizing,
and I knew the moment I began my obsession wouldn't end until the engine was in a complete state.
Since I was going into this blind during my senior year at highschool, I knew almost nothing about
c++ or the build systems that accompany it, so learning all of that at once was both a breath of fresh
air and a frustration. However, after several days of work, I became very comfortable with cmake, my build
system of choice, and most basic concepts of c++, allowing me to begin working on the engine itself.
It was at this point where I started hitting some real knowledge barriers and ended up going to multiple
resources to learn and understand the inner workings of a graphics renderer. Resources such as the
[Hazel Engine Series](https://www.youtube.com/watch?v=JxIZbV_XjAs&list=PLlrATfBNZ98dC-V-N3m0Go4deliWHPFwT),
[Learn C++ Website](https://www.learncpp.com/learn-cpp-site-index/), and [OpenGL Tutorials](https://www.opengl.org/sdk/docs/tutorials/)
helped me tremendously here.
## Challenges & Roadblocks
Some of the biggest challenges I faced was overcoming the knowledge barrier I had on the inner workings
of graphics rendering and low level programming overall. Thankfuly, being stubborn and obsessed can
sometimes help, and in this case after numerous attempts, I began meticoulouly learning every aspect
of the engine as I developed it, and over time it began paying off, snowballing into a very fun experience.
Some food for thought, after you overcome the biggest barrier in your journey, the rest seems laughably easy.
## Summary
Looking back, working on Iridescent was some of the most fun I have ever had programming as of yet,
and I am still craving for the next project to top this one. It was only by working on this engine
that I realized just how much I love working on the little details, obsessing over each little system
in the engine. Definitely something I will have to do again, maybe write a little game, but thats for
future me to decide :D

View File

@@ -0,0 +1,179 @@
---
title: "Revive Auto Parts"
description: "A car parts listing site built for a client"
demoUrl: "https://reviveauto.parts"
techStack: ["Tanstack", "React Query", "Fastapi"]
date: "2025-01-04"
image: "/projects/reviveauto/thumbnail.jpeg"
---
## Overview
A car parts listing website built to provide an intuitive and efficient experience
for users searching for automotive parts. This project was commissioned by a client
and showcases some pretty cool modern web technologies, enabling excellent
performance and a clean user interface.
## Key Features
* **Dynamic & Simple Routing**: Powered by TanStack Router, enabling seamless navigation
and deep-linking for product categories and details.
* **Real-Time Data Fetching**: Utilized React Query to handle server state and caching,
ensuring users have up-to-date information.
* **Infinite Scroll**: Implemented React Query's infinite scrolling and memoization
optimization techniques to ensure fast and seamless scrolling through listings.
* **Dynamic Filters**: Created dynamic filters which are editable on the admin panel,
allowing for a high level of customization.
* **Responsive Design**: Built with Tailwind and Shadcn for a fully responsive layout,
providing a consistent experience across desktop and mobile devices.
* **Optimized Performance**: Leveraged Vite for fast builds and optimized code-splitting,
improving page load times drastically.
## Development Highlights
I had numerous highlights and *aha* moments when developing this site. One of these has to
be the site layout, built with [shadcn/ui](https://ui.shadcn.com) components. I had used
this component library in a previous site, but I had yet to grasp just how powerful this
collection of components is. We truly do stand on the shoulders of giants, and using this
library not only allowed me to very quickly prototype a design, but to then flesh it out
without having to dive into the weeds of UI development.
Another great highlight has to be [Tanstack Router](https://tanstack.com/router/latest).
As a seasoned developer, I have had many opportunities to try a lot of different routers
across several frameworks. As many have before me, we stumble onto the nextjs router, and
tend not to look back. However, tanstack did something I did not expect, and it takes routing
to the next level. With TanStack, the routes folder is solely focused on defining routes and
layouts, providing a cleaner, more modular structure. This is a stark contrast to Next.js's
approach, where the app directory can quickly become convoluted by mixing route definitions
with server-side logic, API calls, and other concerns. Anybody who has built a Nextjs project
bigger than a To-Do app can likely relate to the mental pain that is trying to find a route or
endpoint in your app router when its nested and hidden away four or five directories deep.
Another memorable highlight was writing my backend in Python using [Fastapi](https://fastapi.tiangolo.com/).
Sometimes, a project doesn't need a complex nodejs runtime, or an ORM built for a massive service.
As a python enthusiast, I found the combination of fastapi & sqlmodel to be just perfect for this project,
and defining api endpoints and schemas were quite enjoyable. As I do, I decided to roll my own authentication,
and found Python to be a great environment in which to do so.
Lastly, I have to touch on React Query. The combo that is React Query and Fastapi can truly be magical.
To truly showcase what I mean, here's an example of a query and endpoint working together:
```typescript
// features/auth/services/login/queries.ts
export function useLogin() {
const { setCredentials } = useUserStore();
return useMutation<LoginResponseSchemaType, AxiosError<BackendError>, LoginRequestSchemaType>({
mutationFn: user => login(user),
onSuccess: data => {
setCredentials({
accessToken: data.access_token,
});
},
});
}
const login = backend<z.infer<typeof LoginRequestSchema>, z.infer<typeof LoginResponseSchema>>({
method: "POST",
path: `${BACKEND_URL}${AUTH}/login`,
requestSchema: LoginRequestSchema,
responseSchema: LoginResponseSchema,
type: "public",
});
```
```typescript
// features/auth/services/login/schemas
import { z } from "zod";
export const LoginRequestSchema = z.object({
email: z
.string()
.min(1, "Email is required!")
.trim()
.email({ message: "Invalid email!" })
.toLowerCase(),
password: z.string().trim().min(8, { message: "Password must be at least 8 characters long!" }),
});
export type LoginRequestSchemaType = z.infer<typeof LoginRequestSchema>;
export const LoginResponseSchema = z.object({
access_token: z.string(),
});
export type LoginResponseSchemaType = z.infer<typeof LoginResponseSchema>;
```
```python
# routes/auth.py
class LoginResponse(BaseModel):
access_token: str
@router.post(
"/login",
description=
"""
The login endpoint.
Used to create access and refresh tokens for a user.
The refresh token is set as an HTTP-only cookie.
""",
summary="Create access token and set refresh token as HTTP-only cookie."
)
async def login(
response: Response,
form_data: LoginSchema = Body(...),
session: Session = Depends(get_session)
) -> Any:
# Fetch user using the form_data sent by the client
user = await service.authenticate(
email=form_data.email,
password=form_data.password,
session=session
)
# If service returns None, raise exception
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
)
# Set refresh token as HTTP-only cookie
response.set_cookie(
key="refresh_token",
value=create_token(user, type="refresh"),
httponly=True,
path="/auth/refresh",
domain=Config.JWT_COOKIE_DOMAIN,
expires=datetime.now(timezone.utc) + Config.JWT_REFRESH_EXPIRES
)
return LoginResponse(
access_token=create_token(user, type="access")
)
```
Using Zod, the frontend is validating what a user is attempting to submit
before calling the endpoint. If Zod does not throw an error, our frontend will
call our backend endpoint, which also expects the right types to be present.
Afterwards, the backend will respond back with a response that our frontend
will also validate. This allows for a complete multi-directional
request -> response type validation!
## Challenges & Roadblocks
For the most part, there were really no challenges, say for some hiccups here and there.
Probably the most painful parts were creating unit tests for the frontend, and scraping
Facebook for a total of 1,384 posts for my client, who wanted the posts imported over.
As one can imagine, that process is not simple to do manually by hand, so I wrote multiple
python scripts using the seleneum library to fetch the posts from the sellers account, a process
which took multiple attempts, several overnight scrapes, and lots of data sanitation afterwards.
Other than that, everything else was an absolute joy to work on, and I finished the project in under
15K LOC.
## Summary
Sometimes, building a CRUD app can be lots of fun, no matter how many times you've done it before.
All it takes is adding something new to the stack, and striving to improve the code you write. This
project was exactly that, a mix of new technologies working together to power a fairly neat site,
which can be viewed [here](https://reviveauto.parts). Maybe there is a car part there which the reader
might need. As always, thanks for the read :)

View File

@@ -0,0 +1,36 @@
---
title: "Website"
description: "My personal website and blog"
githubUrl: "https://github.com/timmypidashev/web"
demoUrl: "https://timmypidashev.dev"
techStack: ["Astro", "Typescript", "MDX"]
date: "2024-10-03"
image: "/projects/web/thumbnail.jpeg"
---
## Overview
A portfolio website and resume for yours truly.
## Development Highlights
Building the second version of my website was a dream. This was my first time using Astro,
and wow, just wow! For starters, The entire site is ~1,500 LOC (excluding MDX). That is
small for the amount of things I was able to accomplish. That aside, Astro's simplicity is
unmatched. Coming from React-based frameworks, the ability to just import react components
and have them work out of the box was unexpected. And the astro layout files! They are so
easy to look at, it just turns on some kind of code ocd within my head. I can't stop admiring
it. All this to say, Astro was the perfect choice for my website.
## Challenges & Roadblocks
Although the development process was extremely simple and fun, there really were no challenges,
which allowed me to express my creativity to my fullest. Building out Conway's game of life as
a canvas background simulation wasn't the first idea I had, and that whole endeavour started as a
growing vines simulation similar to that of the old Poptropica flash game loading screens, all the way
to a full fishtank simulation like Asciiquarium. But, they just never felt final to me, and this simulation
was simple to implement and most importantly very lightweight compared to the other simulations I was
working on.
## Summary
The result is deeply satisfying - a personal website that authentically reflects my style.
It captures my minimalist desktop aesthetic, down to the font choices and clean layout.
After many hours of careful work, I am very proud of what I have created for myself and hope
the reader agrees :)

9
src/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
declare namespace App {
interface Locals {
user: import("./lib/user").User | null;
session: import("./lib/session").Session | null;
}
}

View File

@@ -0,0 +1,74 @@
---
import "@/style/globals.css";
import { ClientRouter } from "astro:transitions";
import Header from "@/components/header";
import Footer from "@/components/footer";
import Background from "@/components/background";
export interface Props {
title: string;
description: string;
}
const { title, description } = Astro.props;
const ogImage = "https://timmypidashev.dev/og-image.jpg";
---
<html lang="en">
<head>
<title>{title}</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<!-- OpenGraph -->
<meta property="og:image" content={ogImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content={ogImage} />
<meta name="twitter:description" content={description} />
<!-- Basic meta description for search engines -->
<meta name="description" content={description} />
<!-- Also used in OpenGraph for social media sharing -->
<meta property="og:description" content={description} />
<link rel="icon" type="image/jpeg" href="/me.jpeg" />
<ClientRouter
defaultTransition={false}
handleFocus={false}
/>
<style>
::view-transition-new(:root) {
animation: none;
}
::view-transition-old(:root) {
animation: 90ms ease-out both fade-out;
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
</style>
</head>
<body class="bg-background text-foreground min-h-screen flex flex-col">
<Header client:load />
<main class="flex-1 flex flex-col">
<div class="max-w-5xl mx-auto pt-12 px-4 py-8 flex-1">
<Background layout="content" position="right" client:only="react" transition:persist />
<div>
<slot />
</div>
<Background layout="content" position="left" client:only="react" transition:persist />
</div>
</main>
<div class="mt-auto">
<Footer client:load transition:persist />
</div>
<script>
document.addEventListener("astro:after-navigation", () => {
window.scrollTo(0, 0);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,50 @@
---
const { content } = Astro.props;
import "@/style/globals.css";
import { ClientRouter } from "astro:transitions";
import Header from "@/components/header";
import Footer from "@/components/footer";
import Background from "@/components/background";
export interface Props {
title: string;
description: string;
}
const { title, description } = Astro.props;
const ogImage = "https://timmypidashev.dev/og-image.jpg";
---
<html lang="en">
<head>
<title>{title}</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<!-- OpenGraph -->
<meta property="og:image" content={ogImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content={ogImage} />
<meta name="twitter:description" content={description} />
<!-- Basic meta description for search engines -->
<meta name="description" content={description} />
<!-- Also used in OpenGraph for social media sharing -->
<meta property="og:description" content={description} />
<link rel="icon" type="image/jpeg" href="/me.jpeg" />
<link rel="sitemap" href="/sitemap-index.xml" />
<ClientRouter />
</head>
<body class="bg-background text-foreground">
<Header client:load />
<main transition:animate="fade">
<Background layout="index" client:only="react" transition:persist />
<slot />
</main>
<Footer client:load transition:persist fixed=true />
</body>
</html>

View File

@@ -0,0 +1,70 @@
---
import "@/style/globals.css";
import { ClientRouter } from "astro:transitions";
import Header from "@/components/header";
import Footer from "@/components/footer";
import Background from "@/components/background";
export interface Props {
title: string;
description: string;
}
const { title, description } = Astro.props;
const ogImage = "https://timmypidashev.dev/og-image.jpg";
---
<html lang="en">
<head>
<title>{title}</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<!-- OpenGraph -->
<meta property="og:image" content={ogImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content={ogImage} />
<meta name="twitter:description" content={description} />
<!-- Basic meta description for search engines -->
<meta name="description" content={description} />
<!-- Also used in OpenGraph for social media sharing -->
<meta property="og:description" content={description} />
<link rel="icon" type="image/jpeg" href="/me.jpeg" />
<ClientRouter
defaultTransition={false}
handleFocus={false}
/>
<style>
::view-transition-new(:root) {
animation: none;
}
::view-transition-old(:root) {
animation: 90ms ease-out both fade-out;
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
</style>
</head>
<body class="bg-background text-foreground min-h-screen flex flex-col">
<main class="flex-1 flex flex-col">
<div class="max-w-5xl mx-auto pt-12 px-4 py-8 flex-1">
<Background layout="content" position="right" client:only="react" transition:persist />
<div>
<slot />
</div>
<Background layout="content" position="left" client:only="react" transition:persist />
</div>
</main>
<script>
document.addEventListener("astro:after-navigation", () => {
window.scrollTo(0, 0);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,14 @@
import readingTime from "reading-time";
type Post = {
title: string
file: string
rawContent: () => string
}
export default function getPostData(post: Post) {
return {
slug: post.file.split('/').pop().split('.').shift(),
readingTime: readingTime(post.rawContent()).text,
}
}

View File

@@ -0,0 +1,59 @@
import { type Article, type Person, type WebSite, type WithContext } from "schema-dts";
import type { CollectionEntry } from 'astro:content';
export const blogWebsite: WithContext<WebSite> = {
"@context": "https://schema.org",
"@type": "WebSite",
url: `${import.meta.env.SITE}/blog/`,
name: "Timothy Pidsashev - Blog",
description: "Timothy Pidsashev's blog",
inLanguage: "en_US",
};
export const mainWebsite: WithContext<WebSite> = {
"@context": "https://schema.org",
"@type": "WebSite",
url: import.meta.env.SITE,
name: "Timothy Pidashev - Personal website",
description: "Timothy Pidashev's contact page, portfolio and blog",
inLanguage: "en_US",
};
export const personSchema: WithContext<Person> = {
"@context": "https://schema.org",
"@type": "Person",
name: "Timothy Pidashev",
url: "https://timmypidashev.dev",
sameAs: [
"https://github.com/timmypidashev",
"https://www.linkedin.com/in/timothy-pidashev-4353812b8",
],
jobTitle: "Software Engineer",
worksFor: {
"@type": "Organization",
name: "Fathers House Christian Center",
url: "https://fhccenter.org",
},
};
export function getArticleSchema(post: CollectionEntry<"blog">) {
const articleStructuredData: WithContext<Article> = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.data.title,
url: `${import.meta.env.SITE}/blog/${post.slug}/`,
description: post.data.excerpt,
datePublished: post.data.date.toString(),
publisher: {
"@type": "Person",
name: "Timothy Pidashev",
url: import.meta.env.SITE,
},
author: {
"@type": "Person",
name: "Timothy Pidashev",
url: import.meta.env.SITE,
},
};
return articleStructuredData;
}

18
src/src/pages/404.astro Normal file
View File

@@ -0,0 +1,18 @@
---
import IndexLayout from "@/layouts/index.astro";
import GlitchText from "@/components/404/glitched-text";
const title = "404 Not Found";
---
<IndexLayout content={{ title: "404 | Timothy Pidashev" }}>
<main class="min-h-screen flex flex-col items-center justify-center p-4 text-center">
<GlitchText client:only />
<p class="text-xl text-orange mb-8">Whoops! This page doesn't exist :(</p>
<button
onclick="window.history.back()"
class="underline text-green hover:opacity-70 transition-opacity"
>
go back
</button>
</main>
</IndexLayout>

40
src/src/pages/about.astro Normal file
View File

@@ -0,0 +1,40 @@
---
import "@/style/globals.css"
import ContentLayout from "@/layouts/content.astro";
import Intro from "@/components/about/intro";
import AllTimeStats from "@/components/about/stats-alltime";
import DetailedStats from "@/components/about/stats-detailed";
import Timeline from "@/components/about/timeline";
import CurrentFocus from "@/components/about/current-focus";
import OutsideCoding from "@/components/about/outside-coding";
---
<ContentLayout
title="About | Timothy Pidashev"
description="A software engineer passionate about the web, open source, and building innovative solutions."
>
<div class="min-h-screen">
<section class="h-screen flex items-center justify-center">
<Intro client:load />
</section>
<section class="flex items-center justify-center py-16">
<AllTimeStats client:only="react" />
</section>
<section class="flex items-center justify-center py-16">
<DetailedStats client:only="react" />
</section>
<section class="flex items-center justify-center py-16">
<Timeline client:load />
</section>
<section class="flex items-center justify-center py-16">
<CurrentFocus client:load />
</section>
<section class="flex items-center justify-center py-16">
<OutsideCoding client:load />
</section>
</div>
</ContentLayout>

View File

@@ -0,0 +1,32 @@
import type { APIRoute } from 'astro';
export const GET: APIRoute = async () => {
const WAKATIME_API_KEY = import.meta.env.WAKATIME_API_KEY;
try {
const response = await fetch(
'https://wakatime.com/api/v1/users/current/summaries?range=last_6_months', {
headers: {
'Authorization': `Basic ${Buffer.from(WAKATIME_API_KEY).toString('base64')}`
}
}
);
const data = await response.json();
return new Response(
JSON.stringify({ data: data.data }),
{
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
);
} catch (error) {
console.error('API Error:', error);
return new Response(
JSON.stringify({ error: 'Failed to fetch WakaTime data' }),
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,22 @@
import type { APIRoute } from "astro";
import { exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
export const GET: APIRoute = async () => {
try {
const { stdout } = await execAsync(`curl -H "Authorization: Basic ${import.meta.env.WAKATIME_API_KEY}" https://wakatime.com/api/v1/users/current/all_time_since_today`);
return new Response(stdout, {
status: 200,
headers: {
"Content-Type": "application/json"
}
});
} catch (error) {
return new Response(JSON.stringify({ error: "Failed to fetch stats" }), {
status: 500
});
}
}

View File

@@ -0,0 +1,33 @@
// src/pages/api/wakatime/detailed.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async () => {
const WAKATIME_API_KEY = import.meta.env.WAKATIME_API_KEY;
try {
const response = await fetch(
'https://wakatime.com/api/v1/users/current/stats/last_7_days?timeout=15', {
headers: {
'Authorization': `Basic ${Buffer.from(WAKATIME_API_KEY).toString('base64')}`
}
}
);
const data = await response.json();
return new Response(
JSON.stringify({ data: data.data }),
{
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
);
} catch (error) {
console.error('API Error:', error);
return new Response(
JSON.stringify({ error: 'Failed to fetch WakaTime data' }),
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,106 @@
---
import { getCollection } from "astro:content";
import { Image } from "astro:assets";
import ContentLayout from "@/layouts/content.astro";
import { getArticleSchema } from "@/lib/structuredData";
import { blogWebsite } from "@/lib/structuredData";
import { Comments } from "@/components/blog/comments";
// This is a dynamic route in SSR mode
const { slug } = Astro.params;
// Fetch blog posts
const posts = await getCollection("blog");
const post = posts.find(post => post.slug === slug);
if (!post || post.data.isDraft === true) {
return new Response(null, {
status: 404,
statusText: "Not found"
});
}
// Dynamically render the content
const { Content } = await post.render();
// Format the date
const formattedDate = new Date(post.data.date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric"
});
const articleStructuredData = getArticleSchema(post);
const breadcrumbsStructuredData = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Blog",
item: `${import.meta.env.SITE}/blog/`,
},
{
"@type": "ListItem",
position: 2,
name: post.data.title,
item: `${import.meta.env.SITE}/blog/${post.slug}/`,
},
],
};
const jsonLd = {
"@context": "https://schema.org",
"@graph": [articleStructuredData, breadcrumbsStructuredData, blogWebsite],
};
---
<ContentLayout
title={`${post.data.title} | Timothy Pidashev`}
description={post.data.description}
>
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
<div class="relative max-w-8xl mx-auto">
<article class="prose prose-invert prose-lg mx-auto max-w-4xl">
{post.data.image && (
<div class="-mx-4 sm:mx-0 mb-8">
<Image
src={post.data.image}
alt={post.data.title}
class="w-full h-auto rounded-lg object-cover"
width={1200}
height={630}
quality={100}
/>
</div>
)}
<h1 class="text-3xl pt-4">{post.data.title}</h1>
<p class="lg:text-2xl sm:text-lg">{post.data.description}</p>
<div class="mt-4 md:mt-6">
<div class="flex flex-wrap items-center gap-2 md:gap-3 text-sm md:text-base text-foreground/80">
<span class="text-orange">{post.data.author}</span>
<span class="text-foreground/50">•</span>
<time dateTime={post.data.date instanceof Date ? post.data.date.toISOString() : post.data.date} class="text-blue">
{formattedDate}
</time>
</div>
</div>
<div class="flex flex-wrap gap-2 mt-4 md:mt-6">
{post.data.tags.map((tag) => (
<span
class="text-xs md:text-base text-aqua hover:text-aqua-bright transition-colors duration-200"
onclick={`window.location.href='/blog/tag/${tag}'`}
>
#{tag}
</span>
))}
</div>
<div class="prose prose-invert prose-lg max-w-none">
<Content />
</div>
</article>
<Comments client:idle />
</div>
</ContentLayout>

View File

@@ -0,0 +1,29 @@
---
import { getCollection } from "astro:content";
import ContentLayout from "@/layouts/content.astro";
import { BlogHeader } from "@/components/blog/header";
import { BlogPostList } from "@/components/blog/post-list";
const posts = (await getCollection("blog", ({ data }) => {
return data.isDraft !== true;
})).sort((a, b) => {
return b.data.date.valueOf() - a.data.date.valueOf()
}).map(post => ({
...post,
data: {
...post.data,
date: post.data.date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric"
})
}
}));
---
<ContentLayout
title="Blog | Timothy Pidashev"
description="My experiences and technical insights into software development and the ever-evolving world of programming."
>
<BlogHeader client:load />
<BlogPostList posts={posts} client:load />
</ContentLayout>

View File

@@ -0,0 +1,28 @@
---
import { getCollection } from "astro:content";
import ContentLayout from "@/layouts/content.astro";
import TagList from "@/components/blog/tag-list";
const posts = (await getCollection("blog", ({ data }) => {
return data.isDraft !== true;
})).sort((a, b) => {
return b.data.date.valueOf() - a.data.date.valueOf()
}).map(post => ({
...post,
data: {
...post.data,
date: post.data.date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric"
})
}
}));
---
<ContentLayout
title="Blog | Timothy Pidashev"
description="My experiences and technical insights into software development and the ever-evolving world of programming."
>
<TagList posts={posts} />
</ContentLayout>

15
src/src/pages/index.astro Normal file
View File

@@ -0,0 +1,15 @@
---
export const prerender = false;
import "@/style/globals.css"
import IndexLayout from "@/layouts/index.astro";
import Hero from "@/components/hero";
---
<IndexLayout
title="Timothy Pidashev"
description="Software engineer passionate about systems programming, artificial intelligence, and building innovative solutions. Personal site featuring my projects, technical blog, and research."
>
<Hero client:load />
</IndexLayout>

View File

@@ -0,0 +1,68 @@
---
export const prerender = true;
import { getCollection } from "astro:content";
import ContentLayout from "@/layouts/content.astro";
import { Comments } from "@/components/blog/comments";
export async function getStaticPaths() {
const projects = await getCollection("projects");
return projects.map(project => ({
params: { slug: project.slug },
props: { project },
}));
}
const { project } = Astro.props;
const { Content } = await project.render();
---
<ContentLayout title={`${project.data.title} | Timothy Pidashev`}>
<article class="w-full mx-auto px-4 pt-6 sm:pt-12">
{/* Image Section */}
{project.data.image && (
<div class="w-full rounded-lg overflow-hidden mb-8 border-2 border-foreground/20 aspect-video">
<img
src={project.data.image}
alt={project.data.title}
class="w-full h-full object-cover"
/>
</div>
)}
<header class="mb-8">
<h1 class="text-3xl sm:text-4xl font-bold text-yellow-bright mb-4">
{project.data.title}
</h1>
<div class="flex flex-wrap gap-2 mb-4">
{project.data.techStack.map(tech => (
<span class="text-xs px-2 py-1 rounded-full bg-purple-bright/10 text-purple-bright">
{tech}
</span>
))}
</div>
<div class="flex gap-4">
{project.data.githubUrl && (
<a href={project.data.githubUrl}
class="text-foreground/70 hover:text-blue-bright transition-colors">
View Source
</a>
)}
{project.data.demoUrl && (
<a href={project.data.demoUrl}
class="text-foreground/70 hover:text-green-bright transition-colors">
Live Link
</a>
)}
</div>
</header>
<div class="prose prose-invert prose-lg max-w-none">
<Content />
</div>
</article>
<Comments client:idle />
</ContentLayout>

View File

@@ -0,0 +1,18 @@
---
import { getCollection } from "astro:content";
import ContentLayout from "@/layouts/content.astro";
import { ProjectList } from "@/components/projects/project-list";
const projects = (await getCollection("projects", ({ data }) => {
return data.isDraft !== true;
})).sort(
(a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
);
---
<ContentLayout
title="Projects | Timothy Pidashev"
description="My portfolio of software, including web apps, dev tools, and passion projects."
>
<ProjectList projects={projects} client:load />
</ContentLayout>

View File

@@ -0,0 +1,15 @@
---
import "@/style/globals.css"
import ContentLayout from "@/layouts/content.astro";
import Resume from "@/components/resume";
---
<ContentLayout
title="Resume | Timothy Pidashev"
description = "My professional experience, skills, and development journey in software engineering."
>
<div class="flex items-center justify-center w-full">
<Resume client:load />
</div>
</ContentLayout>

View File

@@ -0,0 +1,13 @@
import type { APIRoute } from "astro";
const getRobotsTxt = (sitemapURL: URL) => `
User-agent: *
Allow: /
Sitemap: ${sitemapURL.href}
`;
export const GET: APIRoute = ({ site }) => {
const sitemapURL = new URL("sitemap-index.xml", site);
return new Response(getRobotsTxt(sitemapURL));
};

29
src/src/pages/rss.ts Normal file
View File

@@ -0,0 +1,29 @@
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import type { APIContext } from "astro";
export async function GET(context: APIContext) {
const blog = await getCollection("blog");
const sortedPosts = blog
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
return rss({
title: "Timothy Pidashev",
description: "My experiences and technical insights into software development and the ever-evolving world of programming.",
site: context.site!,
items: sortedPosts.map((post) => ({
title: post.data.title,
pubDate: post.data.date,
description: post.data.description,
link: `/blog/${post.slug}/`,
author: post.data.author,
categories: post.data.tags,
enclosure: post.data.image ? {
url: new URL(`blog/${post.slug}/thumbnail.png`, context.site).toString(),
type: 'image/jpeg',
length: 0
} : undefined
})),
});
}

348
src/src/style/comments.css Normal file
View File

@@ -0,0 +1,348 @@
/*!
* Gruvbox Dark Theme for Giscus - Minimalist Edition
* Pure black background with simplified design
* Hosted on a linode bucket
*/
main {
/* Syntax highlighting colors */
--color-prettylights-syntax-comment: #928374;
--color-prettylights-syntax-constant: #83a598;
--color-prettylights-syntax-entity: #d3869b;
--color-prettylights-syntax-storage-modifier-import: #ebdbb2;
--color-prettylights-syntax-entity-tag: #b8bb26;
--color-prettylights-syntax-keyword: #fb4934;
--color-prettylights-syntax-string: #83a598;
--color-prettylights-syntax-variable: #fe8019;
--color-prettylights-syntax-brackethighlighter-unmatched: #fb4934;
--color-prettylights-syntax-invalid-illegal-text: #ebdbb2;
--color-prettylights-syntax-invalid-illegal-bg: #cc241d;
--color-prettylights-syntax-carriage-return-text: #ebdbb2;
--color-prettylights-syntax-carriage-return-bg: #cc241d;
--color-prettylights-syntax-string-regexp: #b8bb26;
--color-prettylights-syntax-markup-list: #fabd2f;
--color-prettylights-syntax-markup-heading: #83a598;
--color-prettylights-syntax-markup-italic: #ebdbb2;
--color-prettylights-syntax-markup-bold: #fe8019;
--color-prettylights-syntax-markup-deleted-text: #fb4934;
--color-prettylights-syntax-markup-deleted-bg: #000000;
--color-prettylights-syntax-markup-inserted-text: #b8bb26;
--color-prettylights-syntax-markup-inserted-bg: #000000;
--color-prettylights-syntax-markup-changed-text: #fabd2f;
--color-prettylights-syntax-markup-changed-bg: #000000;
--color-prettylights-syntax-markup-ignored-text: #ebdbb2;
--color-prettylights-syntax-markup-ignored-bg: #000000;
--color-prettylights-syntax-meta-diff-range: #d3869b;
--color-prettylights-syntax-brackethighlighter-angle: #928374;
--color-prettylights-syntax-sublimelinter-gutter-mark: #928374;
--color-prettylights-syntax-constant-other-reference-link: #83a598;
/* Button colors */
--color-btn-text: #ebdbb2;
--color-btn-bg: #000000;
--color-btn-border: #3c3836;
--color-btn-shadow: 0 0 #0000;
--color-btn-inset-shadow: 0 0 #0000;
--color-btn-hover-bg: #282828;
--color-btn-hover-border: #504945;
--color-btn-active-bg: #1d2021;
--color-btn-active-border: #504945;
--color-btn-selected-bg: #000000;
/* Primary button colors */
--color-btn-primary-text: #ebdbb2;
--color-btn-primary-bg: #458588;
--color-btn-primary-border: #000000;
--color-btn-primary-shadow: 0 0 #0000;
--color-btn-primary-inset-shadow: 0 0 #0000;
--color-btn-primary-hover-bg: #83a598;
--color-btn-primary-hover-border: #000000;
--color-btn-primary-selected-bg: #458588;
--color-btn-primary-selected-shadow: 0 0 #0000;
--color-btn-primary-disabled-text: #ebdbb280;
--color-btn-primary-disabled-bg: #45858899;
--color-btn-primary-disabled-border: #000000;
/* Control colors */
--color-action-list-item-default-hover-bg: #28282850;
--color-segmented-control-bg: #28282850;
--color-segmented-control-button-bg: #000000;
--color-segmented-control-button-selected-border: #504945;
/* Foreground colors */
--color-fg-default: #ebdbb2;
--color-fg-muted: #a89984;
--color-fg-subtle: #7c6f64;
/* Background colors - pure black */
--color-canvas-default: #000000;
--color-canvas-overlay: #000000;
--color-canvas-inset: #000000;
--color-canvas-subtle: #0a0a0a;
/* Border colors - minimal */
--color-border-default: #28282880;
--color-border-muted: #28282850;
--color-neutral-muted: #28282840;
/* Accent colors */
--color-accent-fg: #83a598;
--color-accent-emphasis: #458588;
--color-accent-muted: #83a59866;
--color-accent-subtle: #83a5981a;
/* Status colors */
--color-success-fg: #b8bb26;
--color-attention-fg: #fabd2f;
--color-attention-muted: #fabd2f66;
--color-attention-subtle: #fabd2f26;
--color-danger-fg: #fb4934;
--color-danger-muted: #fb493466;
--color-danger-subtle: #fb49341a;
/* Shadow color */
--color-primer-shadow-inset: 0 0 #0000;
/* Scale colors */
--color-scale-gray-7: #282828;
--color-scale-blue-8: #458588;
/* Social reaction colors */
--color-social-reaction-bg-hover: #28282850;
--color-social-reaction-bg-reacted-hover: #45858850;
}
/* Loader SVG */
main .pagination-loader-container {
background-image: url(https://github.com/images/modules/pulls/progressive-disclosure-line-dark.svg);
}
/* Custom Giscus styles - Minimalist */
.gsc-reactions-count {
display: none;
}
.gsc-timeline {
flex-direction: column-reverse;
}
.gsc-header {
padding-bottom: 1rem;
font-family: "Comic Code", monospace;
border-bottom: none;
}
.gsc-comments > .gsc-header {
order: 1;
}
.gsc-comments > .gsc-comment-box {
margin-bottom: 1rem;
order: 2;
font-family: "Comic Code", monospace;
background-color: #000000;
border-radius: 0.25rem;
border: 1px solid #28282850;
}
.gsc-comments > .gsc-timeline {
order: 3;
}
.gsc-homepage-bg {
background-color: #000000;
}
/* Loading image */
main .gsc-loading-image {
background-image: url(https://github.githubassets.com/images/mona-loading-dimmed.gif);
}
/* Additional custom styles - Minimalist */
.gsc-comment {
border: 1px solid #28282850;
border-radius: 0.25rem;
margin-bottom: 1rem;
background-color: #000000;
transition: border-color 0.2s ease;
}
.gsc-comment-header {
background-color: #0a0a0a;
padding: 0.75rem;
border-bottom: 1px solid #28282830;
font-family: "Comic Code", monospace;
}
.gsc-comment-content {
padding: 1rem;
font-family: "Comic Code", monospace;
}
.gsc-comment-author {
color: var(--color-fg-default);
font-weight: 600;
}
.gsc-comment-author-avatar img {
border-radius: 50%;
}
.gsc-comment-reactions {
border-top: none;
padding-top: 0.5rem;
}
.gsc-reply-box {
background-color: #000000;
border-radius: 0.25rem;
margin-top: 0.5rem;
margin-left: 1rem;
font-family: "Comic Code", monospace;
border: 1px solid #28282840;
}
/* Text input areas */
.gsc-comment-box-textarea {
background-color: #0a0a0a;
border: 1px solid #28282850;
border-radius: 0.25rem;
color: var(--color-fg-default);
font-family: "Comic Code", monospace;
padding: 0.75rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.gsc-comment-box-textarea:focus {
border-color: var(--color-accent-fg);
box-shadow: 0 0 0 2px #45858830;
outline: none;
}
/* Buttons */
.gsc-comment-box-buttons button {
font-family: "Comic Code", monospace;
border-radius: 0.25rem;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
/* Code block styling */
.gsc-comment pre {
background-color: #0a0a0a;
border-radius: 0.25rem;
padding: 1rem;
overflow-x: auto;
border: 1px solid #28282840;
}
.gsc-comment code {
font-family: "Comic Code", monospace;
background-color: #0a0a0a;
color: var(--color-prettylights-syntax-entity);
padding: 0.2em 0.4em;
border-radius: 0.25rem;
}
/* Add hover effects - subtle */
.gsc-comment:hover {
border-color: #504945;
}
.gsc-social-reaction-summary-item:hover {
background-color: #28282850;
}
/* Dark scrollbar - minimal */
.gsc-timeline::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.gsc-timeline::-webkit-scrollbar-track {
background: #000000;
border-radius: 4px;
}
.gsc-timeline::-webkit-scrollbar-thumb {
background: #28282880;
border-radius: 4px;
}
.gsc-timeline::-webkit-scrollbar-thumb:hover {
background: #3c3836;
}
/* Remove unnecessary borders and separators */
.gsc-comment-footer,
.gsc-comment-footer-separator,
.gsc-reactions-button,
.gsc-social-reaction-summary-item:not(:hover) {
border: none;
}
.gsc-upvote svg {
fill: #83a598;
}
.gsc-downvote svg {
fill: #fb4934;
}
/* Add subtle hover transitions */
.gsc-comment-box,
.gsc-comment,
.gsc-comment-reactions,
button,
.gsc-reply-box {
transition: all 0.2s ease-in-out;
}
.gsc-main {
border: none !important;
}
.gsc-left-header {
background-color: #000000 !important;
}
.gsc-right-header {
background-color: #000000 !important;
}
.gsc-header-status {
background-color: #000000 !important;
}
/* Remove any additional borders */
.gsc-comment-box,
.gsc-comment-box-md-toolbar,
.gsc-comment-box-buttons {
border: none !important;
}
.gsc-comment-box-md-toolbar-item {
color: #83a598 !important;
}
/* Simplify the editor toolbar */
.gsc-comment-box-md-toolbar {
background-color: #0a0a0a !important;
padding: 0.5rem !important;
}
/* Hide the "powered by giscus" text */
.gsc-comments .gsc-powered-by {
display: none !important;
}
/* Alternative approach if the above doesn't work */
.gsc-comments footer {
display: none !important;
}
/* Another approach targeting specifically the text */
.gsc-comments .gsc-powered-by a {
color: #000000 !important;
opacity: 0 !important;
visibility: hidden !important;
}

64
src/src/style/globals.css Normal file

File diff suppressed because one or more lines are too long

222
src/tailwind.config.cjs Normal file
View File

@@ -0,0 +1,222 @@
module.exports = {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
theme: {
extend: {
fontFamily: {
"comic-code": ["Comic Code", "monospace"],
},
colors: {
background: "#000000",
foreground: "#ebdbb2",
red: {
DEFAULT: "#cc241d",
bright: "#fb4934"
},
orange: {
DEFAULT: "#d65d0e",
bright: "#fe8019"
},
green: {
DEFAULT: "#98971a",
bright: "#b8bb26"
},
yellow: {
DEFAULT: "#d79921",
bright: "#fabd2f"
},
blue: {
DEFAULT: "#458588",
bright: "#83a598"
},
purple: {
DEFAULT: "#b16286",
bright: "#d3869b"
},
aqua: {
DEFAULT: "#689d6a",
bright: "#8ec07c"
}
},
keyframes: {
"draw-line": {
"0%": { "stroke-dashoffset": "100" },
"100%": { "stroke-dashoffset": "0" }
},
"fade-in": {
"0%": { opacity: "0" },
"100%": { opacity: "1" }
}
},
animation: {
"draw-line": "draw-line 0.6s ease-out forwards",
"fade-in": "fade-in 0.3s ease-in-out forwards"
},
typography: (theme) => ({
DEFAULT: {
css: {
color: theme("colors.foreground"),
"--tw-prose-body": theme("colors.foreground"),
"--tw-prose-headings": theme("colors.yellow.bright"),
"--tw-prose-links": theme("colors.blue.bright"),
"--tw-prose-bold": theme("colors.orange.bright"),
"--tw-prose-quotes": theme("colors.green.bright"),
"--tw-prose-code": theme("colors.purple.bright"),
"--tw-prose-hr": theme("colors.foreground"),
"--tw-prose-bullets": theme("colors.foreground"),
// Base text color
color: theme("colors.foreground"),
// Headings
h1: {
color: theme("colors.yellow.bright"),
fontWeight: "700",
},
h2: {
color: theme("colors.yellow.bright"),
fontWeight: "600",
},
h3: {
color: theme("colors.yellow.bright"),
fontWeight: "600",
},
h4: {
color: theme("colors.yellow.bright"),
fontWeight: "600",
},
// Links
a: {
color: theme("colors.blue.bright"),
"&:hover": {
color: theme("colors.blue.DEFAULT"),
},
textDecoration: "none",
borderBottom: `1px solid ${theme("colors.blue.bright")}`,
transition: "all 0.2s ease-in-out",
},
// Code
'code:not([data-language])': {
color: theme('colors.purple.bright'),
backgroundColor: '#282828',
padding: '0',
borderRadius: '0.25rem',
fontFamily: 'Comic Code, monospace',
fontWeight: '400',
fontSize: 'inherit', // Match the parent text size
'&::before': { content: 'none' },
'&::after': { content: 'none' },
},
'pre': {
backgroundColor: '#282828',
color: theme("colors.foreground"),
borderRadius: '0.5rem',
overflow: 'visible', // This allows the copy button to be positioned outside
position: 'relative', // For the copy button positioning
marginTop: '1.5rem', // Space for the copy button and language label
fontSize: 'inherit', // Match the parent font size
},
'pre code': {
display: 'block',
fontFamily: 'Comic Code, monospace',
fontSize: '1em', // This will inherit from the prose-lg setting
padding: '0',
overflow: 'auto', // Enable horizontal scrolling
whiteSpace: 'pre',
'&::before': { content: 'none' },
'&::after': { content: 'none' },
},
'[data-rehype-pretty-code-fragment]:nth-of-type(2) pre': {
'[data-line]::before': {
content: 'counter(line)',
counterIncrement: 'line',
display: 'inline-block',
width: '1rem',
marginRight: '1rem',
textAlign: 'right',
color: '#86e1fc',
},
'[data-highlighted-line]::before': {
color: '#86e1fc',
},
},
// Bold
strong: {
color: theme("colors.orange.bright"),
fontWeight: "600",
},
// Lists
ul: {
li: {
"&::before": {
backgroundColor: theme("colors.foreground"),
},
},
},
// Blockquotes
blockquote: {
borderLeftColor: theme("colors.green.bright"),
color: theme("colors.green.bright"),
fontStyle: "italic",
quotes: "\"\\201C\"\"\\201D\"\"\\2018\"\"\\2019\"",
p: {
"&::before": { content: "none" },
"&::after": { content: "none" },
},
},
// Horizontal rules
hr: {
borderColor: theme("colors.foreground"),
opacity: "0.2",
},
// Table
table: {
thead: {
borderBottomColor: theme("colors.foreground"),
th: {
color: theme("colors.yellow.bright"),
},
},
tbody: {
tr: {
borderBottomColor: theme("colors.foreground"),
},
},
},
// Images
img: {
borderRadius: "0.375rem",
},
// Figures
figcaption: {
color: theme("colors.foreground"),
opacity: "0.8",
},
},
},
lg: {
css: {
"pre code": {
fontSize: "1rem",
},
},
},
}),
},
},
plugins: [
require("@tailwindcss/typography"),
],
};

11
src/tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Some files were not shown because too many files have changed in this diff Show More