Compare commits

..

63 Commits

Author SHA1 Message Date
dependabot[bot]
ec8c56bc90 Bump js-yaml from 4.1.0 to 4.1.1 in /src
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-15 11:13:59 +00:00
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
62 changed files with 4923 additions and 2321 deletions

View File

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

View File

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

View File

@@ -24,4 +24,4 @@ RUN echo "PUBLIC_VERSION=${CONTAINER_WEB_VERSION}" > /app/.env && \
EXPOSE 3000 EXPOSE 3000
CMD ["pnpm", "install", "&&", "pnpm", "run", "dev"] CMD ["pnpm", "run", "dev"]

View File

@@ -35,11 +35,13 @@ RUN pnpm run build
FROM node:22-alpine FROM node:22-alpine
WORKDIR /app WORKDIR /app
# Install serve
RUN npm install -g serve
# Copy built files # Copy built files
COPY --from=builder /app/dist ./dist 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 EXPOSE 3000
CMD ["serve", "-s", "dist", "-l", "3000"]
# Deployment command
CMD ["node", "./dist/server/entry.mjs"]

3
.gitignore vendored
View File

@@ -1,5 +1,8 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Env
.env
# astro # astro
.astro/ .astro/

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

152
Makefile
View File

@@ -1,13 +1,13 @@
PROJECT_NAME := "timmypidashev.dev" PROJECT_NAME := "timmypidashev.dev"
PROJECT_AUTHORS := "Timothy Pidashev (timmypidashev) <pidashev.tim@gmail.com>" PROJECT_AUTHORS := "Timothy Pidashev (timmypidashev) <pidashev.tim@gmail.com>"
PROJECT_VERSION := "v1.0.1" PROJECT_VERSION := "v2.1.1"
PROJECT_LICENSE := "MIT" PROJECT_LICENSE := "MIT"
PROJECT_SOURCES := "https://github.com/timmypidashev/web" PROJECT_SOURCES := "https://github.com/timmypidashev/web"
PROJECT_REGISTRY := "ghcr.io/timmypidashev/web" PROJECT_REGISTRY := "ghcr.io/timmypidashev"
PROJECT_ORGANIZATION := "org.opencontainers" PROJECT_ORGANIZATION := "org.opencontainers"
CONTAINER_WEB_NAME := "web" CONTAINER_WEB_NAME := "timmypidashev.dev"
CONTAINER_WEB_VERSION := "v1.0.1" CONTAINER_WEB_VERSION := "v2.1.1"
CONTAINER_WEB_LOCATION := "src/" CONTAINER_WEB_LOCATION := "src/"
CONTAINER_WEB_DESCRIPTION := "My portfolio website!" CONTAINER_WEB_DESCRIPTION := "My portfolio website!"
@@ -17,26 +17,41 @@ CONTAINER_WEB_DESCRIPTION := "My portfolio website!"
help: help:
@echo "Available targets:" @echo "Available targets:"
@echo " run - Runs the docker compose file with the specified environment (local, dev, preview, or release)" @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 " build - Builds the specified docker image with the appropriate environment"
@echo " push - Pushes the built docker image to the registry" @echo " push - Pushes the built docker image to the registry"
@echo " pull - Pulls the latest specified docker image from the registry"
@echo " prune - Removes all built and cached docker images and containers" @echo " prune - Removes all built and cached docker images and containers"
@echo " bump - Bumps the project and container versions" @echo " bump - Bumps the project and container versions"
@echo " exec - Spawns a shell to execute commands from within a running container"
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: build:
# Arguments # Arguments
# [container]: Build context(which container to build ['all' to build every container defined]) # [container]: Build context(which container to build ['all' to build every container defined])
# [environment]: 'local', 'dev', 'preview', or 'release' # [environment]: 'local' or 'release'
# #
# Explanation: # Explanation:
# * Builds the specified docker image with the appropriate environment. # * Builds the specified docker image with the appropriate environment.
# * Passes all generated arguments to docker build-kit. # * Passes all generated arguments to docker build-kit.
# * Installs pre-commit hooks if in a git repository.
# Install pre-commit hooks if in a git repository.
$(call install_precommit)
# Extract container and environment inputted. # Extract container and environment inputted.
$(eval INPUT_TARGET := $(word 2,$(MAKECMDGOALS))) $(eval INPUT_TARGET := $(word 2,$(MAKECMDGOALS)))
@@ -49,33 +64,6 @@ build:
$(foreach container,$(containers),$(call container_build,$(container) $(INPUT_ENVIRONMENT))), \ $(foreach container,$(containers),$(call container_build,$(container) $(INPUT_ENVIRONMENT))), \
$(call container_build,$(INPUT_CONTAINER) $(INPUT_ENVIRONMENT))) $(call container_build,$(INPUT_CONTAINER) $(INPUT_ENVIRONMENT)))
run:
# Arguments:
# [environment]: 'local', 'dev', 'preview', or 'release'
#
# Explanation:
# * Runs the docker compose file with the specified environment(compose.local.yml, compose.dev.yml, compose.preview.yml, or compose.release.yml)
# * Passes all generated arguments to the compose file.
# * Installs pre-commit hooks if in a git repository.
# Install pre-commit hooks if in a git repository.
$(call install_precommit)
# Make sure we have been given proper arguments.
@if [ "$(word 2,$(MAKECMDGOALS))" = "local" ]; then \
echo "Running in local environment"; \
docker compose -f compose.$(word 2,$(MAKECMDGOALS)).yml up --watch --remove-orphans; \
elif [ "$(word 2,$(MAKECMDGOALS))" = "preview" ]; then \
echo "Running in preview environment"; \
docker compose -f compose.$(word 2,$(MAKECMDGOALS)).yml up --remove-orphans; \
elif [ "$(word 2,$(MAKECMDGOALS))" = "release" ]; then \
echo "Running in release environment"; \
docker compose -f compose.$(word 2,$(MAKECMDGOALS)).yml up --remove-orphans; \
else \
echo "Invalid usage. Please use 'make run <'local', 'dev', 'preview', or 'release'>"; \
exit 1; \
fi
push: push:
# Arguments # Arguments
# [container]: Push context(which container to push to the registry) # [container]: Push context(which container to push to the registry)
@@ -93,45 +81,7 @@ push:
# NOTE: docker will complain if the container tag is invalid, no need to validate here. # NOTE: docker will complain if the container tag is invalid, no need to validate here.
@docker push $(PROJECT_REGISTRY)/$(INPUT_CONTAINER):$(INPUT_VERSION) @docker push $(PROJECT_REGISTRY)/$(INPUT_CONTAINER):$(INPUT_VERSION)
pull:
# TODO: FIX COMMAND PULL
# Arguments
# [container]: Pull context (which container to pull from the registry)
# [environment]: 'local', 'dev', 'preview', or 'release'
#
# Explanation:
# * Pulls the specified container version from the registry defined in the user configuration.
# * Uses sed and basename to extract the repository name.
# 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))))
# Validate environment
@if [ "$(strip $(INPUT_ENVIRONMENT))" != "local" ] [ "$(strip $(INPUT_ENVIRONMENT))" != "dev" ] && [ "$(strip $(INPUT_ENVIRONMENT))" != "preview" ] && [ "$(strip $(INPUT_ENVIRONMENT))" != "release" ]; then \
echo "Invalid environment. Please specify 'local', 'dev', 'preview', or 'release'"; \
exit 1; \
fi
# Extract repository name from PROJECT_SOURCES using sed and basename
$(eval REPO_NAME := $(shell echo "$(PROJECT_SOURCES)" | sed 's|https://github.com/[^/]*/||' | sed 's/\.git$$//' | xargs basename))
# Determine the correct tag based on the environment and container
$(eval TAG := $(if $(filter $(INPUT_ENVIRONMENT),local),\
$(INPUT_CONTAINER):$(INPUT_ENVIRONMENT),\
$(if $(filter $(INPUT_CONTAINER),$(REPO_NAME)),\
$(PROJECT_REGISTRY):$(if $(filter $(INPUT_ENVIRONMENT),prev),prev,$(call container_version,$(INPUT_CONTAINER))),\
$(PROJECT_REGISTRY)/$(INPUT_CONTAINER):$(if $(filter $(INPUT_ENVIRONMENT),prev),prev,$(call container_version,$(INPUT_CONTAINER))))))
# Pull the specified container from the registry
@echo "Pulling container: $(INPUT_CONTAINER)"
@echo "Environment: $(INPUT_ENVIRONMENT)"
@echo "Tag: $(TAG)"
@docker pull $(TAG)
prune: prune:
# TODO: IMPLEMENT COMMAND PRUNE
# Removes all built and cached docker images and containers. # Removes all built and cached docker images and containers.
bump: bump:
@@ -164,22 +114,7 @@ bump:
# Bump the container version and global version in the Makefile # 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/^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; 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;
# Commit and push to git origin
git add .
git commit -a -S -m "Bump $(INPUT_CONTAINER) to v$(shell docker run usvc/semver:latest bump $(INPUT_SEMANTIC_TYPE) $(OLD_PROJECT_VERSION))"
git push
git push origin tag v$(shell docker run usvc/semver:latest bump $(INPUT_SEMANTIC_TYPE) $(OLD_PROJECT_VERSION))
exec:
# 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))))
$(eval COMPOSE_FILE := compose.$(INPUT_ENVIRONMENT).yml)
docker compose -f $(COMPOSE_FILE) run --service-ports $(INPUT_CONTAINER) sh
# This function generates Docker build arguments based on variables defined in the 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. # It extracts variable assignments, removes whitespace, and formats them as build arguments.
# Additionally, it appends any custom shell generated arguments defined below. # Additionally, it appends any custom shell generated arguments defined below.
@@ -194,7 +129,6 @@ define args
gsub(":", "", $$1); \ gsub(":", "", $$1); \
printf "--build-arg %s=%s ", $$1, $$2 \ printf "--build-arg %s=%s ", $$1, $$2 \
}') \ }') \
--build-arg ENVIRONMENT='"$(shell echo $(INPUT_ENVIRONMENT))"' \
--build-arg BUILD_DATE='"$(shell date)"' \ --build-arg BUILD_DATE='"$(shell date)"' \
--build-arg GIT_COMMIT='"$(shell git rev-parse HEAD)"' --build-arg GIT_COMMIT='"$(shell git rev-parse HEAD)"'
endef endef
@@ -221,19 +155,22 @@ define container_build
$(eval ENVIRONMENT := $(word 2,$1)) $(eval ENVIRONMENT := $(word 2,$1))
$(eval ARGS := $(shell echo $(args))) $(eval ARGS := $(shell echo $(args)))
$(eval VERSION := $(strip $(call container_version,$(CONTAINER)))) $(eval VERSION := $(strip $(call container_version,$(CONTAINER))))
$(eval PROJECT := $(strip $(subst ",,$(PROJECT_NAME)))) $(eval TAG := $(PROJECT_NAME):$(ENVIRONMENT))
$(eval TAG := $(PROJECT).$(CONTAINER):$(ENVIRONMENT))
@echo "Building container: $(CONTAINER)" @echo "Building container: $(CONTAINER)"
@echo "Environment: $(ENVIRONMENT)" @echo "Environment: $(ENVIRONMENT)"
@echo "Version: $(VERSION)" @echo "Version: $(VERSION)"
@if [ "$(strip $(ENVIRONMENT))" != "local" ] && [ "$(strip $(ENVIRONMENT))" != "dev" ] && [ "$(strip $(ENVIRONMENT))" != "preview" ] && [ "$(strip $(ENVIRONMENT))" != "release" ]; then \ @if [ "$(strip $(ENVIRONMENT))" != "local" ] && [ "$(strip $(ENVIRONMENT))" != "release" ]; then \
echo "Invalid environment. Please specify 'local', 'dev', 'preview', or 'release'"; \ echo "Invalid environment. Please specify 'local' or 'release'"; \
exit 1; \ exit 1; \
fi fi
docker buildx build --no-cache --load -t $(TAG) -f .docker/Dockerfile.$(ENVIRONMENT) ./$(strip $(subst $(SPACE),,$(call container_location,$(CONTAINER))))/. $(ARGS) $(call labels,$(shell echo $(CONTAINER_NAME) | tr '[:lower:]' '[:upper:]')) --debug $(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 endef
define container_location define container_location
@@ -241,26 +178,9 @@ define container_location
$(CONTAINER_$(CONTAINER_NAME)_LOCATION) $(CONTAINER_$(CONTAINER_NAME)_LOCATION)
endef endef
define container_name
$(strip $(shell echo '$(1)' | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]'))
endef
define container_version define container_version
$(strip $(eval CONTAINER_NAME := $(shell echo $(1) | tr '[:lower:]' '[:upper:]'))) \ $(strip $(eval CONTAINER_NAME := $(shell echo $(1) | tr '[:lower:]' '[:upper:]'))) \
$(if $(CONTAINER_$(CONTAINER_NAME)_VERSION), \ $(if $(CONTAINER_$(CONTAINER_NAME)_VERSION), \
$(shell echo $(strip $(strip $(CONTAINER_$(CONTAINER_NAME)_VERSION))) | tr -d '[:space:]'), \ $(shell echo $(strip $(strip $(CONTAINER_$(CONTAINER_NAME)_VERSION))) | tr -d '[:space:]'), \
$(error Version data for container $(1) not found)) $(error Version data for container $(1) not found))
endef endef
define install_precommit
$(strip \
$(shell \
if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then \
pre-commit install > /dev/null 2>&1; \
fi \
) \
)
endef
%:
@:

View File

@@ -1 +1,3 @@
![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"/> <img src=".github/preview.jpeg" title="Preview"/>

View File

@@ -1,35 +0,0 @@
services:
caddy:
container_name: proxy
image: caddy:latest
ports:
- 80:80
- 443:443
volumes:
- ./.caddy/Caddyfile.dev:/etc/caddy/Caddyfile:rw
networks:
- web_proxy
depends_on:
- web
web:
container_name: web
image: web:dev
volumes:
- ./src/node_modules:/app/node_modules
- ./src/sandbox.config.json:/app/sandbox.config.json
- ./src/.stackblitzrc:/app/.stackblitzrc
- ./src/astro.config.mjs:/app/astro.config.mjs
- ./src/tailwind.config.cjs:/app/tailwind.config.cjs
- ./src/tsconfig.json:/app/tsconfig.json
- ./src/pnpm-lock.yaml:/app/pnpm-lock.yaml
- ./src/package.json:/app/package.json
- ./src/public:/app/public
- ./src/src:/app/src
networks:
- web_proxy
networks:
web_proxy:
name: web_proxy
external: true

0
compose.local.yml Normal file
View File

View File

@@ -1,20 +1,28 @@
services: services:
caddy: caddy:
container_name: proxy container_name: caddy
image: caddy:latest image: caddy:latest
ports: ports:
- 80:80 - 80:80
- 443:443 - 443:443
volumes: volumes:
- ./.caddy/Caddyfile.dev:/etc/caddy/Caddyfile:rw - ./.caddy/Caddyfile.release:/etc/caddy/Caddyfile:rw
networks: networks:
- proxy_network - proxy_network
depends_on: depends_on:
- release.timmypidashev.dev - 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
release.timmypidashev.dev: timmypidashev.dev:
container_name: timmypidashev container_name: timmypidashev.dev
image: ghcr.io/timmypidashev/timmypidashev.dev:release image: ghcr.io/timmypidashev/timmypidashev.dev:latest
networks: networks:
- proxy_network - proxy_network

View File

@@ -1,8 +0,0 @@
{
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1"
}
}

90
pnpm-lock.yaml generated
View File

@@ -1,90 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@types/react':
specifier: ^18.3.12
version: 18.3.12
'@types/react-dom':
specifier: ^18.3.1
version: 18.3.1
react:
specifier: ^18.3.1
version: 18.3.1
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
packages:
'@types/prop-types@15.7.13':
resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==}
'@types/react-dom@18.3.1':
resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==}
'@types/react@18.3.12':
resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==}
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
react: ^18.3.1
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
snapshots:
'@types/prop-types@15.7.13': {}
'@types/react-dom@18.3.1':
dependencies:
'@types/react': 18.3.12
'@types/react@18.3.12':
dependencies:
'@types/prop-types': 15.7.13
csstype: 3.1.3
csstype@3.1.3: {}
js-tokens@4.0.0: {}
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
react-dom@18.3.1(react@18.3.1):
dependencies:
loose-envify: 1.4.0
react: 18.3.1
scheduler: 0.23.2
react@18.3.1:
dependencies:
loose-envify: 1.4.0
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0

View File

@@ -1,14 +1,22 @@
import { defineConfig } from "astro/config"; import { defineConfig } from "astro/config";
import node from "@astrojs/node";
import tailwind from "@astrojs/tailwind"; import tailwind from "@astrojs/tailwind";
import react from "@astrojs/react"; import react from "@astrojs/react";
import mdx from "@astrojs/mdx"; import mdx from "@astrojs/mdx";
import rehypePrettyCode from "rehype-pretty-code"; import rehypePrettyCode from "rehype-pretty-code";
import rehypeSlug from "rehype-slug"; import rehypeSlug from "rehype-slug";
import sitemap from "@astrojs/sitemap"; import sitemap from "@astrojs/sitemap";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
output: "server",
server: {
host: true,
port: 3000,
},
adapter: node({
mode: "standalone",
}),
site: "https://timmypidashev.dev", site: "https://timmypidashev.dev",
build: { build: {
// Enable build-time optimizations // Enable build-time optimizations
@@ -35,7 +43,7 @@ export default defineConfig({
rehypePrettyCode, rehypePrettyCode,
{ {
theme: { theme: {
"name": "Custom Gruvbox Dark", "name": "Darkbox",
"type": "dark", "type": "dark",
"colors": { "colors": {
"editor.background": "#000000", "editor.background": "#000000",
@@ -163,6 +171,7 @@ export default defineConfig({
} }
], ],
}, },
keepBackground: true,
}, },
], ],
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "src", "name": "src",
"version": "v1.0.1", "version": "2.1.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "astro dev --host", "dev": "astro dev --host",
@@ -8,25 +8,36 @@
"preview": "astro preview" "preview": "astro preview"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/react": "^4.1.2", "@astrojs/react": "^4.4.0",
"@astrojs/tailwind": "^5.1.4", "@astrojs/tailwind": "^6.0.2",
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/typography": "^0.5.16",
"@types/react": "^18.3.12", "@types/react": "^18.3.20",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.6",
"astro": "^5.1.2", "astro": "^5.14.1",
"tailwindcss": "^3.4.15" "tailwindcss": "^3.4.17"
}, },
"dependencies": { "dependencies": {
"@astrojs/mdx": "^4.0.3", "@astrojs/mdx": "^4.3.6",
"@astrojs/sitemap": "^3.2.1", "@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", "lucide-react": "^0.468.0",
"marked": "^15.0.8",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-responsive": "^10.0.0", "react-icons": "^5.5.0",
"react-responsive": "^10.0.1",
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
"rehype-pretty-code": "^0.14.0", "rehype-pretty-code": "^0.14.1",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"schema-dts": "^1.1.2", "schema-dts": "^1.1.5",
"typewriter-effect": "^2.21.0" "shiki": "^3.12.2",
"typewriter-effect": "^2.21.0",
"unist-util-visit": "^5.0.0"
} }
} }

