mirror of
https://github.com/timmypidashev/web.git
synced 2026-04-14 19:13:51 +00:00
Compare commits
63 Commits
v2.0.0
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
805a1d6a5c | ||
|
95081b8b77
|
|||
|
40b6359d8f
|
|||
|
d61080722d
|
|||
|
5117218a1a
|
|||
|
f355373ba1
|
|||
|
384cb82efb
|
|||
|
7ff6f6542b
|
|||
|
9ad08dc85d
|
|||
|
12631dbd42
|
|||
|
1758dc3153
|
|||
|
9496030d41
|
|||
|
30f264a6bb
|
|||
|
7992fcbd49
|
|||
|
60a9fb0339
|
|||
|
6711de5eb6
|
|||
|
f2b4660300
|
|||
|
97608e983c
|
|||
|
ce812e8466
|
|||
|
d44988b39c
|
|||
|
d885ea4e6b
|
|||
|
6cfa4c5b7d
|
|||
|
f2e85dc6d8
|
|||
|
fce17d397e
|
|||
|
7cc954ae07
|
|||
|
c6aa014d29
|
|||
|
a9cbbb7e8e
|
|||
|
788eb84488
|
|||
|
4fc5a07249
|
|||
|
aca5d53bd1
|
|||
|
f1af80afaf
|
|||
|
257000e81d
|
|||
|
8b30228c4a
|
|||
|
99e1cd5639
|
|||
|
7446d8296a
|
|||
|
6b424ae8e4
|
|||
|
04489a53d1
|
|||
|
b40134833b
|
|||
|
e2bf036919
|
|||
|
7443947131
|
|||
|
0589ff9c7c
|
|||
|
0c2e7f505d
|
|||
|
cfbe43ab8b
|
|||
|
b5120b60df
|
|||
|
b6b98023da
|
|||
|
37c63db863
|
|||
|
61cca45350
|
|||
|
4b37d29a43
|
|||
|
d4f51b121e
|
|||
|
2e088c5c9f
|
|||
|
6ef97bb5f7
|
|||
|
bc4ddb7eae
|
|||
|
d69d3a0249
|
|||
|
ee3918f428
|
|||
|
c9ab7a37b9
|
|||
|
935d2a9077
|
|||
|
|
3c067b6c49
|
||
|
|
8bb28cffa6
|
||
|
|
a24fea8f3b
|
||
|
|
8e32f21462
|
||
|
|
de871e775e
|
||
|
|
c89318ddd8
|
||
|
|
b14fd5d7e7
|
@@ -1,5 +1,5 @@
|
|||||||
timmypidashev.local {
|
timmypidashev.local {
|
||||||
tls internal
|
tls internal
|
||||||
|
|
||||||
reverse_proxy web:4321
|
reverse_proxy timmypidashev.dev:4321
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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"]
|
||||||
@@ -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
3
.gitignore
vendored
@@ -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
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "src/public/scripts"]
|
||||||
|
path = src/public/scripts
|
||||||
|
url = https://github.com/timmypidashev/scripts
|
||||||
150
Makefile
150
Makefile
@@ -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:
|
||||||
@@ -165,21 +115,6 @@ bump:
|
|||||||
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
|
|
||||||
|
|
||||||
%:
|
|
||||||
@:
|
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
|
|
||||||
|

|
||||||
<img src=".github/preview.jpeg" title="Preview"/>
|
<img src=".github/preview.jpeg" title="Preview"/>
|
||||||
|
|||||||
@@ -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
0
compose.local.yml
Normal 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
|
||||||
|
|
||||||
release.timmypidashev.dev:
|
watchtower:
|
||||||
container_name: timmypidashev
|
container_name: updates
|
||||||
image: ghcr.io/timmypidashev/timmypidashev.dev:release
|
image: containrrr/watchtower
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- $HOME/.docker/config.json:/config.json
|
||||||
|
command: --interval 120 --cleanup --label-enable
|
||||||
|
|
||||||
|
timmypidashev.dev:
|
||||||
|
container_name: timmypidashev.dev
|
||||||
|
image: ghcr.io/timmypidashev/timmypidashev.dev:latest
|
||||||
networks:
|
networks:
|
||||||
- proxy_network
|
- proxy_network
|
||||||
|
|
||||||
|
|||||||
@@ -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
90
pnpm-lock.yaml
generated
@@ -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
|
|
||||||
@@ -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,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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
3716
src/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
3
src/pnpm-workspace.yaml
Normal file
3
src/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
|
- sharp
|
||||||
BIN
src/public/blog/breaking-the-chromebook-cage/thumbnail.png
Normal file
BIN
src/public/blog/breaking-the-chromebook-cage/thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
BIN
src/public/blog/thinkpad-t440p-coreboot-guide/thumbnail.png
Normal file
BIN
src/public/blog/thinkpad-t440p-coreboot-guide/thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
14
src/public/favicon.svg
Normal file
14
src/public/favicon.svg
Normal 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
65
src/public/pgp.asc
Normal 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
1
src/public/scripts
Submodule
Submodule src/public/scripts added at b555dc1e10
56
src/src/components/404/glitched-text.tsx
Normal file
56
src/src/components/404/glitched-text.tsx
Normal 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;
|
||||||
@@ -3,10 +3,14 @@ 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 (
|
||||||
|
|||||||
150
src/src/components/about/stats-activity.tsx
Normal file
150
src/src/components/about/stats-activity.tsx
Normal 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;
|
||||||
213
src/src/components/about/stats-alltime.tsx
Normal file
213
src/src/components/about/stats-alltime.tsx
Normal 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;
|
||||||
175
src/src/components/about/stats-detailed.tsx
Normal file
175
src/src/components/about/stats-detailed.tsx
Normal 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;
|
||||||
@@ -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];
|
|
||||||
|
|
||||||
cell.opacity += (cell.targetOpacity - cell.opacity) * TRANSITION_SPEED;
|
const canvas = canvasRef.current;
|
||||||
cell.scale += (cell.targetScale - cell.scale) * SCALE_SPEED;
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const mouseX = e.clientX - rect.left;
|
||||||
|
const mouseY = e.clientY - rect.top;
|
||||||
|
const cellSize = getCellSize();
|
||||||
|
|
||||||
if (cell.transitioning) {
|
mouseRef.current.isDown = true;
|
||||||
if (!cell.next && cell.scale < 0.05) {
|
mouseRef.current.lastClickTime = Date.now();
|
||||||
cell.alive = false;
|
|
||||||
cell.transitioning = false;
|
const grid = gridRef.current;
|
||||||
cell.transitionComplete = true;
|
|
||||||
cell.scale = 0;
|
// Calculate which cell was clicked
|
||||||
cell.opacity = 0;
|
const cellX = Math.floor((mouseX - grid.offsetX) / cellSize);
|
||||||
}
|
const cellY = Math.floor((mouseY - grid.offsetY) / cellSize);
|
||||||
else if (cell.next && !cell.alive && !cell.transitionComplete) {
|
|
||||||
cell.alive = true;
|
if (cellX >= 0 && cellX < grid.cols && cellY >= 0 && cellY < grid.rows) {
|
||||||
cell.transitioning = false;
|
mouseRef.current.cellX = cellX;
|
||||||
cell.transitionComplete = true;
|
mouseRef.current.cellY = cellY;
|
||||||
cell.targetScale = 1;
|
|
||||||
cell.targetOpacity = 1;
|
const cell = grid.cells[cellX][cellY];
|
||||||
}
|
|
||||||
else if (cell.next && !cell.alive && cell.transitionComplete) {
|
if (cell.alive) {
|
||||||
cell.transitioning = true;
|
// Create ripple effect from existing cell
|
||||||
cell.targetScale = 1;
|
createRippleEffect(grid, cellX, cellY);
|
||||||
cell.targetOpacity = 1;
|
} else {
|
||||||
|
// Spawn new cell at empty position
|
||||||
|
spawnCellAtPosition(grid, cellX, cellY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!canvasRef.current || !gridRef.current) return;
|
||||||
|
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const cellSize = getCellSize();
|
||||||
|
|
||||||
|
mouseRef.current.x = e.clientX - rect.left;
|
||||||
|
mouseRef.current.y = e.clientY - rect.top;
|
||||||
|
|
||||||
|
// Drawing functionality - place cells while dragging
|
||||||
|
if (mouseRef.current.isDown) {
|
||||||
|
const grid = gridRef.current;
|
||||||
|
|
||||||
|
// Calculate which cell the mouse is over
|
||||||
|
const cellX = Math.floor((mouseRef.current.x - grid.offsetX) / cellSize);
|
||||||
|
const cellY = Math.floor((mouseRef.current.y - grid.offsetY) / cellSize);
|
||||||
|
|
||||||
|
// Only draw if we're on a new cell
|
||||||
|
if (cellX !== mouseRef.current.cellX || cellY !== mouseRef.current.cellY) {
|
||||||
|
mouseRef.current.cellX = cellX;
|
||||||
|
mouseRef.current.cellY = cellY;
|
||||||
|
|
||||||
|
// Spawn cell at this position if it's empty
|
||||||
|
if (cellX >= 0 && cellX < grid.cols && cellY >= 0 && cellY < grid.rows) {
|
||||||
|
const cell = grid.cells[cellX][cellY];
|
||||||
|
if (!cell.alive && !cell.transitioning) {
|
||||||
|
spawnCellAtPosition(grid, cellX, cellY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
mouseRef.current.isDown = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
mouseRef.current.isDown = false;
|
||||||
|
mouseRef.current.x = -1000;
|
||||||
|
mouseRef.current.y = -1000;
|
||||||
|
};
|
||||||
|
|
||||||
const setupCanvas = (canvas: HTMLCanvasElement, width: number, height: number) => {
|
const 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;
|
// Base opacity
|
||||||
const xOffset = (cellSize - scaledSize) / 2;
|
ctx.globalAlpha = cell.opacity * 0.9;
|
||||||
const yOffset = (cellSize - scaledSize) / 2;
|
|
||||||
|
|
||||||
const x = grid.offsetX + i * CELL_SIZE + (CELL_SIZE - cellSize) / 2 + xOffset;
|
const scaledSize = displayCellSize * cell.scale;
|
||||||
const y = grid.offsetY + j * CELL_SIZE + (CELL_SIZE - cellSize) / 2 + yOffset;
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
34
src/src/components/blog/comments.tsx
Normal file
34
src/src/components/blog/comments.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
38
src/src/components/blog/header.tsx
Normal file
38
src/src/components/blog/header.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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">
|
||||||
|
|||||||
114
src/src/components/blog/tag-list.tsx
Normal file
114
src/src/components/blog/tag-list.tsx
Normal 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;
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -87,16 +87,19 @@ export default function Header() {
|
|||||||
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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
264
src/src/components/mdx/command.tsx
Normal file
264
src/src/components/mdx/command.tsx
Normal 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 };
|
||||||
68
src/src/components/mdx/video.tsx
Normal file
68
src/src/components/mdx/video.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
src/src/content/blog/breaking-the-chromebook-cage.mdx
Normal file
9
src/src/content/blog/breaking-the-chromebook-cage.mdx
Normal 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
|
||||||
|
---
|
||||||
@@ -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;
|
||||||
@@ -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%"
|
||||||
---
|
---
|
||||||
|
|||||||
308
src/src/content/blog/thinkpad-t440p-coreboot-guide.mdx
Normal file
308
src/src/content/blog/thinkpad-t440p-coreboot-guide.mdx
Normal 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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Assembling the SPI Flasher
|
||||||
|
|
||||||
|
Place the SPI flasher ribbon cable into the correct slot and make sure its the 3.3v variant
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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!
|
||||||
@@ -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()
|
||||||
}),
|
}),
|
||||||
}),
|
})
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
> 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
7
src/src/env.d.ts
vendored
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
70
src/src/layouts/resource.astro
Normal file
70
src/src/layouts/resource.astro
Normal 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>
|
||||||
@@ -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> = {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -15,6 +17,14 @@ import OutsideCoding from "@/components/about/outside-coding";
|
|||||||
<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 />
|
||||||
</section>
|
</section>
|
||||||
@@ -27,4 +37,4 @@ import OutsideCoding from "@/components/about/outside-coding";
|
|||||||
<OutsideCoding client:load />
|
<OutsideCoding client:load />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</MainLayout>
|
</ContentLayout>
|
||||||
|
|||||||
32
src/src/pages/api/wakatime/activity.ts
Normal file
32
src/src/pages/api/wakatime/activity.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/src/pages/api/wakatime/alltime.ts
Normal file
22
src/src/pages/api/wakatime/alltime.ts
Normal 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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/src/pages/api/wakatime/detailed.ts
Normal file
33
src/src/pages/api/wakatime/detailed.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,35 @@
|
|||||||
---
|
---
|
||||||
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 = {
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
28
src/src/pages/blog/tags/index.astro
Normal file
28
src/src/pages/blog/tags/index.astro
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
29
src/src/pages/rss.ts
Normal 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
348
src/src/style/comments.css
Normal 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
@@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user