3716
src/pnpm-lock.yaml generated

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

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

1
src/public/scripts Submodule

Submodule src/public/scripts added at b555dc1e10

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

@@ -3,12 +3,16 @@ import { ChevronDownIcon } from "@/components/icons";
export default function Intro() { export default function Intro() {
const scrollToNext = () => { const scrollToNext = () => {
window.scrollTo({ const nextSection = document.querySelector("section")?.nextElementSibling;
top: window.innerHeight, if (nextSection) {
behavior: "smooth" const offset = nextSection.offsetTop - (window.innerHeight - nextSection.offsetHeight) / 2; // Center the section
}); window.scrollTo({
top: offset,
behavior: "smooth"
});
}
}; };
return ( return (
<div className="w-full max-w-4xl px-4"> <div className="w-full max-w-4xl px-4">
<div className="space-y-8 md:space-y-12"> <div className="space-y-8 md:space-y-12">

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

@@ -4,6 +4,7 @@ interface Cell {
alive: boolean; alive: boolean;
next: boolean; next: boolean;
color: [number, number, number]; color: [number, number, number];
baseColor: [number, number, number]; // Original color
currentX: number; currentX: number;
currentY: number; currentY: number;
targetX: number; targetX: number;
@@ -12,8 +13,13 @@ interface Cell {
targetOpacity: number; targetOpacity: number;
scale: number; scale: number;
targetScale: number; targetScale: number;
elevation: number; // For 3D effect
targetElevation: number;
transitioning: boolean; transitioning: boolean;
transitionComplete: boolean; transitionComplete: boolean;
rippleEffect: number; // For ripple animation
rippleStartTime: number; // When ripple started
rippleDistance: number; // Distance from ripple center
} }
interface Grid { interface Grid {
@@ -24,17 +30,33 @@ interface Grid {
offsetY: number; offsetY: number;
} }
interface MousePosition {
x: number;
y: number;
isDown: boolean;
lastClickTime: number;
cellX: number;
cellY: number;
}
interface BackgroundProps { interface BackgroundProps {
layout?: 'index' | 'sidebar'; layout?: 'index' | 'sidebar';
position?: 'left' | 'right'; position?: 'left' | 'right';
} }
const CELL_SIZE = 25; const CELL_SIZE_MOBILE = 15;
const TRANSITION_SPEED = 0.1; const CELL_SIZE_DESKTOP = 25;
const SCALE_SPEED = 0.15; const TARGET_FPS = 60; // Target frame rate
const CYCLE_FRAMES = 120; 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 INITIAL_DENSITY = 0.15;
const SIDEBAR_WIDTH = 240; 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> = ({ const Background: React.FC<BackgroundProps> = ({
layout = 'index', layout = 'index',
@@ -43,8 +65,17 @@ const Background: React.FC<BackgroundProps> = ({
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const gridRef = useRef<Grid>(); const gridRef = useRef<Grid>();
const animationFrameRef = useRef<number>(); const animationFrameRef = useRef<number>();
const frameCount = useRef(0); const lastUpdateTimeRef = useRef<number>(0);
const lastCycleTimeRef = useRef<number>(0);
const resizeTimeoutRef = useRef<NodeJS.Timeout>(); 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 randomColor = (): [number, number, number] => {
const colors = [ const colors = [
@@ -58,11 +89,18 @@ const Background: React.FC<BackgroundProps> = ({
return colors[Math.floor(Math.random() * colors.length)]; 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 calculateGridDimensions = (width: number, height: number) => {
const cols = Math.floor(width / CELL_SIZE); const cellSize = getCellSize();
const rows = Math.floor(height / CELL_SIZE); const cols = Math.floor(width / cellSize);
const offsetX = Math.floor((width - (cols * CELL_SIZE)) / 2); const rows = Math.floor(height / cellSize);
const offsetY = Math.floor((height - (rows * CELL_SIZE)) / 2); const offsetX = Math.floor((width - (cols * cellSize)) / 2);
const offsetY = Math.floor((height - (rows * cellSize)) / 2);
return { cols, rows, offsetX, offsetY }; return { cols, rows, offsetX, offsetY };
}; };
@@ -70,21 +108,30 @@ const Background: React.FC<BackgroundProps> = ({
const { cols, rows, offsetX, offsetY } = calculateGridDimensions(width, height); const { cols, rows, offsetX, offsetY } = calculateGridDimensions(width, height);
const cells = Array(cols).fill(0).map((_, i) => const cells = Array(cols).fill(0).map((_, i) =>
Array(rows).fill(0).map((_, j) => ({ Array(rows).fill(0).map((_, j) => {
alive: Math.random() < INITIAL_DENSITY, const baseColor = randomColor();
next: false, return {
color: randomColor(), alive: Math.random() < INITIAL_DENSITY,
currentX: i, next: false,
currentY: j, color: [...baseColor] as [number, number, number],
targetX: i, baseColor: baseColor,
targetY: j, currentX: i,
opacity: 0, currentY: j,
targetOpacity: 0, targetX: i,
scale: 0, targetY: j,
targetScale: 0, opacity: 0,
transitioning: false, targetOpacity: 0,
transitionComplete: false 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 }; const grid = { cells, cols, rows, offsetX, offsetY };
@@ -121,7 +168,7 @@ const Background: React.FC<BackgroundProps> = ({
if (grid.cells[col][row].alive) { if (grid.cells[col][row].alive) {
neighbors.count++; neighbors.count++;
neighbors.colors.push(grid.cells[col][row].color); neighbors.colors.push(grid.cells[col][row].baseColor);
} }
} }
} }
@@ -144,65 +191,274 @@ const Background: React.FC<BackgroundProps> = ({
}; };
const computeNextState = (grid: Grid) => { 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 i = 0; i < grid.cols; i++) {
for (let j = 0; j < grid.rows; j++) { for (let j = 0; j < grid.rows; j++) {
const cell = grid.cells[i][j]; const cell = grid.cells[i][j];
const { count, colors } = countNeighbors(grid, i, j); const { count, colors } = countNeighbors(grid, i, j);
// Standard Conway's Game of Life rules
if (cell.alive) { if (cell.alive) {
cell.next = count === 2 || count === 3; cell.next = count === 2 || count === 3;
} else { } else {
cell.next = count === 3; cell.next = count === 3;
if (cell.next) { if (cell.next) {
cell.color = averageColors(colors); 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) { if (cell.alive !== cell.next && !cell.transitioning) {
cell.transitioning = true; cell.transitioning = true;
cell.transitionComplete = false; cell.transitionComplete = false;
if (!cell.next) {
cell.targetScale = 0; // Random delay for staggered animation effect
cell.targetOpacity = 0; 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 updateCellAnimations = (grid: Grid) => { const handleMouseDown = (e: MouseEvent) => {
for (let i = 0; i < grid.cols; i++) { if (!gridRef.current || !canvasRef.current) return;
for (let j = 0; j < grid.rows; j++) {
const cell = grid.cells[i][j]; 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;
cell.opacity += (cell.targetOpacity - cell.opacity) * TRANSITION_SPEED; // Spawn cell at this position if it's empty
cell.scale += (cell.targetScale - cell.scale) * SCALE_SPEED; if (cellX >= 0 && cellX < grid.cols && cellY >= 0 && cellY < grid.rows) {
const cell = grid.cells[cellX][cellY];
if (cell.transitioning) { if (!cell.alive && !cell.transitioning) {
if (!cell.next && cell.scale < 0.05) { spawnCellAtPosition(grid, cellX, cellY);
cell.alive = false;
cell.transitioning = false;
cell.transitionComplete = true;
cell.scale = 0;
cell.opacity = 0;
}
else if (cell.next && !cell.alive && !cell.transitionComplete) {
cell.alive = true;
cell.transitioning = false;
cell.transitionComplete = true;
cell.targetScale = 1;
cell.targetOpacity = 1;
}
else if (cell.next && !cell.alive && cell.transitionComplete) {
cell.transitioning = true;
cell.targetScale = 1;
cell.targetOpacity = 1;
} }
} }
} }
} }
}; };
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 setupCanvas = (canvas: HTMLCanvasElement, width: number, height: number) => {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) return; if (!ctx) return;
@@ -242,12 +498,15 @@ const Background: React.FC<BackgroundProps> = ({
const ctx = setupCanvas(canvas, displayWidth, displayHeight); const ctx = setupCanvas(canvas, displayWidth, displayHeight);
if (!ctx) return; if (!ctx) return;
frameCount.current = 0; lastUpdateTimeRef.current = 0;
lastCycleTimeRef.current = 0;
const cellSize = getCellSize();
// Only initialize new grid if one doesn't exist or dimensions changed // Only initialize new grid if one doesn't exist or dimensions changed
if (!gridRef.current || if (!gridRef.current ||
gridRef.current.cols !== Math.floor(displayWidth / CELL_SIZE) || gridRef.current.cols !== Math.floor(displayWidth / cellSize) ||
gridRef.current.rows !== Math.floor(displayHeight / CELL_SIZE)) { gridRef.current.rows !== Math.floor(displayHeight / cellSize)) {
gridRef.current = initGrid(displayWidth, displayHeight); gridRef.current = initGrid(displayWidth, displayHeight);
} }
}, 250); }, 250);
@@ -264,17 +523,58 @@ const Background: React.FC<BackgroundProps> = ({
gridRef.current = initGrid(displayWidth, displayHeight); gridRef.current = initGrid(displayWidth, displayHeight);
} }
const animate = () => { // 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; if (signal.aborted) return;
frameCount.current++; // 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) { if (gridRef.current) {
if (frameCount.current % CYCLE_FRAMES === 0) { // Check if it's time for the next life cycle
if (cycleElapsed >= CYCLE_TIME) {
computeNextState(gridRef.current); computeNextState(gridRef.current);
lastCycleTimeRef.current = currentTime;
} }
updateCellAnimations(gridRef.current); updateCellAnimations(gridRef.current, clampedDeltaTime);
} }
// Draw frame // Draw frame
@@ -283,25 +583,49 @@ const Background: React.FC<BackgroundProps> = ({
if (gridRef.current) { if (gridRef.current) {
const grid = gridRef.current; const grid = gridRef.current;
const cellSize = CELL_SIZE * 0.8; const cellSize = getCellSize();
const roundness = cellSize * 0.2; const displayCellSize = cellSize * 0.8;
const roundness = displayCellSize * 0.2;
for (let i = 0; i < grid.cols; i++) { for (let i = 0; i < grid.cols; i++) {
for (let j = 0; j < grid.rows; j++) { for (let j = 0; j < grid.rows; j++) {
const cell = grid.cells[i][j]; const cell = grid.cells[i][j];
if (cell.opacity > 0.01) { // 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; const [r, g, b] = cell.color;
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.globalAlpha = cell.opacity * 0.8;
const scaledSize = cellSize * cell.scale;
const xOffset = (cellSize - scaledSize) / 2;
const yOffset = (cellSize - scaledSize) / 2;
const x = grid.offsetX + i * CELL_SIZE + (CELL_SIZE - cellSize) / 2 + xOffset; // Base opacity
const y = grid.offsetY + j * CELL_SIZE + (CELL_SIZE - cellSize) / 2 + yOffset; 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; 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.beginPath();
ctx.moveTo(x + scaledRoundness, y); ctx.moveTo(x + scaledRoundness, y);
ctx.lineTo(x + scaledSize - scaledRoundness, y); ctx.lineTo(x + scaledSize - scaledRoundness, y);
@@ -313,6 +637,22 @@ const Background: React.FC<BackgroundProps> = ({
ctx.lineTo(x, y + scaledRoundness); ctx.lineTo(x, y + scaledRoundness);
ctx.quadraticCurveTo(x, y, x + scaledRoundness, y); ctx.quadraticCurveTo(x, y, x + scaledRoundness, y);
ctx.fill(); 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
} }
} }
} }
@@ -323,11 +663,13 @@ const Background: React.FC<BackgroundProps> = ({
animationFrameRef.current = requestAnimationFrame(animate); animationFrameRef.current = requestAnimationFrame(animate);
}; };
document.addEventListener('visibilitychange', handleVisibilityChange, { signal });
window.addEventListener('resize', handleResize, { signal }); window.addEventListener('resize', handleResize, { signal });
animate(); animate(performance.now());
return () => { return () => {
controller.abort(); controller.abort();
document.removeEventListener('visibilitychange', handleVisibilityChange);
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
if (animationFrameRef.current) { if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current); cancelAnimationFrame(animationFrameRef.current);
@@ -336,14 +678,14 @@ const Background: React.FC<BackgroundProps> = ({
clearTimeout(resizeTimeoutRef.current); clearTimeout(resizeTimeoutRef.current);
} }
}; };
}, []); // Empty dependency array since we're managing state internally }, [layout]); // Added layout as a dependency since it's used in the effect
const getContainerClasses = () => { const getContainerClasses = () => {
if (layout === 'index') { if (layout === 'index') {
return 'fixed inset-0 -z-10'; return 'fixed inset-0 -z-10';
} }
const baseClasses = 'fixed top-0 bottom-0 hidden lg:block -z-10 pointer-events-none'; const baseClasses = 'fixed top-0 bottom-0 hidden lg:block -z-10';
return position === 'left' return position === 'left'
? `${baseClasses} left-0` ? `${baseClasses} left-0`
: `${baseClasses} right-0`; : `${baseClasses} right-0`;
@@ -354,8 +696,9 @@ const Background: React.FC<BackgroundProps> = ({
<canvas <canvas
ref={canvasRef} ref={canvasRef}
className="w-full h-full bg-black" 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" /> <div className="absolute inset-0 bg-black/30 backdrop-blur-sm pointer-events-none" />
</div> </div>
); );
}; };

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

@@ -27,12 +27,7 @@ const formatDate = (dateString: string) => {
export const BlogPostList = ({ posts }: BlogPostListProps) => { export const BlogPostList = ({ posts }: BlogPostListProps) => {
return ( return (
<div className="w-full max-w-6xl mx-auto pt-24 sm:pt-24"> <div className="w-full max-w-6xl mx-auto">
<h1 className="text-2xl sm:text-3xl font-bold text-purple mb-12 text-center px-4 leading-relaxed">
Latest Thoughts <br className="sm:hidden" />
& Writings
</h1>
<ul className="space-y-6 md:space-y-10"> <ul className="space-y-6 md:space-y-10">
{posts.map((post) => ( {posts.map((post) => (
<li key={post.slug} className="group px-4 md:px-0"> <li key={post.slug} className="group px-4 md:px-0">

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

@@ -12,8 +12,8 @@ export default function Footer({ fixed = false }) {
)); ));
return ( return (
<footer className={`w-full font-bold ${fixed ? "fixed bottom-0 left-0 right-0" : ""}`}> <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"> <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} {footerLinks}
</div> </div>
</footer> </footer>

View File

@@ -80,23 +80,26 @@ export default function Header() {
</div> </div>
); );
}); });
return ( return (
<header <header
className={` className={`
fixed z-50 top-0 left-0 right-0 fixed z-50 top-0 left-0 right-0
font-bold font-bold
transition-transform duration-300 transition-transform duration-300
pointer-events-none
${visible ? "translate-y-0" : "-translate-y-full"} ${visible ? "translate-y-0" : "-translate-y-full"}
`} `}
> >
<div className={` <div className={`
w-full flex flex-row items-center justify-center w-full flex flex-row items-center justify-center
pointer-events-none
${!isIndexPage ? 'bg-black md:bg-transparent' : ''} ${!isIndexPage ? 'bg-black md:bg-transparent' : ''}
`}> `}>
<div className={` <div className={`
w-full md:w-auto flex flex-row pt-1 px-2 text-lg lg:text-3xl md:text-2xl 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 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' : ''} ${!isIndexPage ? 'bg-black md:px-20' : ''}
`}> `}>
{headerLinks} {headerLinks}

View File

@@ -72,8 +72,8 @@ export default function Hero() {
}; };
return ( return (
<div className="flex justify-center items-center min-h-screen"> <div className="flex justify-center items-center min-h-screen pointer-events-none">
<div className="text-4xl font-bold text-center"> <div className="text-4xl font-bold text-center pointer-events-none [&_a]:pointer-events-auto">
<Typewriter <Typewriter
options={typewriterOptions} options={typewriterOptions}
onInit={handleInit} onInit={handleInit}

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

@@ -3,7 +3,7 @@ title: My First Post!
description: A quick introduction description: A quick introduction
author: Timothy Pidashev author: Timothy Pidashev
tags: [greeting] tags: [greeting]
date: January 9, 2025 date: 2025-01-09
image: "/blog/my-first-post/thumbnail.png" image: "/blog/my-first-post/thumbnail.png"
imagePosition: "center 30%" imagePosition: "center 30%"
--- ---

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!

View File

@@ -7,9 +7,12 @@ export const collections = {
description: z.string(), description: z.string(),
author: z.string(), author: z.string(),
tags: z.array(z.string()), tags: z.array(z.string()),
date: z.string(), date: z.coerce.date().transform((date) => {
return new Date(date.setUTCHours(12, 0, 0, 0));
}),
image: z.string().optional(), image: z.string().optional(),
imagePosition: z.string().optional(), imagePosition: z.string().optional(),
isDraft: z.boolean().optional()
}), }),
}), }),
projects: defineCollection({ projects: defineCollection({
@@ -20,7 +23,7 @@ export const collections = {
demoUrl: z.string().url().optional(), demoUrl: z.string().url().optional(),
techStack: z.array(z.string()), techStack: z.array(z.string()),
date: z.string(), date: z.string(),
image: z.string().optional(), image: z.string().optional()
}), }),
}), })
}; };

View File

@@ -1,104 +0,0 @@
---
title: "I corebooted my T440p, here's how I did it."
author: "Timothy Pidashev"
date: "2024/06/05"
description: "This is a sample MDX file."
tags: ["coreboot", "t440p", "dgpu"]
---
```python
# discord api
import discord
from discord.ext import commands
# custom utilities
from Utilities import log
log = log.Logger("errors")
class Errors(commands.Cog):
def __init__(self, client):
self.client = client
@commands.Cog.listener()
async def on_ready(self):
await log.info("Errors cog loaded.")
@commands.Cog.listener()
async def on_command_error(self, context, error):
if isinstance(error, commands.CheckFailure):
await context.reply(
"You are not priveleged enough to use this command.",
mention_author=False
)
else:
await context.reply(
f"**Error**\n```diff\n- {error}```",
mention_author=False
)
def setup(client):
client.add_cog(Errors(client))
```
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
*Italic Text*
_Italic Text_
**Bold Text**
__Bold Text__
* Bullet List
* Item 1
* Item 2
* Subitem 1
* Subitem 2
1. Numbered List
1. Item 1
2. Item 2
- Subitem 1
- Subitem 2
[Link Text](https://example.com)
![Image Alt Text](https://example.com/image.jpg)
> Blockquote
>
> Lorem ipsum dolor sit amet, consectetur adipiscing elit.
`Inline Code`
| Table Header 1 | Table Header 2 |
|----------------|----------------|
| Table Row 1 | Table Row 1 |
| Table Row 2 | Table Row 2 |
<sup>Superscript Text</sup>
<sub>Subscript Text</sub>
<mark>Highlighted Text</mark>
<ins>Underlined Text</ins>
<del>Strikethrough Text</del>
<abbr title="Abbreviation">Abbreviation</abbr>

7
src/src/env.d.ts vendored
View File

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

View File

@@ -1,6 +1,7 @@
--- ---
import "@/style/globals.css"; import "@/style/globals.css";
import { ClientRouter } from "astro:transitions"; import { ClientRouter } from "astro:transitions";
import Header from "@/components/header"; import Header from "@/components/header";
import Footer from "@/components/footer"; import Footer from "@/components/footer";
import Background from "@/components/background"; import Background from "@/components/background";
@@ -31,12 +32,11 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
<meta name="description" content={description} /> <meta name="description" content={description} />
<!-- Also used in OpenGraph for social media sharing --> <!-- Also used in OpenGraph for social media sharing -->
<meta property="og:description" content={description} /> <meta property="og:description" content={description} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/jpeg" href="/me.jpeg" />
<ClientRouter <ClientRouter
defaultTransition={false} defaultTransition={false}
handleFocus={false} handleFocus={false}
/> />
<style> <style>
::view-transition-new(:root) { ::view-transition-new(:root) {
animation: none; animation: none;
@@ -45,24 +45,26 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
::view-transition-old(:root) { ::view-transition-old(:root) {
animation: 90ms ease-out both fade-out; animation: 90ms ease-out both fade-out;
} }
@keyframes fade-out { @keyframes fade-out {
from { opacity: 1; } from { opacity: 1; }
to { opacity: 0; } to { opacity: 0; }
} }
</style> </style>
</head> </head>
<body class="bg-background text-foreground"> <body class="bg-background text-foreground min-h-screen flex flex-col">
<Header client:load /> <Header client:load />
<main> <main class="flex-1 flex flex-col">
<div class="max-w-5xl mx-auto pt-12 px-4 py-8"> <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 /> <Background layout="content" position="right" client:only="react" transition:persist />
<slot /> <div>
<slot />
</div>
<Background layout="content" position="left" client:only="react" transition:persist /> <Background layout="content" position="left" client:only="react" transition:persist />
</div> </div>
</main> </main>
<Footer client:load transition:persist /> <div class="mt-auto">
<Footer client:load transition:persist />
</div>
<script> <script>
document.addEventListener("astro:after-navigation", () => { document.addEventListener("astro:after-navigation", () => {
window.scrollTo(0, 0); window.scrollTo(0, 0);

View File

@@ -35,7 +35,7 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
<meta name="description" content={description} /> <meta name="description" content={description} />
<!-- Also used in OpenGraph for social media sharing --> <!-- Also used in OpenGraph for social media sharing -->
<meta property="og:description" content={description} /> <meta property="og:description" content={description} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/jpeg" href="/me.jpeg" />
<link rel="sitemap" href="/sitemap-index.xml" /> <link rel="sitemap" href="/sitemap-index.xml" />
<ClientRouter /> <ClientRouter />
</head> </head>

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

@@ -2,12 +2,12 @@ import { type Article, type Person, type WebSite, type WithContext } from "schem
import type { CollectionEntry } from 'astro:content'; import type { CollectionEntry } from 'astro:content';
export const blogWebsite: WithContext<WebSite> = { export const blogWebsite: WithContext<WebSite> = {
'@context': 'https://schema.org', "@context": "https://schema.org",
'@type': 'WebSite', "@type": "WebSite",
url: `${import.meta.env.SITE}/blog/`, url: `${import.meta.env.SITE}/blog/`,
name: 'Dzmitry Kozhukh blog', name: "Timothy Pidsashev - Blog",
description: 'Frontend insights', description: "Timothy Pidsashev's blog",
inLanguage: 'en_US', inLanguage: "en_US",
}; };
export const mainWebsite: WithContext<WebSite> = { export const mainWebsite: WithContext<WebSite> = {

View File

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

View File

@@ -2,6 +2,8 @@
import "@/style/globals.css" import "@/style/globals.css"
import ContentLayout from "@/layouts/content.astro"; import ContentLayout from "@/layouts/content.astro";
import Intro from "@/components/about/intro"; 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 Timeline from "@/components/about/timeline";
import CurrentFocus from "@/components/about/current-focus"; import CurrentFocus from "@/components/about/current-focus";
import OutsideCoding from "@/components/about/outside-coding"; import OutsideCoding from "@/components/about/outside-coding";
@@ -14,6 +16,14 @@ import OutsideCoding from "@/components/about/outside-coding";
<section class="h-screen flex items-center justify-center"> <section class="h-screen flex items-center justify-center">
<Intro client:load /> <Intro client:load />
</section> </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"> <section class="flex items-center justify-center py-16">
<Timeline client:load /> <Timeline client:load />
@@ -27,4 +37,4 @@ import OutsideCoding from "@/components/about/outside-coding";
<OutsideCoding client:load /> <OutsideCoding client:load />
</section> </section>
</div> </div>
</MainLayout> </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

@@ -1,24 +1,37 @@
--- ---
import { CollectionEntry, getCollection } from "astro:content"; import { getCollection } from "astro:content";
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import ContentLayout from "@/layouts/content.astro"; import ContentLayout from "@/layouts/content.astro";
import { getArticleSchema } from "@/lib/structuredData"; import { getArticleSchema } from "@/lib/structuredData";
import { blogWebsite } from "@/lib/structuredData"; import { blogWebsite } from "@/lib/structuredData";
import { Comments } from "@/components/blog/comments";
interface Props { // This is a dynamic route in SSR mode
post: CollectionEntry<"blog">; 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"
});
} }
export async function getStaticPaths() {
const posts = await getCollection("blog"); // Dynamically render the content
return posts.map((post) => ({
params: { slug: post.slug },
props: post,
}));
}
const post = Astro.props;
const { Content } = await post.render(); 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 articleStructuredData = getArticleSchema(post);
const breadcrumbsStructuredData = { const breadcrumbsStructuredData = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "BreadcrumbList", "@type": "BreadcrumbList",
@@ -37,16 +50,20 @@ const breadcrumbsStructuredData = {
}, },
], ],
}; };
const jsonLd = { const jsonLd = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@graph": [articleStructuredData, breadcrumbsStructuredData, blogWebsite], "@graph": [articleStructuredData, breadcrumbsStructuredData, blogWebsite],
}; };
--- ---
<ContentLayout> <ContentLayout
title={`${post.data.title} | Timothy Pidashev`}
description={post.data.description}
>
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} /> <script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
<div class="relative max-w-8xl mx-auto"> <div class="relative max-w-8xl mx-auto">
<article class="prose prose-lg mx-auto max-w-4xl"> <article class="prose prose-invert prose-lg mx-auto max-w-4xl">
{post.data.image && ( {post.data.image && (
<div class="-mx-4 sm:mx-0 mb-8"> <div class="-mx-4 sm:mx-0 mb-8">
<Image <Image
@@ -61,16 +78,29 @@ const jsonLd = {
)} )}
<h1 class="text-3xl pt-4">{post.data.title}</h1> <h1 class="text-3xl pt-4">{post.data.title}</h1>
<p class="lg:text-2xl sm:text-lg">{post.data.description}</p> <p class="lg:text-2xl sm:text-lg">{post.data.description}</p>
<p class="text-lg">{post.data.author} | {post.data.date}</p> <div class="mt-4 md:mt-6">
<div class="flex flex-wrap gap-2 pb-4"> <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) => ( {post.data.tags.map((tag) => (
<span class="text-sm px-2 py-1 bg-muted rounded-full"> <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} #{tag}
</span> </span>
))} ))}
</div> </div>
<hr class="bg-orange" /> <div class="prose prose-invert prose-lg max-w-none">
<Content /> <Content />
</div>
</article> </article>
<Comments client:idle />
</div> </div>
</ContentLayout> </ContentLayout>

View File

@@ -1,19 +1,29 @@
--- ---
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import ContentLayout from "@/layouts/content.astro"; import ContentLayout from "@/layouts/content.astro";
import { BlogHeader } from "@/components/blog/header";
import { BlogPostList } from "@/components/blog/post-list"; import { BlogPostList } from "@/components/blog/post-list";
const posts = (await getCollection("blog", ({ data }) => { const posts = (await getCollection("blog", ({ data }) => {
return data.isDraft !== true; return data.isDraft !== true;
})).sort( })).sort((a, b) => {
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf() 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 <ContentLayout
title="Blog | Timothy Pidashev" title="Blog | Timothy Pidashev"
description="My experiences and technical insights into software development and the ever-evolving world of programming." description="My experiences and technical insights into software development and the ever-evolving world of programming."
> >
<BlogHeader client:load />
<BlogPostList posts={posts} client:load /> <BlogPostList posts={posts} client:load />
</ContentLayout> </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>

View File

@@ -1,4 +1,6 @@
--- ---
export const prerender = false;
import "@/style/globals.css" import "@/style/globals.css"
import IndexLayout from "@/layouts/index.astro"; import IndexLayout from "@/layouts/index.astro";
@@ -7,7 +9,7 @@ import Hero from "@/components/hero";
<IndexLayout <IndexLayout
title="Timothy Pidashev" title="Timothy Pidashev"
description="Turning coffee into code since 2018." 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 /> <Hero client:load />
</IndexLayout> </IndexLayout>

View File

@@ -1,6 +1,10 @@
--- ---
export const prerender = true;
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import ContentLayout from "@/layouts/content.astro"; import ContentLayout from "@/layouts/content.astro";
import { Comments } from "@/components/blog/comments";
export async function getStaticPaths() { export async function getStaticPaths() {
const projects = await getCollection("projects"); const projects = await getCollection("projects");
@@ -56,8 +60,9 @@ const { Content } = await project.render();
</div> </div>
</header> </header>
<div class="prose prose-invert max-w-none"> <div class="prose prose-invert prose-lg max-w-none">
<Content /> <Content />
</div> </div>
</article> </article>
<Comments client:idle />
</ContentLayout> </ContentLayout>

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

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,9 @@ module.exports = {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"], content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
theme: { theme: {
extend: { extend: {
fontFamily: {
"comic-code": ["Comic Code", "monospace"],
},
colors: { colors: {
background: "#000000", background: "#000000",
foreground: "#ebdbb2", foreground: "#ebdbb2",
@@ -38,10 +41,15 @@ module.exports = {
"draw-line": { "draw-line": {
"0%": { "stroke-dashoffset": "100" }, "0%": { "stroke-dashoffset": "100" },
"100%": { "stroke-dashoffset": "0" } "100%": { "stroke-dashoffset": "0" }
},
"fade-in": {
"0%": { opacity: "0" },
"100%": { opacity: "1" }
} }
}, },
animation: { animation: {
"draw-line": "draw-line 0.6s ease-out forwards" "draw-line": "draw-line 0.6s ease-out forwards",
"fade-in": "fade-in 0.3s ease-in-out forwards"
}, },
typography: (theme) => ({ typography: (theme) => ({
DEFAULT: { DEFAULT: {
@@ -88,6 +96,56 @@ module.exports = {
transition: "all 0.2s ease-in-out", 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 // Bold
strong: { strong: {
color: theme("colors.orange.bright"), color: theme("colors.orange.bright"),
@@ -115,44 +173,6 @@ module.exports = {
}, },
}, },
// Code
code: {
color: theme("colors.purple.bright"),
backgroundColor: "#282828", // A dark gray that works with black
padding: "0.2em 0.4em",
borderRadius: "0.25rem",
fontWeight: "400",
"&::before": {
content: "\"\"",
},
"&::after": {
content: "\"\"",
},
},
// Inline code
"code::before": {
content: "\"\"",
},
"code::after": {
content: "\"\"",
},
// Pre
pre: {
backgroundColor: "#282828",
color: theme("colors.foreground"),
code: {
backgroundColor: "transparent",
padding: "0",
color: "inherit",
fontSize: "inherit",
fontWeight: "inherit",
"&::before": { content: "none" },
"&::after": { content: "none" },
},
},
// Horizontal rules // Horizontal rules
hr: { hr: {
borderColor: theme("colors.foreground"), borderColor: theme("colors.foreground"),
@@ -186,6 +206,13 @@ module.exports = {
}, },
}, },
}, },
lg: {
css: {
"pre code": {
fontSize: "1rem",
},
},
},
}), }),
}, },
}, },