Compare commits
223 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
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
|
||
|
|
3d07b2b714
|
||
|
|
3880e2ab7b
|
||
|
|
72ee036c08
|
||
|
|
d083ec090c
|
||
|
|
05844b2446
|
||
|
|
844c8d49d4
|
||
|
|
de1411b01a
|
||
|
|
bfda37ee0b
|
||
|
|
2777d14007
|
||
|
|
f37688f2d1
|
||
|
|
acad2cc0ca
|
||
|
|
6466602276
|
||
|
|
d657951158
|
||
|
|
6a35f48097 | ||
|
|
6805fe57d7
|
||
|
|
133c0944bc
|
||
|
|
02290388da
|
||
|
|
b2455cb1e2
|
||
|
|
5f06079b5b
|
||
|
|
5681e4b1ad
|
||
|
|
2519182e86
|
||
|
|
42495f2316
|
||
|
|
035944887b
|
||
|
|
21772ae6cb
|
||
|
|
efe0b9713f
|
||
|
|
f5211cc799
|
||
|
|
b618f6e807
|
||
|
|
d5cbe73c2d
|
||
|
|
d96a27e612
|
||
|
|
b3da439864
|
||
|
|
76ecd1a392 | ||
|
|
deeef2f8a0
|
||
|
|
26877cf18a
|
||
|
|
dfd5b15ed9
|
||
|
|
22c9391c37
|
||
|
|
b90108e70f
|
||
|
|
0ff2116794
|
||
|
|
2fcdf6272e
|
||
|
|
9d7414e0c9
|
||
|
|
93d9b3e014
|
||
|
|
c3bc253182
|
||
|
|
aaf29f45a0
|
||
|
|
502b1a93e1
|
||
|
|
efa4be2fd9
|
||
|
|
6bd0616d54
|
||
|
|
73e6e2c354
|
||
|
|
f96629a6b4
|
||
|
|
4c97f4f52d
|
||
|
|
ef9522cf3e
|
||
|
|
189774def8
|
||
|
|
6fd37f854d
|
||
|
|
55f1ff96d4
|
||
|
|
4b5de24ef6
|
||
|
|
dc4cf3fbbc
|
||
|
|
0a08ef4b0c
|
||
|
|
3e3bc486e2
|
||
|
|
df411e42ce
|
||
|
|
bc6e7a0278
|
||
|
|
1a72c07e82
|
||
|
|
afa9013ff0
|
||
|
|
b459052b44
|
||
|
|
21d1fc9f8c
|
||
|
|
068d2a5c7a
|
||
|
|
e20fc0d197
|
||
|
|
bded192500
|
||
|
|
d813123d95
|
||
|
|
b626ce3abb
|
||
|
|
242e4d8a7f
|
||
|
|
c16f92e576
|
||
|
|
8b33094cef | ||
|
|
6b09180743
|
||
|
|
4d205596fc
|
||
|
|
4f0959f433
|
||
|
|
8bf39116e9
|
||
|
|
2879ab0563
|
||
|
|
3ba0a94793
|
||
|
|
a2555d1940
|
||
|
|
55391f7ee5
|
||
|
|
7c3bd72fa0
|
||
|
|
1cf61969af
|
||
|
|
8d23faf7ad
|
||
|
|
4136bf2622
|
||
|
|
2c9c0b08d0
|
||
|
|
9720e6faf4
|
||
|
|
2acb40c90c
|
||
|
|
b04cd10453
|
||
|
|
b37f35350b
|
||
|
|
773085b2e0
|
||
|
|
6c2b82086a
|
||
|
|
53a3832fc4
|
||
|
|
62fdbf1fc1
|
||
|
|
fbcc0385a4
|
||
|
|
d69b327bb6
|
||
|
|
93a2bf9caa
|
||
|
|
7b0d09f2c9
|
||
|
|
385b237906
|
||
|
|
70c7d03576
|
||
|
|
9f4c069f7f
|
||
|
|
36112fe04e
|
||
|
|
9422553c9c
|
||
|
|
ed1dc91bd7
|
||
|
|
998841e1e7
|
||
|
|
e96e679a35
|
||
|
|
73402aec0b
|
||
|
|
65a46162d7
|
||
|
|
6a6804f43a
|
||
|
|
2fd5c7ec36
|
||
|
|
b87d34410a
|
||
|
|
bceec10c3f
|
||
|
|
f23ddf6e5c
|
||
|
|
c6c5f1c067
|
||
|
|
09365d828a
|
||
|
|
d1684a1472
|
||
|
|
ec1a5103c3
|
||
|
|
e7f70b4c02
|
||
|
|
8b6a760d91
|
||
|
|
56f799266b
|
||
|
|
893c59585e
|
||
|
|
1c255069e7
|
||
|
|
47bbbb01fa
|
||
|
|
71b28b6059
|
||
|
|
9483382799
|
||
|
|
42215fcad4
|
||
|
|
d3c260a0fa
|
||
|
|
cb2ac819e0
|
||
|
|
fde907781a
|
||
|
|
3b7fe795e8
|
||
|
|
1fee9df3a1
|
||
|
|
4f93517f9e
|
||
|
|
9204d1c569
|
||
|
|
8f57e420b5
|
||
|
|
0e534d670d
|
||
|
|
6799028dff
|
||
|
|
219d891c23
|
||
|
|
7820806c26
|
||
|
|
d666c62af1
|
||
|
|
99d518f475
|
||
|
|
4117802d8c
|
||
|
|
a827dba86f
|
||
|
|
34dfde16d5
|
||
|
|
bf43cbe9fd
|
||
|
|
7561484d25
|
||
|
|
34aecb70d5
|
||
|
|
df1e0b5e00
|
||
|
|
d7fdb4866a | ||
|
|
6d7b58d2a9 | ||
|
|
eeee364c93 | ||
|
|
32c0b774a2 | ||
|
|
259d92dfe1 | ||
|
|
974bb7956e | ||
|
|
3c54f2470e | ||
|
|
447098b7ef | ||
|
|
30e6d32b09 | ||
|
|
bba2c6fbb9 | ||
|
|
41f9cd8fd3 | ||
|
|
1723aae6fc | ||
|
|
60b9e62881 | ||
|
|
f134a3c3d5 | ||
|
|
3c371383d5 | ||
|
|
074f13683a | ||
|
|
c9385a50b8 | ||
|
|
76b2b20e2f | ||
|
|
e207049644 | ||
|
|
64718e30e5 | ||
|
|
b26b405eca | ||
|
|
78c96c0aff | ||
|
|
73aecb076f | ||
|
|
9114ee14af | ||
|
|
69f75857be | ||
|
|
8a2f8edb30 | ||
|
|
8cb801f51e | ||
|
|
c66ee24dce | ||
|
|
441b26912f | ||
|
|
4433a1523d | ||
|
|
56934cf46a | ||
|
|
a2cd51deae | ||
|
|
638e90e858 | ||
|
|
ace2695d7f | ||
|
|
dad9d4fd1b | ||
|
|
f39d8aa690 | ||
|
|
98a7a589cc | ||
|
|
ad75d90d9f | ||
|
|
8a9a1335f3 | ||
|
|
bbe25f26e7 | ||
|
|
3c979be288 | ||
|
|
fbbca11032 | ||
|
|
f4e630ee76 | ||
|
|
af2a4c104a | ||
|
|
8e86979394 | ||
|
|
7b875a85c1 | ||
|
|
1f820e4008 | ||
|
|
54361ac79b | ||
|
|
e9ac5400b4 |
5
.caddy/Caddyfile.local
Normal file
@@ -0,0 +1,5 @@
|
||||
timmypidashev.local {
|
||||
tls internal
|
||||
|
||||
reverse_proxy timmypidashev.dev:4321
|
||||
}
|
||||
5
.caddy/Caddyfile.release
Normal file
@@ -0,0 +1,5 @@
|
||||
timmypidashev.dev {
|
||||
tls pidashev.tim@gmail.com
|
||||
|
||||
reverse_proxy timmypidashev.dev:3000
|
||||
}
|
||||
27
.docker/Dockerfile.local
Normal file
@@ -0,0 +1,27 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
ARG CONTAINER_WEB_VERSION
|
||||
ARG ENVIRONMENT
|
||||
ARG BUILD_DATE
|
||||
ARG GIT_COMMIT
|
||||
|
||||
RUN set -eux \
|
||||
& apk add \
|
||||
--no-cache \
|
||||
nodejs \
|
||||
curl
|
||||
|
||||
RUN curl -L https://unpkg.com/@pnpm/self-installer | node
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN echo "PUBLIC_VERSION=${CONTAINER_WEB_VERSION}" > /app/.env && \
|
||||
echo "PUBLIC_ENVIRONMENT=${ENVIRONMENT}" >> /app/.env && \
|
||||
echo "PUBLIC_BUILD_DATE=${BUILD_DATE}" >> /app/.env && \
|
||||
echo "PUBLIC_GIT_COMMIT=${GIT_COMMIT}" >> /app/.env
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["pnpm", "run", "dev"]
|
||||
48
.docker/Dockerfile.release
Normal file
@@ -0,0 +1,48 @@
|
||||
# Stage 1: Build and install dependencies
|
||||
FROM node:22-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Install necessary dependencies, including pnpm
|
||||
RUN set -eux \
|
||||
&& apk add --no-cache nodejs curl \
|
||||
&& npm install -g pnpm
|
||||
|
||||
# Copy package files first (for better caching)
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Now copy the rest of your source code
|
||||
COPY . .
|
||||
|
||||
# Set build arguments
|
||||
ARG CONTAINER_WEB_VERSION
|
||||
ARG ENVIRONMENT
|
||||
ARG BUILD_DATE
|
||||
ARG GIT_COMMIT
|
||||
|
||||
# Create .env file with build-time environment variables
|
||||
RUN echo "PUBLIC_VERSION=${CONTAINER_WEB_VERSION}" > /app/.env && \
|
||||
echo "PUBLIC_ENVIRONMENT=${ENVIRONMENT}" >> /app/.env && \
|
||||
echo "PUBLIC_BUILD_DATE=${BUILD_DATE}" >> /app/.env && \
|
||||
echo "PUBLIC_GIT_COMMIT=${GIT_COMMIT}" >> /app/.env
|
||||
|
||||
# Build the project
|
||||
RUN pnpm run build
|
||||
|
||||
# Stage 2: Serve static files
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
|
||||
# Install serve
|
||||
RUN npm install -g http-server
|
||||
|
||||
# Copy built files
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Expose port 3000
|
||||
EXPOSE 3000
|
||||
|
||||
# Deployment command
|
||||
CMD ["http-server", "dist", "-a", "127.0.0.1", "-p", "3000"]
|
||||
BIN
.github/preview.jpeg
vendored
Normal file
|
After Width: | Height: | Size: 77 KiB |
83
.github/scripts/deploy_release.sh
vendored
Executable file
@@ -0,0 +1,83 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Set variables
|
||||
BRANCH_NAME="$1"
|
||||
COMMIT_HASH="$2"
|
||||
GHCR_USERNAME="$3"
|
||||
GHCR_TOKEN="$4"
|
||||
DEPLOY_TYPE="$5"
|
||||
REPO_OWNER="$6"
|
||||
COMPOSE_FILE="$7"
|
||||
CADDYFILE="$8"
|
||||
MAKEFILE="$9"
|
||||
|
||||
# Echo out variable names and their content on single lines
|
||||
echo "BRANCH_NAME: $BRANCH_NAME"
|
||||
echo "COMMIT_HASH: $COMMIT_HASH"
|
||||
echo "GHCR_USERNAME: $GHCR_USERNAME"
|
||||
echo "DEPLOY_TYPE: $DEPLOY_TYPE"
|
||||
echo "REPO_OWNER: $REPO_OWNER"
|
||||
echo "COMPOSE_FILE: $COMPOSE_FILE"
|
||||
echo "CADDYFILE: $CADDYFILE"
|
||||
echo "MAKEFILE: $MAKEFILE"
|
||||
|
||||
# Set the staging directory
|
||||
STAGING_DIR="/root/deployments/.staging-${COMMIT_HASH}"
|
||||
|
||||
# Set the tmux session name for release
|
||||
TMUX_SESSION="deployment-release"
|
||||
|
||||
# Function to cleanup existing release deployment
|
||||
cleanup_release_deployment() {
|
||||
echo "Cleaning up existing release deployment..."
|
||||
# Stop and remove all release containers
|
||||
docker-compose -f "/root/deployments/release/docker-compose.yml" down -v 2>/dev/null
|
||||
# Remove release images
|
||||
docker rmi $(docker images "ghcr.io/$REPO_OWNER/*:release" -q) 2>/dev/null
|
||||
# Kill release tmux session if it exists
|
||||
tmux kill-session -t "$TMUX_SESSION" 2>/dev/null
|
||||
# Remove release deployment directory
|
||||
rm -rf /root/deployments/release
|
||||
}
|
||||
|
||||
# Function to create deployment directory
|
||||
create_deployment_directory() {
|
||||
echo "Creating deployment directory..."
|
||||
mkdir -p /root/deployments/release
|
||||
}
|
||||
|
||||
# Function to pull Docker images
|
||||
pull_docker_images() {
|
||||
echo "Pulling Docker images..."
|
||||
docker pull ghcr.io/$REPO_OWNER/web:release
|
||||
}
|
||||
|
||||
# Function to copy and prepare files
|
||||
copy_and_prepare_files() {
|
||||
echo "Copying and preparing files..."
|
||||
# Copy files preserving names and locations
|
||||
install -D "$STAGING_DIR/$COMPOSE_FILE" "/root/deployments/release/$COMPOSE_FILE"
|
||||
install -D "$STAGING_DIR/$CADDYFILE" "/root/deployments/release/$CADDYFILE"
|
||||
install -D "$STAGING_DIR/$MAKEFILE" "/root/deployments/release/$MAKEFILE"
|
||||
# Replace {$COMMIT_HASH} with $COMMIT_HASH in $CADDYFILE
|
||||
sed -i "s/{\$COMMIT_HASH}/$COMMIT_HASH/g" "/root/deployments/release/$CADDYFILE"
|
||||
# Replace {commit_hash} with $COMMIT_HASH in $COMPOSE_FILE
|
||||
sed -i "s/{commit_hash}/$COMMIT_HASH/g" "/root/deployments/release/$COMPOSE_FILE"
|
||||
}
|
||||
|
||||
# Function to start the deployment
|
||||
start_deployment() {
|
||||
echo "Starting deployment..."
|
||||
# Create new tmux session with specific name
|
||||
tmux new-session -d -s "$TMUX_SESSION"
|
||||
tmux send-keys -t "$TMUX_SESSION" "cd /root/deployments/release && make run release" Enter
|
||||
}
|
||||
|
||||
# Main execution
|
||||
cleanup_release_deployment
|
||||
create_deployment_directory
|
||||
copy_and_prepare_files
|
||||
cd "/root/deployments/release"
|
||||
pull_docker_images
|
||||
start_deployment
|
||||
echo "Release build $COMMIT_HASH deployed successfully!"
|
||||
44
.gitignore
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# Env
|
||||
.env
|
||||
|
||||
# astro
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
/.pnp
|
||||
.pnpm-store
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
dist/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "src/public/scripts"]
|
||||
path = src/public/scripts
|
||||
url = https://github.com/timmypidashev/scripts
|
||||
19
LICENSE
Normal file
@@ -0,0 +1,19 @@
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
186
Makefile
Normal file
@@ -0,0 +1,186 @@
|
||||
PROJECT_NAME := "timmypidashev.dev"
|
||||
PROJECT_AUTHORS := "Timothy Pidashev (timmypidashev) <pidashev.tim@gmail.com>"
|
||||
PROJECT_VERSION := "v1.0.2"
|
||||
PROJECT_LICENSE := "MIT"
|
||||
PROJECT_SOURCES := "https://github.com/timmypidashev/web"
|
||||
PROJECT_REGISTRY := "ghcr.io/timmypidashev"
|
||||
PROJECT_ORGANIZATION := "org.opencontainers"
|
||||
|
||||
CONTAINER_WEB_NAME := "timmypidashev.dev"
|
||||
CONTAINER_WEB_VERSION := "v2.1.1"
|
||||
CONTAINER_WEB_LOCATION := "src/"
|
||||
CONTAINER_WEB_DESCRIPTION := "My portfolio website!"
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
.PHONY: watch run build push prune bump exec
|
||||
.SILENT: watch run build push prune bump exec
|
||||
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " run - Runs the docker compose file with the specified environment (local or release)"
|
||||
@echo " build - Builds the specified docker image with the appropriate environment"
|
||||
@echo " push - Pushes the built docker image to the registry"
|
||||
@echo " prune - Removes all built and cached docker images and containers"
|
||||
@echo " bump - Bumps the project and container versions"
|
||||
|
||||
run:
|
||||
# Arguments:
|
||||
# [environment]: 'local' or 'release'
|
||||
#
|
||||
# Explanation:
|
||||
# * Runs the docker compose file with the specified environment(compose.local.yml, or compose.release.yml)
|
||||
# * Passes all generated arguments to the compose file.
|
||||
|
||||
# Make sure we have been given proper arguments.
|
||||
@if [ "$(word 2,$(MAKECMDGOALS))" = "local" ]; then \
|
||||
echo "Running in local environment"; \
|
||||
elif [ "$(word 2,$(MAKECMDGOALS))" = "release" ]; then \
|
||||
echo "Running in release environment"; \
|
||||
else \
|
||||
echo "Invalid usage. Please use 'make run local' or 'make run release'"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
docker compose -f compose.$(word 2,$(MAKECMDGOALS)).yml up --remove-orphans
|
||||
|
||||
|
||||
build:
|
||||
# Arguments
|
||||
# [container]: Build context(which container to build ['all' to build every container defined])
|
||||
# [environment]: 'local' or 'release'
|
||||
#
|
||||
# Explanation:
|
||||
# * Builds the specified docker image with the appropriate environment.
|
||||
# * Passes all generated arguments to docker build-kit.
|
||||
|
||||
# Extract container and environment inputted.
|
||||
$(eval INPUT_TARGET := $(word 2,$(MAKECMDGOALS)))
|
||||
$(eval INPUT_CONTAINER := $(firstword $(subst :, ,$(INPUT_TARGET))))
|
||||
$(eval INPUT_ENVIRONMENT := $(lastword $(subst :, ,$(INPUT_TARGET))))
|
||||
|
||||
# Call function container_build either through a for loop for each container
|
||||
# if all is called, or singularly to build the container.
|
||||
$(if $(filter $(strip $(INPUT_CONTAINER)),all), \
|
||||
$(foreach container,$(containers),$(call container_build,$(container) $(INPUT_ENVIRONMENT))), \
|
||||
$(call container_build,$(INPUT_CONTAINER) $(INPUT_ENVIRONMENT)))
|
||||
|
||||
push:
|
||||
# Arguments
|
||||
# [container]: Push context(which container to push to the registry)
|
||||
# [version]: Container version to push.
|
||||
#
|
||||
# Explanation:
|
||||
# * Pushes the specified container version to the registry defined in the user configuration.
|
||||
|
||||
# Extract container and version inputted.
|
||||
$(eval INPUT_TARGET := $(word 2,$(MAKECMDGOALS)))
|
||||
$(eval INPUT_CONTAINER := $(firstword $(subst :, ,$(INPUT_TARGET))))
|
||||
$(eval INPUT_VERSION := $(lastword $(subst :, ,$(INPUT_TARGET))))
|
||||
|
||||
# Push the specified container version to the registry.
|
||||
# NOTE: docker will complain if the container tag is invalid, no need to validate here.
|
||||
@docker push $(PROJECT_REGISTRY)/$(INPUT_CONTAINER):$(INPUT_VERSION)
|
||||
|
||||
prune:
|
||||
# Removes all built and cached docker images and containers.
|
||||
|
||||
bump:
|
||||
# Arguments
|
||||
# [container]: Bump context(which container version to bump)
|
||||
# [semantic_type]: Semantic type context(major, minor, patch)
|
||||
#
|
||||
# Explanation:
|
||||
# * Bumps the specified container version within the makefile config and the container's package.json.
|
||||
# * Bumps the global project version in the makefile, and creates a new git tag with said version.
|
||||
|
||||
# Extract container and semantic_type inputted.
|
||||
$(eval INPUT_TARGET := $(word 2,$(MAKECMDGOALS)))
|
||||
$(eval INPUT_CONTAINER := $(firstword $(subst :, ,$(INPUT_TARGET))))
|
||||
$(eval INPUT_SEMANTIC_TYPE := $(lastword $(subst :, ,$(INPUT_TARGET))))
|
||||
|
||||
# Extract old container and project versions.
|
||||
$(eval OLD_CONTAINER_VERSION := $(subst v,,$(CONTAINER_$(shell echo $(INPUT_CONTAINER) | tr a-z A-Z)_VERSION)))
|
||||
$(eval OLD_PROJECT_VERSION := $(subst v,,$(PROJECT_VERSION)))
|
||||
|
||||
# Pull docker semver becsause the normal command doesn't seem to work; also we don't need to worry about dependencies.
|
||||
docker pull usvc/semver:latest
|
||||
|
||||
# Bump npm package.json file for selected container
|
||||
cd $(call container_location,$(INPUT_CONTAINER)) && npm version $(shell docker run usvc/semver:latest bump $(INPUT_SEMANTIC_TYPE) $(OLD_CONTAINER_VERSION))
|
||||
|
||||
# Bump the git tag to match the new global version
|
||||
git tag v$(shell docker run usvc/semver:latest bump $(INPUT_SEMANTIC_TYPE) $(OLD_PROJECT_VERSION))
|
||||
|
||||
# Bump the container version and global version in the Makefile
|
||||
perl -pi -e 's/^PROJECT_VERSION\s*:=\s*\K.*/"v$(shell docker run usvc/semver:latest bump $(INPUT_SEMANTIC_TYPE) $(OLD_PROJECT_VERSION))"/ if /^PROJECT_VERSION\s*:=/' Makefile;
|
||||
perl -pi -e 's/^CONTAINER_$(shell echo $(INPUT_CONTAINER) | tr a-z A-Z)_VERSION\s*:=\s*\K.*/"v$(shell docker run usvc/semver:latest bump $(INPUT_SEMANTIC_TYPE) $(OLD_CONTAINER_VERSION))"/ if /^CONTAINER_$(shell echo $(INPUT_CONTAINER) | tr a-z A-Z)_VERSION\s*:=/' Makefile;
|
||||
|
||||
# This function generates Docker build arguments based on variables defined in the Makefile.
|
||||
# It extracts variable assignments, removes whitespace, and formats them as build arguments.
|
||||
# Additionally, it appends any custom shell generated arguments defined below.
|
||||
define args
|
||||
$(shell \
|
||||
grep -E '^[[:alnum:]_]+[[:space:]]*[:?]?[[:space:]]*=' $(MAKEFILE_LIST) | \
|
||||
awk 'BEGIN {FS = ":="} { \
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$$/, "", $$2); \
|
||||
gsub(/^/, "\x27", $$2); \
|
||||
gsub(/$$/, "\x27", $$2); \
|
||||
gsub(/[[:space:]]+/, "", $$1); \
|
||||
gsub(":", "", $$1); \
|
||||
printf "--build-arg %s=%s ", $$1, $$2 \
|
||||
}') \
|
||||
--build-arg BUILD_DATE='"$(shell date)"' \
|
||||
--build-arg GIT_COMMIT='"$(shell git rev-parse HEAD)"'
|
||||
endef
|
||||
|
||||
# This function generates labels based on variables defined in the Makefile.
|
||||
# It extracts only the selected container variables and is used to echo this information
|
||||
# to the docker buildx engine in the command line.
|
||||
define labels
|
||||
--label $(PROJECT_ORGANIZATION).image.title=$(CONTAINER_$(1)_NAME) \
|
||||
--label $(PROJECT_ORGANIZATION).image.description=$(CONTAINER_$(1)_DESCRIPTION) \
|
||||
--label $(PROJECT_ORGANIZATION).image.authors=$(PROJECT_AUTHORS) \
|
||||
--label $(PROJECT_ORGANIZATION).image.url=$(PROJECT_SOURCES) \
|
||||
--label $(PROJECT_ORGANIZATION).image.source=$(PROJECT_SOURCES)/$(CONTAINER_$(1)_LOCATION)
|
||||
endef
|
||||
|
||||
# This function returns a list of container names defined in the Makefile.
|
||||
# In order for this function to return a container, it needs to have this format: CONTAINER_%_NAME!
|
||||
define containers
|
||||
$(strip $(filter-out $(_NL),$(foreach var,$(.VARIABLES),$(if $(filter CONTAINER_%_NAME,$(var)),$(strip $($(var)))))))
|
||||
endef
|
||||
|
||||
define container_build
|
||||
$(eval CONTAINER := $(word 1,$1))
|
||||
$(eval ENVIRONMENT := $(word 2,$1))
|
||||
$(eval ARGS := $(shell echo $(args)))
|
||||
$(eval VERSION := $(strip $(call container_version,$(CONTAINER))))
|
||||
$(eval TAG := $(PROJECT_NAME):$(ENVIRONMENT))
|
||||
|
||||
@echo "Building container: $(CONTAINER)"
|
||||
@echo "Environment: $(ENVIRONMENT)"
|
||||
@echo "Version: $(VERSION)"
|
||||
|
||||
@if [ "$(strip $(ENVIRONMENT))" != "local" ] && [ "$(strip $(ENVIRONMENT))" != "release" ]; then \
|
||||
echo "Invalid environment. Please specify 'local' or 'release'"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
$(if $(filter $(strip $(ENVIRONMENT)),release), \
|
||||
$(eval TAG := $(PROJECT_REGISTRY)/$(PROJECT_NAME):$(VERSION)), \
|
||||
)
|
||||
|
||||
docker buildx build --load -t $(TAG) -f .docker/Dockerfile.$(ENVIRONMENT) ./$(strip $(subst $(SPACE),,$(call container_location,$(CONTAINER))))/. $(ARGS) $(call labels,$(shell echo $(CONTAINER_NAME) | tr '[:lower:]' '[:upper:]')) --no-cache
|
||||
endef
|
||||
|
||||
define container_location
|
||||
$(strip $(eval CONTAINER_NAME := $(shell echo $(1) | tr '[:lower:]' '[:upper:]'))) \
|
||||
$(CONTAINER_$(CONTAINER_NAME)_LOCATION)
|
||||
endef
|
||||
|
||||
define container_version
|
||||
$(strip $(eval CONTAINER_NAME := $(shell echo $(1) | tr '[:lower:]' '[:upper:]'))) \
|
||||
$(if $(CONTAINER_$(CONTAINER_NAME)_VERSION), \
|
||||
$(shell echo $(strip $(strip $(CONTAINER_$(CONTAINER_NAME)_VERSION))) | tr -d '[:space:]'), \
|
||||
$(error Version data for container $(1) not found))
|
||||
endef
|
||||
@@ -1,2 +1 @@
|
||||
# Portfolio
|
||||
My portfolio website!
|
||||
<img src=".github/preview.jpeg" title="Preview"/>
|
||||
|
||||
58
blog.html
@@ -1,58 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Blog</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="Timothy Pidashev" content="A 17-year-old on an epic journey to become a software developer!" />
|
||||
<meta name="keywords" content="Timothy Pidashev, timmypidashev, timmy, programming, github" />
|
||||
<link rel="shortcut icon" type="image/png" href="images/elements/png/timmy.png"/>
|
||||
<link rel="stylesheet" href="css/styles.css">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/aos@next/dist/aos.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Overlay -->
|
||||
<div id="myNav" class="overlay">
|
||||
<a href="javascript:void(0)" class="closebtn" onclick="closeNav()">×</a>
|
||||
<div class="overlay-content">
|
||||
<a href="download.html">Download YT</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navbar -->
|
||||
<div class="header">
|
||||
<a href="#default" class="logo" data-aos="zoom-in">Timmy</a>
|
||||
<div class="header-right">
|
||||
<a href="index.html" data-aos="zoom-in">About</a>
|
||||
<a class="active" href="blog.html" data-aos="zoom-in" data-aos="zoom-in">Blog</a>
|
||||
<a href="projects.html" data-aos="zoom-in">Projects</a>
|
||||
<span style="font-size:30px;cursor:pointer" onclick="openNav()">☰ </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<!-- Section -->
|
||||
<section class="centered">
|
||||
<div>
|
||||
<h1 class="hero__header" data-aos="flip-down">Work In Progress</h1>
|
||||
</div>
|
||||
<img src="images/elements/png/line_short.png" alt="line" class="divider__line" data-aos="flip-left">
|
||||
</section>
|
||||
|
||||
<!-- JS -->
|
||||
<script src="js/closeNav.js"></script>
|
||||
<script src="js/openNav.js"></script>
|
||||
<script src="https://unpkg.com/aos@next/dist/aos.js"></script>
|
||||
<script>AOS.init(
|
||||
{
|
||||
once: false,
|
||||
mirror: true,
|
||||
anchorPlacement: 'top-bottom',
|
||||
offset: 0,
|
||||
duration: 800
|
||||
});</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
0
compose.local.yml
Normal file
32
compose.release.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
services:
|
||||
caddy:
|
||||
container_name: caddy
|
||||
image: caddy:latest
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- ./.caddy/Caddyfile.release:/etc/caddy/Caddyfile:rw
|
||||
networks:
|
||||
- proxy_network
|
||||
depends_on:
|
||||
- timmypidashev.dev
|
||||
|
||||
watchtower:
|
||||
container_name: updates
|
||||
image: containrrr/watchtower
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- $HOME/.docker/config.json:/config.json
|
||||
command: --interval 120 --cleanup --label-enable
|
||||
|
||||
timmypidashev.dev:
|
||||
container_name: timmypidashev
|
||||
image: ghcr.io/timmypidashev/timmypidashev.dev:latest
|
||||
networks:
|
||||
- proxy_network
|
||||
|
||||
networks:
|
||||
proxy_network:
|
||||
name: proxy_network
|
||||
external: true
|
||||
188
css/styles.css
@@ -1,188 +0,0 @@
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Navbar */
|
||||
.header {
|
||||
overflow: hidden;
|
||||
background-color: #FB93DA;
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
.header a {
|
||||
float: left;
|
||||
color: #404082;
|
||||
text-shadow: 3px 2px 2px rgba(199, 130, 59, 0.15);
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
text-decoration: none;
|
||||
font-size: 18px;
|
||||
line-height: 25px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header a.logo {
|
||||
font-size: 25px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.header a:hover {
|
||||
background-color: #D26BB9;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.header a.active {
|
||||
background-color: #404082;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* Overlay */
|
||||
.overlay {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgb(0,0,0);
|
||||
background-color: rgba(0,0,0, 0.9);
|
||||
overflow-x: hidden;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.overlay-content {
|
||||
position: relative;
|
||||
top: 25%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.overlay a {
|
||||
padding: 8px;
|
||||
text-decoration: none;
|
||||
font-size: 36px;
|
||||
color: #818181;
|
||||
display: block;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.overlay a:hover, .overlay a:focus {
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
.overlay .closebtn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 45px;
|
||||
font-size: 60px;
|
||||
}
|
||||
|
||||
/* Style */
|
||||
html {
|
||||
background-color: #FB93DA;
|
||||
color: #404082;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 1vh;
|
||||
text-shadow: 3px 2px 2px rgba(199, 130, 59, 0.15);
|
||||
-ms-overflow-style: none; /* Hide scrollbar for Internet Explorer and Edge */
|
||||
scrollbar-width: none; /* Hide scrollbar for Firefox */
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.html::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#end__of__page {
|
||||
margin-bottom: -30vh;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pink__colored {
|
||||
color: #D26BB9;
|
||||
}
|
||||
|
||||
.orange__highlight {
|
||||
background-color: #D26BB9;
|
||||
border-radius: 20px;
|
||||
line-height: 1.5;
|
||||
padding-right: 0.9vw;
|
||||
padding-left: 0.9vw;
|
||||
font-weight: 520;
|
||||
text-shadow: none;
|
||||
box-shadow: 3px 2px 2px rgba(199, 130, 59, 0.15);
|
||||
}
|
||||
|
||||
.hero__header {
|
||||
font-size: 6vw;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.divider__line {
|
||||
height: 4vw;
|
||||
}
|
||||
|
||||
.about__header {
|
||||
font-size: 3vw;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.logo {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.logo:hover {
|
||||
transform: scale(1.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.logo__image {
|
||||
margin-left: 2vw;
|
||||
margin-right: 2vw;
|
||||
width: 10vw;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1024px) {
|
||||
.hero__header {
|
||||
font-size: 12vw;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.about__header {
|
||||
font-size: 5vw;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.divider__line {
|
||||
height: 8vw;
|
||||
}
|
||||
|
||||
.logo__image {
|
||||
margin-left: 2vw;
|
||||
margin-right: 2vw;
|
||||
width: 14vw;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Download YT</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="Timothy Pidashev" content="A 17-year-old on an epic journey to become a software developer!" />
|
||||
<meta name="keywords" content="Timothy Pidashev, timmypidashev, timmy, programming, github" />
|
||||
<link rel="shortcut icon" type="image/png" href="images/elements/png/timmy.png"/>
|
||||
<link rel="stylesheet" href="css/styles.css">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/aos@next/dist/aos.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Overlay -->
|
||||
<div id="myNav" class="overlay">
|
||||
<a href="javascript:void(0)" class="closebtn" onclick="closeNav()">×</a>
|
||||
<div class="overlay-content">
|
||||
<a href="download.html">Download YT</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navbar -->
|
||||
<div class="header">
|
||||
<a href="#default" class="logo" data-aos="zoom-in">Timmy</a>
|
||||
<div class="header-right">
|
||||
<a href="index.html" data-aos="zoom-in">About</a>
|
||||
<a href="blog.html" data-aos="zoom-in" data-aos="zoom-in">Blog</a>
|
||||
<a href="projects.html" data-aos="zoom-in">Projects</a>
|
||||
<span style="font-size:30px;cursor:pointer" onclick="openNav()">☰ </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<!-- Section -->
|
||||
<section class="centered">
|
||||
<div>
|
||||
<h1 class="hero__header" data-aos="flip-down">Work In Progress</h1>
|
||||
</div>
|
||||
<img src="images/elements/png/line_short.png" alt="line" class="divider__line" data-aos="flip-left">
|
||||
</section>
|
||||
|
||||
<!-- JS -->
|
||||
<script src="js/closeNav.js"></script>
|
||||
<script src="js/openNav.js"></script>
|
||||
<script src="https://unpkg.com/aos@next/dist/aos.js"></script>
|
||||
<script>AOS.init(
|
||||
{
|
||||
once: false,
|
||||
mirror: true,
|
||||
anchorPlacement: 'top-bottom',
|
||||
offset: 0,
|
||||
duration: 800
|
||||
});</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 653 B |
|
Before Width: | Height: | Size: 630 B |
|
Before Width: | Height: | Size: 515 B |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 36 KiB |
108
index.html
@@ -1,108 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Timothy Pidashev</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="Timothy Pidashev" content="A 17-year-old on an epic journey to become a software developer!" />
|
||||
<meta name="keywords" content="Timothy Pidashev, timmypidashev, timmy, programming, github" />
|
||||
<link rel="shortcut icon" type="image/png" href="images/elements/png/timmy.png"/>
|
||||
<link rel="stylesheet" href="css/styles.css">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/aos@next/dist/aos.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Overlay -->
|
||||
<div id="myNav" class="overlay">
|
||||
<a href="javascript:void(0)" class="closebtn" onclick="closeNav()">×</a>
|
||||
<div class="overlay-content">
|
||||
<a href="download.html">Download YT</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navbar -->
|
||||
<div class="header">
|
||||
<a href="#default" class="logo" data-aos="zoom-in">Timmy</a>
|
||||
<div class="header-right">
|
||||
<a class="active" href="index.html" data-aos="zoom-in">About</a>
|
||||
<a href="blog.html" data-aos="zoom-in" data-aos="zoom-in">Blog</a>
|
||||
<a href="projects.html" data-aos="zoom-in">Projects</a>
|
||||
<span style="font-size:30px;cursor:pointer" onclick="openNav()">☰ </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<section class="centered">
|
||||
<div>
|
||||
<h1 class="hero__header pink__colored" data-aos="fade-right">Hello, Im</h1>
|
||||
<h1 class="hero__header" data-aos="flip-down">Timothy</h1>
|
||||
<h1 class="hero__header" data-aos="fade-left">Pidashev</h1>
|
||||
</div>
|
||||
<img src="images/elements/png/line_short.png" alt="line" class="divider__line" data-aos="flip-left">
|
||||
</section>
|
||||
<section class="centered">
|
||||
<div>
|
||||
<h1 class="about__header" data-aos="zoom-out">
|
||||
I'm a 17-year-old on an
|
||||
<span class="orange__highlight">epic journey</span>
|
||||
</h1>
|
||||
<h1 class="about__header" data-aos="flip-down">
|
||||
to become a
|
||||
<span class="orange__highlight">software developer!</span>
|
||||
</h1>
|
||||
</div>
|
||||
<img src="images/elements/png/line_short.png" alt="line" class="divider__line" data-aos="flip-left">
|
||||
</section>
|
||||
<div id="end__of__page" class="centered">
|
||||
<div class="row">
|
||||
<div class="logo">
|
||||
<a href="https://www.youtube.com/channel/UCEpaCDz-wZ21kR8nA8xDSzg" target="_blank">
|
||||
<img src="images/elements/png/youtube.png" alt="youtube logo" data-aos="fade-left" class="logo__image">
|
||||
</a>
|
||||
</div>
|
||||
<div class="logo">
|
||||
<a href="https://twitter.com/Timothy89184676" target="_blank">
|
||||
<img src="images/elements/png/twitter.png" alt="twitter logo" data-aos="zoom-in" class="logo__image">
|
||||
</a>
|
||||
</div>
|
||||
<div class="logo">
|
||||
<a href="https://discord.gg/34RqygKbtX" target="_blank">
|
||||
<img src="images/elements/png/discord.png" alt="discord logo" data-aos="fade-right" class="logo__image">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="logo">
|
||||
<a href="https://timmyverybored.itch.io/" target="_blank">
|
||||
<img src="images/elements/png/itch.png" alt="itch logo" data-aos="fade-right" class="logo__image">
|
||||
</a>
|
||||
</div>
|
||||
<div class="logo">
|
||||
<a href="https://github.com/timmypidashev" target="_blank">
|
||||
<img src="images/elements/png/github.png" alt="github logo" data-aos="zoom-out" class="logo__image">
|
||||
</a>
|
||||
</div>
|
||||
<div class="logo">
|
||||
<a href="mailto: pidashev.tim@gmail.com" target="_blank">
|
||||
<img src="images/elements/png/gmail.png" alt="gmail logo" data-aos="fade-left" class="logo__image">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JS -->
|
||||
<script src="js/closeNav.js"></script>
|
||||
<script src="js/openNav.js"></script>
|
||||
<script src="https://unpkg.com/aos@next/dist/aos.js"></script>
|
||||
<script>AOS.init(
|
||||
{
|
||||
once: false,
|
||||
mirror: true,
|
||||
anchorPlacement: 'top-bottom',
|
||||
offset: 0,
|
||||
duration: 800
|
||||
});</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +0,0 @@
|
||||
function closeNav() {
|
||||
document.getElementById("myNav").style.width = "0%";
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
function openNav() {
|
||||
document.getElementById("myNav").style.width = "100%";
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Projects</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="Timothy Pidashev" content="A 17-year-old on an epic journey to become a software developer!" />
|
||||
<meta name="keywords" content="Timothy Pidashev, timmypidashev, timmy, programming, github" />
|
||||
<link rel="shortcut icon" type="image/png" href="images/elements/png/timmy.png"/>
|
||||
<link rel="stylesheet" href="css/styles.css">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://unpkg.com/aos@next/dist/aos.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Overlay -->
|
||||
<div id="myNav" class="overlay">
|
||||
<a href="javascript:void(0)" class="closebtn" onclick="closeNav()">×</a>
|
||||
<div class="overlay-content">
|
||||
<a href="download.html">Download YT</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navbar -->
|
||||
<div class="header">
|
||||
<a href="#default" class="logo" data-aos="zoom-in">Timmy</a>
|
||||
<div class="header-right">
|
||||
<a href="index.html" data-aos="zoom-in">About</a>
|
||||
<a href="blog.html" data-aos="zoom-in" data-aos="zoom-in">Blog</a>
|
||||
<a class="active" href="projects.html" data-aos="zoom-in">Projects</a>
|
||||
<span style="font-size:30px;cursor:pointer" onclick="openNav()">☰ </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<!-- Section -->
|
||||
<section class="centered">
|
||||
<div>
|
||||
<h1 class="hero__header" data-aos="flip-down">Work In Progress</h1>
|
||||
</div>
|
||||
<img src="images/elements/png/line_short.png" alt="line" class="divider__line" data-aos="flip-left">
|
||||
</section>
|
||||
|
||||
<!-- JS -->
|
||||
<script src="js/closeNav.js"></script>
|
||||
<script src="js/openNav.js"></script>
|
||||
<script src="https://unpkg.com/aos@next/dist/aos.js"></script>
|
||||
<script>AOS.init(
|
||||
{
|
||||
once: false,
|
||||
mirror: true,
|
||||
anchorPlacement: 'top-bottom',
|
||||
offset: 0,
|
||||
duration: 800
|
||||
});</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
6
src/.stackblitzrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"startCommand": "npm start",
|
||||
"env": {
|
||||
"ENABLE_CJS_IMPORTS": true
|
||||
}
|
||||
}
|
||||
11
src/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Astro with Tailwind
|
||||
|
||||
```
|
||||
npm init astro -- --template with-tailwindcss
|
||||
```
|
||||
|
||||
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/with-tailwindcss)
|
||||
|
||||
Astro comes with [Tailwind](https://tailwindcss.com) support out of the box. This example showcases how to style your Astro project with Tailwind.
|
||||
|
||||
For complete setup instructions, please see our [Styling Guide](https://docs.astro.build/guides/styling#-tailwind).
|
||||
184
src/astro.config.mjs
Normal file
@@ -0,0 +1,184 @@
|
||||
import { defineConfig } from "astro/config";
|
||||
import node from "@astrojs/node";
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
import react from "@astrojs/react";
|
||||
import mdx from "@astrojs/mdx";
|
||||
import rehypePrettyCode from "rehype-pretty-code";
|
||||
import rehypeSlug from "rehype-slug";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
output: "server",
|
||||
adapter: node({
|
||||
mode: "standalone",
|
||||
}),
|
||||
site: "https://timmypidashev.dev",
|
||||
build: {
|
||||
// Enable build-time optimizations
|
||||
inlineStylesheets: "auto",
|
||||
// Split large components into smaller chunks
|
||||
splitComponents: true,
|
||||
},
|
||||
integrations: [
|
||||
tailwind(),
|
||||
react(),
|
||||
mdx({
|
||||
syntaxHighlight: false,
|
||||
rehypePlugins: [
|
||||
/**
|
||||
* Adds ids to headings
|
||||
*/
|
||||
rehypeSlug,
|
||||
[
|
||||
/**
|
||||
* Enhances code blocks with syntax highlighting, line numbers,
|
||||
* titles, and allows highlighting specific lines and words
|
||||
*/
|
||||
|
||||
rehypePrettyCode,
|
||||
{
|
||||
theme: {
|
||||
"name": "Darkbox",
|
||||
"type": "dark",
|
||||
"colors": {
|
||||
"editor.background": "#000000",
|
||||
"editor.foreground": "#ebdbb2"
|
||||
},
|
||||
"tokenColors": [
|
||||
{
|
||||
"scope": ["comment", "punctuation.definition.comment"],
|
||||
"settings": {
|
||||
"foreground": "#928374",
|
||||
"fontStyle": "italic"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": ["constant", "variable.other.constant"],
|
||||
"settings": {
|
||||
"foreground": "#d3869b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "variable",
|
||||
"settings": {
|
||||
"foreground": "#ebdbb2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": ["keyword", "storage.type", "storage.modifier"],
|
||||
"settings": {
|
||||
"foreground": "#fb4934"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": ["string", "punctuation.definition.string"],
|
||||
"settings": {
|
||||
"foreground": "#b8bb26"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": ["entity.name.function", "support.function"],
|
||||
"settings": {
|
||||
"foreground": "#b8bb26"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "entity.name.type",
|
||||
"settings": {
|
||||
"foreground": "#fabd2f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": ["entity.name.tag", "punctuation.definition.tag"],
|
||||
"settings": {
|
||||
"foreground": "#83a598"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": ["entity.other.attribute-name"],
|
||||
"settings": {
|
||||
"foreground": "#8ec07c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": ["punctuation", "meta.brace"],
|
||||
"settings": {
|
||||
"foreground": "#ebdbb2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "markup.inline.raw",
|
||||
"settings": {
|
||||
"foreground": "#fe8019"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": ["markup.heading"],
|
||||
"settings": {
|
||||
"foreground": "#b8bb26",
|
||||
"fontStyle": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": ["markup.bold"],
|
||||
"settings": {
|
||||
"foreground": "#fe8019",
|
||||
"fontStyle": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": ["markup.italic"],
|
||||
"settings": {
|
||||
"foreground": "#fe8019",
|
||||
"fontStyle": "italic"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": ["markup.list"],
|
||||
"settings": {
|
||||
"foreground": "#83a598"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": ["markup.quote"],
|
||||
"settings": {
|
||||
"foreground": "#928374",
|
||||
"fontStyle": "italic"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": ["markup.link"],
|
||||
"settings": {
|
||||
"foreground": "#8ec07c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "support.class",
|
||||
"settings": {
|
||||
"foreground": "#fabd2f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scope": "number",
|
||||
"settings": {
|
||||
"foreground": "#d3869b"
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
keepBackground: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
sitemap({
|
||||
filter: (page) => {
|
||||
return !page.includes("/drafts/") && !page.includes("/private/");
|
||||
},
|
||||
changefreq: "weekly",
|
||||
priority: 0.7,
|
||||
lastmod: new Date(),
|
||||
}),
|
||||
],
|
||||
});
|
||||
39
src/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "src",
|
||||
"version": "2.1.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev --host",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/react": "^4.2.4",
|
||||
"@astrojs/tailwind": "^6.0.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@types/react": "^18.3.20",
|
||||
"@types/react-dom": "^18.3.6",
|
||||
"astro": "^5.7.4",
|
||||
"tailwindcss": "^3.4.17"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^4.2.4",
|
||||
"@astrojs/node": "^9.2.0",
|
||||
"@astrojs/rss": "^4.0.11",
|
||||
"@astrojs/sitemap": "^3.3.0",
|
||||
"@giscus/react": "^3.1.0",
|
||||
"@pilcrowjs/object-parser": "^0.0.4",
|
||||
"@react-hook/intersection-observer": "^3.1.2",
|
||||
"arctic": "^3.6.0",
|
||||
"lucide-react": "^0.468.0",
|
||||
"marked": "^15.0.8",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-responsive": "^10.0.1",
|
||||
"reading-time": "^1.5.0",
|
||||
"rehype-pretty-code": "^0.14.1",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"schema-dts": "^1.1.5",
|
||||
"typewriter-effect": "^2.21.0"
|
||||
}
|
||||
}
|
||||
5297
src/pnpm-lock.yaml
generated
Normal file
3
src/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
- sharp
|
||||
BIN
src/public/blog/my-first-post/thumbnail.png
Normal file
|
After Width: | Height: | Size: 6.8 MiB |
BIN
src/public/blog/thinkpad-t440p-coreboot-guide/thumbnail.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
src/public/blog/thinkpad-t440p-modification-guide/thumbnail.png
Normal file
|
After Width: | Height: | Size: 235 KiB |
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 |
BIN
src/public/me.jpeg
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src/public/og-image.jpg
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
src/public/projects/darkbox/thumbnail.jpeg
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/public/projects/fhccenter/thumbnail.jpeg
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
src/public/projects/iridescent/thumbnail.jpeg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
src/public/projects/reviveauto/thumbnail.jpeg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/public/projects/web/thumbnail.jpeg
Normal file
|
After Width: | Height: | Size: 32 KiB |
1
src/public/scripts
Submodule
11
src/sandbox.config.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"infiniteLoopProtection": true,
|
||||
"hardReloadOnChange": false,
|
||||
"view": "browser",
|
||||
"template": "node",
|
||||
"container": {
|
||||
"port": 3000,
|
||||
"startScript": "start",
|
||||
"node": "14"
|
||||
}
|
||||
}
|
||||
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;
|
||||
134
src/src/components/about/current-focus.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
import { Code2, BookOpen, RocketIcon, Compass } from 'lucide-react';
|
||||
|
||||
export default function CurrentFocus() {
|
||||
const recentProjects = [
|
||||
{
|
||||
title: "Darkbox",
|
||||
description: "My gruvbox theme, with a pure black background",
|
||||
href: "/projects/darkbox",
|
||||
tech: ["Neovim", "Lua"]
|
||||
},
|
||||
{
|
||||
title: "Revive Auto Parts",
|
||||
description: "A car parts listing site built for a client",
|
||||
href: "/projects/reviveauto",
|
||||
tech: ["Tanstack", "React Query", "Fastapi"]
|
||||
},
|
||||
{
|
||||
title: "Fhccenter",
|
||||
description: "Website made for a private school",
|
||||
href: "/projects/fhccenter",
|
||||
tech: ["Nextjs", "Typescript", "Prisma"]
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center w-full">
|
||||
<div className="w-full max-w-6xl p-4 sm:px-6 py-6 sm:py-8">
|
||||
<h2 className="text-3xl sm:text-4xl font-bold text-center text-yellow-bright mb-8 sm:mb-12">
|
||||
Current Focus
|
||||
</h2>
|
||||
|
||||
{/* Recent Projects Section */}
|
||||
<div className="mb-8 sm:mb-16">
|
||||
<div className="flex items-center justify-center gap-2 mb-6">
|
||||
<Code2 className="text-yellow-bright" size={24} />
|
||||
<h3 className="text-xl font-bold text-foreground/90">Recent Projects</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 max-w-5xl mx-auto">
|
||||
{recentProjects.map((project) => (
|
||||
<a
|
||||
href={project.href}
|
||||
key={project.title}
|
||||
className="p-4 sm:p-6 rounded-lg border border-foreground/10 hover:border-yellow-bright/50
|
||||
transition-all duration-300 group bg-background/50"
|
||||
>
|
||||
<h4 className="font-bold text-lg group-hover:text-yellow-bright transition-colors">
|
||||
{project.title}
|
||||
</h4>
|
||||
<p className="text-foreground/70 mt-2 text-sm sm:text-base">{project.description}</p>
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{project.tech.map((tech) => (
|
||||
<span key={tech} className="text-xs px-2 py-1 rounded-full bg-foreground/5 text-foreground/60">
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Learning & Interests */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8 max-w-5xl mx-auto">
|
||||
{/* What I'm Learning */}
|
||||
<div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<BookOpen className="text-green-bright" size={24} />
|
||||
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Currently Learning</h3>
|
||||
</div>
|
||||
<ul className="space-y-3 text-sm sm:text-base text-foreground/70">
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-bright flex-shrink-0" />
|
||||
<span>Rust Programming</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-bright flex-shrink-0" />
|
||||
<span>WebAssembly with Rust</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-bright flex-shrink-0" />
|
||||
<span>HTTP/3 & WebTransport</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Project Interests */}
|
||||
<div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<RocketIcon className="text-blue-bright" size={24} />
|
||||
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Project Interests</h3>
|
||||
</div>
|
||||
<ul className="space-y-3 text-sm sm:text-base text-foreground/70">
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-blue-bright flex-shrink-0" />
|
||||
<span>AI Model Integration</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-blue-bright flex-shrink-0" />
|
||||
<span>Rust Systems Programming</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-blue-bright flex-shrink-0" />
|
||||
<span>Cross-platform WASM Apps</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Areas to Explore */}
|
||||
<div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Compass className="text-purple-bright" size={24} />
|
||||
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Want to Explore</h3>
|
||||
</div>
|
||||
<ul className="space-y-3 text-sm sm:text-base text-foreground/70">
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
|
||||
<span>LLM Fine-tuning</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
|
||||
<span>Rust 2024 Edition</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
|
||||
<span>Real-time Web Transport</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
65
src/src/components/about/intro.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
import { ChevronDownIcon } from "@/components/icons";
|
||||
|
||||
export default function Intro() {
|
||||
const scrollToNext = () => {
|
||||
const nextSection = document.querySelector("section")?.nextElementSibling;
|
||||
if (nextSection) {
|
||||
const offset = nextSection.offsetTop - (window.innerHeight - nextSection.offsetHeight) / 2; // Center the section
|
||||
window.scrollTo({
|
||||
top: offset,
|
||||
behavior: "smooth"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-4xl px-4">
|
||||
<div className="space-y-8 md:space-y-12">
|
||||
<div className="flex flex-col sm:flex-row items-center sm:items-center justify-center gap-8 sm:gap-16">
|
||||
<div className="w-32 h-32 sm:w-48 sm:h-48 shrink-0">
|
||||
<img
|
||||
src="/me.jpeg"
|
||||
alt="Timothy Pidashev"
|
||||
className="rounded-lg object-cover w-full h-full ring-2 ring-yellow-bright hover:ring-orange-bright transition-all duration-300"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center sm:text-left space-y-4 sm:space-y-6">
|
||||
<h2 className="text-xl sm:text-5xl font-bold text-yellow-bright">
|
||||
Timothy Pidashev
|
||||
</h2>
|
||||
<div className="text-sm sm:text-xl text-foreground/70 space-y-2 sm:space-y-3">
|
||||
<p className="flex items-center justify-center font-bold sm:justify-start gap-2">
|
||||
<span className="text-blue">Software Systems Engineer</span>
|
||||
</p>
|
||||
<p className="flex items-center justify-center font-bold sm:justify-start gap-2">
|
||||
<span className="text-green">Open Source Enthusiast</span>
|
||||
</p>
|
||||
<p className="flex items-center justify-center font-bold sm:justify-start gap-2">
|
||||
<span className="text-yellow">Coffee Connoisseur</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<p className="text-foreground/80 text-center text-base sm:text-2xl italic max-w-3xl mx-auto font-medium">
|
||||
"Turning coffee into code" isn't just a clever phrase –
|
||||
<span className="text-aqua-bright"> it's how I approach each project:</span>
|
||||
<span className="text-purple-bright"> methodically,</span>
|
||||
<span className="text-blue-bright"> with attention to detail,</span>
|
||||
<span className="text-green-bright"> and a refined process.</span>
|
||||
</p>
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
onClick={scrollToNext}
|
||||
className="text-foreground/50 hover:text-yellow-bright transition-colors duration-300"
|
||||
aria-label="Scroll to next section"
|
||||
>
|
||||
<ChevronDownIcon size={40} className="animate-bounce" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
65
src/src/components/about/outside-coding.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import { Fish, Mountain, Book, Car } from 'lucide-react';
|
||||
|
||||
export default function OutsideCoding() {
|
||||
const interests = [
|
||||
{
|
||||
icon: <Fish className="text-blue-bright" size={20} />,
|
||||
title: "Fishing",
|
||||
description: "Finding peace and adventure on the water, always looking for the next great fishing spot"
|
||||
},
|
||||
{
|
||||
icon: <Mountain className="text-green-bright" size={20} />,
|
||||
title: "Hiking",
|
||||
description: "Exploring trails with friends and seeking out scenic viewpoints in nature"
|
||||
},
|
||||
{
|
||||
icon: <Book className="text-purple-bright" size={20} />,
|
||||
title: "Reading",
|
||||
description: "Deep diving into novels & technical books that expand my horizons & captivate my mind"
|
||||
},
|
||||
{
|
||||
icon: <Car className="text-yellow-bright" size={20} />,
|
||||
title: "Project Cars",
|
||||
description: "Working on automotive projects, modifying & restoring sporty sedans"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center w-full">
|
||||
<div className="w-full max-w-4xl px-4 py-8">
|
||||
<h2 className="text-2xl md:text-4xl font-bold text-center text-yellow-bright mb-8">
|
||||
Outside of Programming
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
{interests.map((interest) => (
|
||||
<div
|
||||
key={interest.title}
|
||||
className="flex flex-col items-center text-center p-4 rounded-lg border border-foreground/10
|
||||
hover:border-yellow-bright/50 transition-all duration-300 bg-background/50"
|
||||
>
|
||||
<div className="mb-3">
|
||||
{interest.icon}
|
||||
</div>
|
||||
<h3 className="font-bold text-foreground/90 mb-2">
|
||||
{interest.title}
|
||||
</h3>
|
||||
<p className="text-sm text-foreground/70">
|
||||
{interest.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-center text-foreground/80 mt-8 max-w-2xl mx-auto text-sm md:text-base italic">
|
||||
When I'm not writing code, you'll find me
|
||||
<span className="text-blue-bright"> out on the water,</span>
|
||||
<span className="text-green-bright"> hiking trails,</span>
|
||||
<span className="text-purple-bright"> reading books,</span>
|
||||
<span className="text-yellow-bright"> or modifying my current ride.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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
@@ -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
@@ -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;
|
||||
81
src/src/components/about/timeline.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
import { Check, Code, GitBranch, Star } from "lucide-react";
|
||||
|
||||
export default function Timeline() {
|
||||
const timelineItems = [
|
||||
{
|
||||
year: "2024",
|
||||
title: "Present",
|
||||
description: "The wisdom of past ventures now flows through my work, whether crafting elegant CRUD applications or embarking on bold projects that expand my limits.",
|
||||
technologies: ["Rust", "Typescript", "Go", "Postgres"],
|
||||
icon: <Code className="text-yellow-bright" size={20} />
|
||||
},
|
||||
{
|
||||
year: "2022",
|
||||
title: "Diving Deeper",
|
||||
description: "The worlds of systems programming and scalable infrastructure collided as I explored low-level C++ graphics programming and containerization with Docker.",
|
||||
technologies: ["C++", "Cmake", "Docker", "Docker Compose"],
|
||||
icon: <GitBranch className="text-green-bright" size={20} />
|
||||
},
|
||||
{
|
||||
year: "2020",
|
||||
title: "Exploring the Stack",
|
||||
description: "Starting with pure HTML and CSS, I explored the foundations of web development, gradually venturing into JavaScript and React to bring my static pages to life.",
|
||||
technologies: ["Javascript", "Tailwind", "React", "Express"],
|
||||
icon: <Star className="text-blue-bright" size={20} />
|
||||
},
|
||||
{
|
||||
year: "2018",
|
||||
title: "Starting the Journey",
|
||||
description: "An elective Python class in 8th grade transformed my keen interest in programming into a relentless obsession, one that drove me to constantly explore new depths.",
|
||||
technologies: ["Python", "Discord.py", "Asyncio", "Sqlite"],
|
||||
icon: <Check className="text-purple-bright" size={20} />
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-6xl px-4 py-8 relative z-0">
|
||||
<h2 className="text-2xl md:text-4xl font-bold text-center text-yellow-bright mb-8 md:mb-12">
|
||||
Journey Through Code
|
||||
</h2>
|
||||
<div className="relative">
|
||||
<div className="absolute left-4 sm:left-1/2 h-full w-0.5 bg-foreground/10 -translate-x-1/2" />
|
||||
|
||||
<div className="ml-8 sm:ml-0">
|
||||
{timelineItems.map((item, index) => (
|
||||
<div key={item.year} className="relative mb-8 md:mb-12 last:mb-0">
|
||||
<div className={`flex flex-col sm:flex-row items-start ${
|
||||
index % 2 === 0 ? 'sm:flex-row-reverse' : ''
|
||||
}`}>
|
||||
<div className="absolute -left-8 sm:left-1/2 w-6 h-6 sm:w-8 sm:h-8 bg-background
|
||||
rounded-full border-2 border-yellow-bright sm:-translate-x-1/2
|
||||
flex items-center justify-center z-10">
|
||||
{item.icon}
|
||||
</div>
|
||||
<div className={`w-full sm:w-[calc(50%-32px)] ${
|
||||
index % 2 === 0 ? 'sm:pr-8 md:pr-12' : 'sm:pl-8 md:pl-12'
|
||||
}`}>
|
||||
<div className="p-4 sm:p-6 bg-background/50 rounded-lg border border-foreground/10
|
||||
hover:border-yellow-bright/50 transition-colors duration-300">
|
||||
<span className="text-xs sm:text-sm font-mono text-yellow-bright">{item.year}</span>
|
||||
<h3 className="text-lg sm:text-xl font-bold text-foreground/90 mt-2">{item.title}</h3>
|
||||
<p className="text-sm sm:text-base text-foreground/70 mt-2">{item.description}</p>
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{item.technologies.map((tech) => (
|
||||
<span key={tech}
|
||||
className="px-2 py-1 text-xs sm:text-sm rounded-full bg-foreground/5
|
||||
text-foreground/60 hover:text-yellow-bright transition-colors duration-300">
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
655
src/src/components/background/index.tsx
Normal file
@@ -0,0 +1,655 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface Cell {
|
||||
alive: boolean;
|
||||
next: boolean;
|
||||
color: [number, number, number];
|
||||
baseColor: [number, number, number]; // Original color
|
||||
currentX: number;
|
||||
currentY: number;
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
opacity: number;
|
||||
targetOpacity: number;
|
||||
scale: number;
|
||||
targetScale: number;
|
||||
elevation: number; // For 3D effect
|
||||
targetElevation: number;
|
||||
transitioning: boolean;
|
||||
transitionComplete: boolean;
|
||||
rippleEffect: number; // For ripple animation
|
||||
}
|
||||
|
||||
interface Grid {
|
||||
cells: Cell[][];
|
||||
cols: number;
|
||||
rows: number;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
}
|
||||
|
||||
interface MousePosition {
|
||||
x: number;
|
||||
y: number;
|
||||
isDown: boolean;
|
||||
lastClickTime: number;
|
||||
cellX: number;
|
||||
cellY: number;
|
||||
}
|
||||
|
||||
interface BackgroundProps {
|
||||
layout?: 'index' | 'sidebar';
|
||||
position?: 'left' | 'right';
|
||||
}
|
||||
|
||||
const CELL_SIZE = 25;
|
||||
const TRANSITION_SPEED = 0.05;
|
||||
const SCALE_SPEED = 0.05;
|
||||
const CYCLE_FRAMES = 180;
|
||||
const INITIAL_DENSITY = 0.15;
|
||||
const SIDEBAR_WIDTH = 240;
|
||||
const MOUSE_INFLUENCE_RADIUS = 150; // Radius of mouse influence in pixels
|
||||
const COLOR_SHIFT_AMOUNT = 30; // Maximum color shift amount
|
||||
const RIPPLE_SPEED = 0.2; // Speed of ripple propagation
|
||||
const ELEVATION_FACTOR = 15; // Max height for 3D effect
|
||||
|
||||
const Background: React.FC<BackgroundProps> = ({
|
||||
layout = 'index',
|
||||
position = 'left'
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const gridRef = useRef<Grid>();
|
||||
const animationFrameRef = useRef<number>();
|
||||
const frameCount = useRef(0);
|
||||
const resizeTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const mouseRef = useRef<MousePosition>({
|
||||
x: -1000,
|
||||
y: -1000,
|
||||
isDown: false,
|
||||
lastClickTime: 0,
|
||||
cellX: -1,
|
||||
cellY: -1
|
||||
});
|
||||
|
||||
const randomColor = (): [number, number, number] => {
|
||||
const colors = [
|
||||
[204, 36, 29], // red
|
||||
[152, 151, 26], // green
|
||||
[215, 153, 33], // yellow
|
||||
[69, 133, 136], // blue
|
||||
[177, 98, 134], // purple
|
||||
[104, 157, 106] // aqua
|
||||
];
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
};
|
||||
|
||||
const calculateGridDimensions = (width: number, height: number) => {
|
||||
const cols = Math.floor(width / CELL_SIZE);
|
||||
const rows = Math.floor(height / CELL_SIZE);
|
||||
const offsetX = Math.floor((width - (cols * CELL_SIZE)) / 2);
|
||||
const offsetY = Math.floor((height - (rows * CELL_SIZE)) / 2);
|
||||
return { cols, rows, offsetX, offsetY };
|
||||
};
|
||||
|
||||
const initGrid = (width: number, height: number): Grid => {
|
||||
const { cols, rows, offsetX, offsetY } = calculateGridDimensions(width, height);
|
||||
|
||||
const cells = Array(cols).fill(0).map((_, i) =>
|
||||
Array(rows).fill(0).map((_, j) => {
|
||||
const baseColor = randomColor();
|
||||
return {
|
||||
alive: Math.random() < INITIAL_DENSITY,
|
||||
next: false,
|
||||
color: [...baseColor] as [number, number, number],
|
||||
baseColor: baseColor,
|
||||
currentX: i,
|
||||
currentY: j,
|
||||
targetX: i,
|
||||
targetY: j,
|
||||
opacity: 0,
|
||||
targetOpacity: 0,
|
||||
scale: 0,
|
||||
targetScale: 0,
|
||||
elevation: 0,
|
||||
targetElevation: 0,
|
||||
transitioning: false,
|
||||
transitionComplete: false,
|
||||
rippleEffect: 0
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const grid = { cells, cols, rows, offsetX, offsetY };
|
||||
computeNextState(grid);
|
||||
|
||||
// Initialize cells with staggered animation
|
||||
for (let i = 0; i < cols; i++) {
|
||||
for (let j = 0; j < rows; j++) {
|
||||
const cell = cells[i][j];
|
||||
if (cell.next) {
|
||||
cell.alive = true;
|
||||
setTimeout(() => {
|
||||
cell.targetOpacity = 1;
|
||||
cell.targetScale = 1;
|
||||
}, Math.random() * 1000);
|
||||
} else {
|
||||
cell.alive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return grid;
|
||||
};
|
||||
|
||||
const countNeighbors = (grid: Grid, x: number, y: number): { count: number, colors: [number, number, number][] } => {
|
||||
const neighbors = { count: 0, colors: [] as [number, number, number][] };
|
||||
|
||||
for (let i = -1; i <= 1; i++) {
|
||||
for (let j = -1; j <= 1; j++) {
|
||||
if (i === 0 && j === 0) continue;
|
||||
|
||||
const col = (x + i + grid.cols) % grid.cols;
|
||||
const row = (y + j + grid.rows) % grid.rows;
|
||||
|
||||
if (grid.cells[col][row].alive) {
|
||||
neighbors.count++;
|
||||
neighbors.colors.push(grid.cells[col][row].baseColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return neighbors;
|
||||
};
|
||||
|
||||
const averageColors = (colors: [number, number, number][]): [number, number, number] => {
|
||||
if (colors.length === 0) return [0, 0, 0];
|
||||
const sum = colors.reduce((acc, color) => [
|
||||
acc[0] + color[0],
|
||||
acc[1] + color[1],
|
||||
acc[2] + color[2]
|
||||
], [0, 0, 0]);
|
||||
return [
|
||||
Math.round(sum[0] / colors.length),
|
||||
Math.round(sum[1] / colors.length),
|
||||
Math.round(sum[2] / colors.length)
|
||||
];
|
||||
};
|
||||
|
||||
const computeNextState = (grid: Grid) => {
|
||||
// First, calculate the next state for all cells based on standard rules
|
||||
for (let i = 0; i < grid.cols; i++) {
|
||||
for (let j = 0; j < grid.rows; j++) {
|
||||
const cell = grid.cells[i][j];
|
||||
const { count, colors } = countNeighbors(grid, i, j);
|
||||
|
||||
// Standard Conway's Game of Life rules
|
||||
if (cell.alive) {
|
||||
cell.next = count === 2 || count === 3;
|
||||
} else {
|
||||
cell.next = count === 3;
|
||||
if (cell.next) {
|
||||
cell.baseColor = averageColors(colors);
|
||||
cell.color = [...cell.baseColor];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then, set up animations for cells that need to change state
|
||||
for (let i = 0; i < grid.cols; i++) {
|
||||
for (let j = 0; j < grid.rows; j++) {
|
||||
const cell = grid.cells[i][j];
|
||||
|
||||
if (cell.alive !== cell.next && !cell.transitioning) {
|
||||
cell.transitioning = true;
|
||||
cell.transitionComplete = false;
|
||||
|
||||
// Random delay for staggered animation effect
|
||||
const delay = Math.random() * 800;
|
||||
|
||||
setTimeout(() => {
|
||||
if (!cell.next) {
|
||||
cell.targetScale = 0;
|
||||
cell.targetOpacity = 0;
|
||||
cell.targetElevation = 0;
|
||||
} else {
|
||||
cell.targetScale = 1;
|
||||
cell.targetOpacity = 1;
|
||||
cell.targetElevation = 0;
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const createRippleEffect = (grid: Grid, centerX: number, centerY: number) => {
|
||||
const maxDistance = Math.max(grid.cols, grid.rows) / 2;
|
||||
|
||||
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) {
|
||||
// Delayed animation based on distance from center
|
||||
setTimeout(() => {
|
||||
cell.rippleEffect = 1; // Start ripple
|
||||
|
||||
// After a short time, reset ripple
|
||||
setTimeout(() => {
|
||||
cell.rippleEffect = 0;
|
||||
}, 300 + distance * 50);
|
||||
}, distance * 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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) => {
|
||||
const mouseX = mouseRef.current.x;
|
||||
const mouseY = mouseRef.current.y;
|
||||
|
||||
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) * TRANSITION_SPEED;
|
||||
cell.scale += (cell.targetScale - cell.scale) * SCALE_SPEED;
|
||||
cell.elevation += (cell.targetElevation - cell.elevation) * SCALE_SPEED;
|
||||
|
||||
// Apply mouse interaction
|
||||
const cellCenterX = grid.offsetX + i * CELL_SIZE + CELL_SIZE / 2;
|
||||
const cellCenterY = grid.offsetY + j * CELL_SIZE + CELL_SIZE / 2;
|
||||
const dx = cellCenterX - mouseX;
|
||||
const dy = cellCenterY - mouseY;
|
||||
const distanceToMouse = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// Color wave effect based on mouse position
|
||||
if (distanceToMouse < MOUSE_INFLUENCE_RADIUS && cell.opacity > 0.1) {
|
||||
// Calculate color adjustment based on distance
|
||||
const influenceFactor = 1 - (distanceToMouse / MOUSE_INFLUENCE_RADIUS);
|
||||
|
||||
// Wave effect with sine function
|
||||
const waveOffset = (frameCount.current * 0.05 + distanceToMouse * 0.05) % (Math.PI * 2);
|
||||
const waveFactor = (Math.sin(waveOffset) * 0.5 + 0.5) * influenceFactor;
|
||||
|
||||
// Adjust color based on wave
|
||||
cell.color = [
|
||||
Math.min(255, Math.max(0, cell.baseColor[0] + COLOR_SHIFT_AMOUNT * waveFactor)),
|
||||
Math.min(255, Math.max(0, cell.baseColor[1] - COLOR_SHIFT_AMOUNT * waveFactor)),
|
||||
Math.min(255, Math.max(0, cell.baseColor[2] + COLOR_SHIFT_AMOUNT * waveFactor))
|
||||
] as [number, number, number];
|
||||
|
||||
// 3D elevation effect when mouse is close
|
||||
cell.targetElevation = ELEVATION_FACTOR * influenceFactor;
|
||||
} else {
|
||||
// Gradually return to base color 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;
|
||||
|
||||
// Reset elevation when mouse moves away
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Gradually decrease ripple effect
|
||||
if (cell.rippleEffect > 0) {
|
||||
cell.rippleEffect = Math.max(0, cell.rippleEffect - RIPPLE_SPEED);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
if (!gridRef.current || !canvasRef.current) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
mouseRef.current.isDown = true;
|
||||
mouseRef.current.lastClickTime = Date.now();
|
||||
|
||||
const grid = gridRef.current;
|
||||
|
||||
// Calculate which cell was clicked
|
||||
const cellX = Math.floor((mouseX - grid.offsetX) / CELL_SIZE);
|
||||
const cellY = Math.floor((mouseY - grid.offsetY) / CELL_SIZE);
|
||||
|
||||
if (cellX >= 0 && cellX < grid.cols && cellY >= 0 && cellY < grid.rows) {
|
||||
mouseRef.current.cellX = cellX;
|
||||
mouseRef.current.cellY = cellY;
|
||||
|
||||
const cell = grid.cells[cellX][cellY];
|
||||
|
||||
if (cell.alive) {
|
||||
// Create ripple effect from existing cell
|
||||
createRippleEffect(grid, cellX, cellY);
|
||||
} else {
|
||||
// Spawn new cell at empty position
|
||||
spawnCellAtPosition(grid, cellX, cellY);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!canvasRef.current) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
mouseRef.current.x = e.clientX - rect.left;
|
||||
mouseRef.current.y = e.clientY - rect.top;
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
mouseRef.current.isDown = false;
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
mouseRef.current.isDown = false;
|
||||
mouseRef.current.x = -1000;
|
||||
mouseRef.current.y = -1000;
|
||||
};
|
||||
|
||||
const setupCanvas = (canvas: HTMLCanvasElement, width: number, height: number) => {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = width * dpr;
|
||||
canvas.height = height * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
|
||||
return ctx;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
// Create an AbortController for cleanup
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
const handleResize = () => {
|
||||
if (signal.aborted) return;
|
||||
|
||||
if (resizeTimeoutRef.current) {
|
||||
clearTimeout(resizeTimeoutRef.current);
|
||||
}
|
||||
|
||||
resizeTimeoutRef.current = setTimeout(() => {
|
||||
if (signal.aborted) return;
|
||||
|
||||
const displayWidth = layout === 'index' ? window.innerWidth : SIDEBAR_WIDTH;
|
||||
const displayHeight = window.innerHeight;
|
||||
|
||||
const ctx = setupCanvas(canvas, displayWidth, displayHeight);
|
||||
if (!ctx) return;
|
||||
|
||||
frameCount.current = 0;
|
||||
|
||||
// Only initialize new grid if one doesn't exist or dimensions changed
|
||||
if (!gridRef.current ||
|
||||
gridRef.current.cols !== Math.floor(displayWidth / CELL_SIZE) ||
|
||||
gridRef.current.rows !== Math.floor(displayHeight / CELL_SIZE)) {
|
||||
gridRef.current = initGrid(displayWidth, displayHeight);
|
||||
}
|
||||
}, 250);
|
||||
};
|
||||
|
||||
const displayWidth = layout === 'index' ? window.innerWidth : SIDEBAR_WIDTH;
|
||||
const displayHeight = window.innerHeight;
|
||||
|
||||
const ctx = setupCanvas(canvas, displayWidth, displayHeight);
|
||||
if (!ctx) return;
|
||||
|
||||
// Only initialize grid if it doesn't exist
|
||||
if (!gridRef.current) {
|
||||
gridRef.current = initGrid(displayWidth, displayHeight);
|
||||
}
|
||||
|
||||
// Add mouse event listeners
|
||||
canvas.addEventListener('mousedown', handleMouseDown, { signal });
|
||||
canvas.addEventListener('mousemove', handleMouseMove, { signal });
|
||||
canvas.addEventListener('mouseup', handleMouseUp, { signal });
|
||||
canvas.addEventListener('mouseleave', handleMouseLeave, { signal });
|
||||
|
||||
const animate = () => {
|
||||
if (signal.aborted) return;
|
||||
|
||||
frameCount.current++;
|
||||
|
||||
if (gridRef.current) {
|
||||
// Every CYCLE_FRAMES, compute the next state
|
||||
if (frameCount.current % CYCLE_FRAMES === 0) {
|
||||
computeNextState(gridRef.current);
|
||||
}
|
||||
|
||||
updateCellAnimations(gridRef.current);
|
||||
}
|
||||
|
||||
// Draw frame
|
||||
ctx.fillStyle = '#000000';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (gridRef.current) {
|
||||
const grid = gridRef.current;
|
||||
const cellSize = CELL_SIZE * 0.8;
|
||||
const roundness = cellSize * 0.2;
|
||||
|
||||
for (let i = 0; i < grid.cols; i++) {
|
||||
for (let j = 0; j < grid.rows; j++) {
|
||||
const cell = grid.cells[i][j];
|
||||
// Draw all transitioning cells, even if they're fading out
|
||||
if ((cell.alive || cell.targetOpacity > 0) && cell.opacity > 0.01) {
|
||||
const [r, g, b] = cell.color;
|
||||
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
||||
|
||||
// Apply ripple and elevation effects to opacity
|
||||
const rippleBoost = cell.rippleEffect * 0.4; // Boost opacity during ripple
|
||||
ctx.globalAlpha = Math.min(1, cell.opacity * 0.8 + rippleBoost);
|
||||
|
||||
const scaledSize = cellSize * cell.scale;
|
||||
const xOffset = (cellSize - scaledSize) / 2;
|
||||
const yOffset = (cellSize - scaledSize) / 2;
|
||||
|
||||
// Apply 3D elevation effect
|
||||
const elevationOffset = cell.elevation;
|
||||
|
||||
const x = grid.offsetX + i * CELL_SIZE + (CELL_SIZE - cellSize) / 2 + xOffset;
|
||||
const y = grid.offsetY + j * CELL_SIZE + (CELL_SIZE - cellSize) / 2 + yOffset - elevationOffset;
|
||||
const scaledRoundness = roundness * cell.scale;
|
||||
|
||||
// Draw shadow for 3D effect if cell has elevation
|
||||
if (elevationOffset > 1) {
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + scaledRoundness, y + elevationOffset + 5);
|
||||
ctx.lineTo(x + scaledSize - scaledRoundness, y + elevationOffset + 5);
|
||||
ctx.quadraticCurveTo(x + scaledSize, y + elevationOffset + 5, x + scaledSize, y + elevationOffset + 5 + scaledRoundness);
|
||||
ctx.lineTo(x + scaledSize, y + elevationOffset + 5 + scaledSize - scaledRoundness);
|
||||
ctx.quadraticCurveTo(x + scaledSize, y + elevationOffset + 5 + scaledSize, x + scaledSize - scaledRoundness, y + elevationOffset + 5 + scaledSize);
|
||||
ctx.lineTo(x + scaledRoundness, y + elevationOffset + 5 + scaledSize);
|
||||
ctx.quadraticCurveTo(x, y + elevationOffset + 5 + scaledSize, x, y + elevationOffset + 5 + scaledSize - scaledRoundness);
|
||||
ctx.lineTo(x, y + elevationOffset + 5 + scaledRoundness);
|
||||
ctx.quadraticCurveTo(x, y + elevationOffset + 5, x + scaledRoundness, y + elevationOffset + 5);
|
||||
ctx.fill();
|
||||
|
||||
// Draw side of elevated cell
|
||||
const sideHeight = elevationOffset;
|
||||
ctx.fillStyle = `rgba(${r*0.7}, ${g*0.7}, ${b*0.7}, ${ctx.globalAlpha})`;
|
||||
|
||||
// Left side
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y + scaledRoundness);
|
||||
ctx.lineTo(x, y + scaledSize - scaledRoundness + sideHeight);
|
||||
ctx.lineTo(x + scaledRoundness, y + scaledSize + sideHeight);
|
||||
ctx.lineTo(x + scaledRoundness, y + scaledSize);
|
||||
ctx.lineTo(x, y + scaledSize - scaledRoundness);
|
||||
ctx.fill();
|
||||
|
||||
// Right side
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + scaledSize, y + scaledRoundness);
|
||||
ctx.lineTo(x + scaledSize, y + scaledSize - scaledRoundness + sideHeight);
|
||||
ctx.lineTo(x + scaledSize - scaledRoundness, y + scaledSize + sideHeight);
|
||||
ctx.lineTo(x + scaledSize - scaledRoundness, y + scaledSize);
|
||||
ctx.lineTo(x + scaledSize, y + scaledSize - scaledRoundness);
|
||||
ctx.fill();
|
||||
|
||||
// Bottom side
|
||||
ctx.fillStyle = `rgba(${r*0.5}, ${g*0.5}, ${b*0.5}, ${ctx.globalAlpha})`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + scaledRoundness, y + scaledSize);
|
||||
ctx.lineTo(x + scaledSize - scaledRoundness, y + scaledSize);
|
||||
ctx.lineTo(x + scaledSize - scaledRoundness, y + scaledSize + sideHeight);
|
||||
ctx.lineTo(x + scaledRoundness, y + scaledSize + sideHeight);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Draw main cell with original color
|
||||
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + scaledRoundness, y);
|
||||
ctx.lineTo(x + scaledSize - scaledRoundness, y);
|
||||
ctx.quadraticCurveTo(x + scaledSize, y, x + scaledSize, y + scaledRoundness);
|
||||
ctx.lineTo(x + scaledSize, y + scaledSize - scaledRoundness);
|
||||
ctx.quadraticCurveTo(x + scaledSize, y + scaledSize, x + scaledSize - scaledRoundness, y + scaledSize);
|
||||
ctx.lineTo(x + scaledRoundness, y + scaledSize);
|
||||
ctx.quadraticCurveTo(x, y + scaledSize, x, y + scaledSize - scaledRoundness);
|
||||
ctx.lineTo(x, y + scaledRoundness);
|
||||
ctx.quadraticCurveTo(x, y, x + scaledRoundness, y);
|
||||
ctx.fill();
|
||||
|
||||
// Draw highlight on top for 3D effect
|
||||
if (elevationOffset > 1) {
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${0.2 * 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();
|
||||
}
|
||||
|
||||
// Draw ripple effect
|
||||
if (cell.rippleEffect > 0) {
|
||||
const rippleRadius = cell.rippleEffect * cellSize * 2;
|
||||
const rippleAlpha = (1 - cell.rippleEffect) * 0.5;
|
||||
|
||||
ctx.strokeStyle = `rgba(255, 255, 255, ${rippleAlpha})`;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
x + scaledSize / 2,
|
||||
y + scaledSize / 2,
|
||||
rippleRadius,
|
||||
0,
|
||||
Math.PI * 2
|
||||
);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize, { signal });
|
||||
animate();
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
if (resizeTimeoutRef.current) {
|
||||
clearTimeout(resizeTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [layout]); // Added layout as a dependency since it's used in the effect
|
||||
|
||||
const getContainerClasses = () => {
|
||||
if (layout === 'index') {
|
||||
return 'fixed inset-0 -z-10';
|
||||
}
|
||||
|
||||
const baseClasses = 'fixed top-0 bottom-0 hidden lg:block -z-10';
|
||||
return position === 'left'
|
||||
? `${baseClasses} left-0`
|
||||
: `${baseClasses} right-0`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={getContainerClasses()}>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="w-full h-full bg-black cursor-pointer"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm pointer-events-none" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Background;
|
||||
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="lazy"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
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>
|
||||
);
|
||||
};
|
||||
99
src/src/components/blog/post-list.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import React from "react";
|
||||
|
||||
type BlogPost = {
|
||||
slug: string;
|
||||
data: {
|
||||
title: string;
|
||||
author: string;
|
||||
date: string;
|
||||
tags: string[];
|
||||
description: string;
|
||||
image?: string;
|
||||
imagePosition?: string;
|
||||
};
|
||||
};
|
||||
|
||||
interface BlogPostListProps {
|
||||
posts: BlogPost[];
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric"
|
||||
});
|
||||
};
|
||||
|
||||
export const BlogPostList = ({ posts }: BlogPostListProps) => {
|
||||
return (
|
||||
<div className="w-full max-w-6xl mx-auto">
|
||||
<ul className="space-y-6 md:space-y-10">
|
||||
{posts.map((post) => (
|
||||
<li key={post.slug} className="group px-4 md:px-0">
|
||||
<a
|
||||
href={`/blog/${post.slug}`}
|
||||
className="block"
|
||||
>
|
||||
<article className="flex flex-col md:flex-row gap-4 md:gap-8 pb-6 md:pb-10 border-b border-foreground/20 last:border-b-0 p-2 md:p-4 rounded-lg group-hover:outline group-hover:outline-2 group-hover:outline-purple transition-all duration-200">
|
||||
{/* Image container with fixed aspect ratio */}
|
||||
<div className="w-full md:w-1/3 aspect-[16/9] overflow-hidden rounded-lg bg-background">
|
||||
<img
|
||||
src={post.data.image || "/blog/placeholder.png"}
|
||||
alt={post.data.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
style={{ objectPosition: post.data.imagePosition || "center center" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content container */}
|
||||
<div className="w-full md:w-2/3 flex flex-col gap-2 md:gap-4 py-1 md:py-2">
|
||||
{/* Title and meta info */}
|
||||
<div className="space-y-1.5 md:space-y-3">
|
||||
<h2 className="text-lg md:text-2xl font-semibold text-yellow group-hover:text-purple transition-colors duration-200 line-clamp-2">
|
||||
{post.data.title}
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 md:gap-3 text-sm md:text-base text-foreground/80">
|
||||
<span className="text-orange">{post.data.author}</span>
|
||||
<span className="text-foreground/50">•</span>
|
||||
<time dateTime={post.data.date} className="text-blue">
|
||||
{formatDate(post.data.date)}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-foreground/90 text-sm md:text-lg leading-relaxed line-clamp-2 mt-0.5 md:mt-0">
|
||||
{post.data.description}
|
||||
</p>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="flex flex-wrap gap-1.5 md:gap-3 mt-1 md:mt-2">
|
||||
{post.data.tags.slice(0, 3).map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="text-xs md:text-base text-aqua hover:text-aqua-bright transition-colors duration-200"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.location.href = `/blog/tag/${tag}`;
|
||||
}}
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
{post.data.tags.length > 3 && (
|
||||
<span className="text-xs md:text-base text-foreground/60">
|
||||
+{post.data.tags.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
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;
|
||||
21
src/src/components/footer/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { Links } from "@/components/footer/links";
|
||||
|
||||
export default function Footer({ fixed = false }) {
|
||||
const footerLinks = Links.map((link) => (
|
||||
<div
|
||||
key={link.id}
|
||||
className={`inline-block ${link.color}`}
|
||||
>
|
||||
<a href={link.href} target="_blank" rel="noopener noreferrer">{link.label}</a>
|
||||
</div>
|
||||
));
|
||||
|
||||
return (
|
||||
<footer className={`w-full font-bold ${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">
|
||||
{footerLinks}
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
14
src/src/components/footer/links.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
interface FooterLink {
|
||||
id: number;
|
||||
href: string;
|
||||
label: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export const Links: FooterLink[] = [
|
||||
{ id: 0, href: "mailto:contact@timmypidashev.dev", label: "Contact", color: "text-green" },
|
||||
{ id: 1, href: "https://github.com/timmypidashev", label: "Github", color: "text-yellow" },
|
||||
{ id: 3, href: "https://www.linkedin.com/in/timothy-pidashev-4353812b8", label: "Linkedin", color: "text-blue" },
|
||||
{ id: 4, href: "https://github.com/timmypidashev/web", label: "Source", color: "text-purple" },
|
||||
{ id: 5, href: "https://github.com/timmypidashev/web/releases", label: "v2", color: "text-aqua" }
|
||||
];
|
||||
107
src/src/components/header/index.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Links } from "@/components/header/links";
|
||||
|
||||
export default function Header() {
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
const [visible, setVisible] = useState(true);
|
||||
const [lastScrollY, setLastScrollY] = useState(0);
|
||||
const [currentPath, setCurrentPath] = useState("");
|
||||
const [shouldAnimate, setShouldAnimate] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
setCurrentPath(document.location.pathname);
|
||||
setTimeout(() => setShouldAnimate(true), 50);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isClient) return;
|
||||
const handleScroll = () => {
|
||||
const currentScrollY = document.documentElement.scrollTop;
|
||||
setVisible(currentScrollY < lastScrollY || currentScrollY < 10);
|
||||
setLastScrollY(currentScrollY);
|
||||
};
|
||||
document.addEventListener("scroll", handleScroll);
|
||||
return () => document.removeEventListener("scroll", handleScroll);
|
||||
}, [lastScrollY, isClient]);
|
||||
|
||||
const checkIsActive = (linkHref: string): boolean => {
|
||||
if (!isClient) return false;
|
||||
|
||||
const path = document.location.pathname;
|
||||
if (linkHref === "/") return path === "/";
|
||||
|
||||
return linkHref !== "/" && path.startsWith(linkHref);
|
||||
};
|
||||
|
||||
const isIndexPage = checkIsActive("/");
|
||||
const headerLinks = Links.map((link) => {
|
||||
const isActive = checkIsActive(link.href);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={link.id}
|
||||
className={`
|
||||
relative inline-block
|
||||
${link.color}
|
||||
${!isIndexPage ? 'bg-black' : ''}
|
||||
`}
|
||||
>
|
||||
<a
|
||||
href={link.href}
|
||||
className="relative inline-block"
|
||||
>
|
||||
{link.label}
|
||||
<div className="absolute -bottom-1 left-0 w-full overflow-hidden">
|
||||
{isClient && isActive && (
|
||||
<svg
|
||||
className="w-full h-3 opacity-100"
|
||||
preserveAspectRatio="none"
|
||||
viewBox="0 0 100 12"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
className="stroke-current transition-[stroke-dashoffset] duration-[600ms] ease-out"
|
||||
d="M0,6
|
||||
C25,4 35,8 50,6
|
||||
S75,4 100,6"
|
||||
fill="none"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
style={{
|
||||
strokeDasharray: 100,
|
||||
strokeDashoffset: shouldAnimate ? 0 : 100
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`
|
||||
fixed z-50 top-0 left-0 right-0
|
||||
font-bold
|
||||
transition-transform duration-300
|
||||
${visible ? "translate-y-0" : "-translate-y-full"}
|
||||
`}
|
||||
>
|
||||
<div className={`
|
||||
w-full flex flex-row items-center justify-center
|
||||
${!isIndexPage ? 'bg-black md:bg-transparent' : ''}
|
||||
`}>
|
||||
<div className={`
|
||||
w-full md:w-auto flex flex-row pt-1 px-2 text-lg lg:text-3xl md:text-2xl
|
||||
items-center justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20 md:py-2
|
||||
${!isIndexPage ? 'bg-black md:px-20' : ''}
|
||||
`}>
|
||||
{headerLinks}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
14
src/src/components/header/links.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
interface HeaderLink {
|
||||
id: number;
|
||||
href: string;
|
||||
label: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export const Links: HeaderLink[] = [
|
||||
{ id: 0, href: "/", label: "Home", color: "text-green" },
|
||||
{ id: 1, href: "/about", label: "About", color: "text-yellow" },
|
||||
{ id: 2, href: "/projects", label: "Projects", color: "text-blue" },
|
||||
{ id: 3, href: "/blog", label: "Blog", color: "text-purple" },
|
||||
{ id: 4, href: "/resume", label: "Resume", color: "text-aqua" }
|
||||
];
|
||||
84
src/src/components/hero/index.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from "react";
|
||||
import Typewriter from "typewriter-effect";
|
||||
|
||||
const html = (strings: TemplateStringsArray, ...values: any[]) => {
|
||||
let result = strings[0];
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
result += values[i] + strings[i + 1];
|
||||
}
|
||||
return result.replace(/\n\s*/g, "").replace(/\s+/g, " ").trim();
|
||||
};
|
||||
|
||||
interface TypewriterOptions {
|
||||
autoStart: boolean;
|
||||
loop: boolean;
|
||||
delay: number;
|
||||
deleteSpeed: number;
|
||||
cursor: string;
|
||||
}
|
||||
|
||||
interface TypewriterInstance {
|
||||
typeString: (str: string) => TypewriterInstance;
|
||||
pauseFor: (ms: number) => TypewriterInstance;
|
||||
deleteAll: () => TypewriterInstance;
|
||||
start: () => TypewriterInstance;
|
||||
}
|
||||
|
||||
export default function Hero() {
|
||||
const SECTION_1 = html`
|
||||
<span>Hello, I'm</span>
|
||||
<br><div class="mb-4"></div>
|
||||
<span><a href="/about" class="text-aqua hover:underline"><strong>Timothy Pidashev</strong></a></span>
|
||||
`;
|
||||
|
||||
const SECTION_2 = html`
|
||||
<span>I've been turning</span>
|
||||
<br><div class="mb-4"></div>
|
||||
<span><a href="/projects" class="text-green hover:underline"><strong>coffee</strong></a> into
|
||||
<a href="/projects" class="text-yellow hover:underline"><strong>code</strong></a></span>
|
||||
<br><div class="mb-4"></div>
|
||||
<span>since <a href="/about" class="text-blue hover:underline"><strong>2018</strong></a>!</span>
|
||||
`;
|
||||
|
||||
const SECTION_3 = html`
|
||||
<span>Check out my</span>
|
||||
<br><div class="mb-4"></div>
|
||||
<span><a href="/blog" class="text-purple hover:underline"><strong>blog</strong></a>/
|
||||
<a href="/projects" class="text-blue hover:underline"><strong>projects</strong></a> or</span>
|
||||
<br><div class="mb-4"></div>
|
||||
<span><a href="/contact" class="text-green hover:underline"><strong>contact</strong></a> me below!</span>
|
||||
`;
|
||||
|
||||
const handleInit = (typewriter: TypewriterInstance): void => {
|
||||
typewriter
|
||||
.typeString(SECTION_1)
|
||||
.pauseFor(2000)
|
||||
.deleteAll()
|
||||
.typeString(SECTION_2)
|
||||
.pauseFor(2000)
|
||||
.deleteAll()
|
||||
.typeString(SECTION_3)
|
||||
.pauseFor(2000)
|
||||
.deleteAll()
|
||||
.start();
|
||||
};
|
||||
|
||||
const typewriterOptions: TypewriterOptions = {
|
||||
autoStart: true,
|
||||
loop: true,
|
||||
delay: 50,
|
||||
deleteSpeed: 800,
|
||||
cursor: '|'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen">
|
||||
<div className="text-4xl font-bold text-center">
|
||||
<Typewriter
|
||||
options={typewriterOptions}
|
||||
onInit={handleInit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
src/src/components/icons/chevron-down.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
export const ChevronDownIcon = (props: React.ComponentProps<typeof ChevronDown>) => {
|
||||
return <ChevronDown {...props} />;
|
||||
};
|
||||
1
src/src/components/icons/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { ChevronDownIcon } from "@/components/icons/chevron-down";
|
||||
78
src/src/components/projects/project-card.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React from "react"
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
interface ProjectCardProps {
|
||||
project: CollectionEntry<"projects">;
|
||||
}
|
||||
|
||||
export function ProjectCard({ project }: ProjectCardProps) {
|
||||
const hasLinks = project.data.githubUrl || project.data.demoUrl;
|
||||
|
||||
return (
|
||||
<article className="group relative h-full">
|
||||
<a
|
||||
href={`/projects/${project.slug}`}
|
||||
className="block rounded-lg border-2 border-foreground/20
|
||||
hover:border-blue transition-all duration-300
|
||||
bg-background overflow-hidden h-full flex flex-col"
|
||||
>
|
||||
<div className="aspect-video w-full border-b border-foreground/20 bg-foreground/5 overflow-hidden flex-shrink-0">
|
||||
{project.data.image ? (
|
||||
<img
|
||||
src={project.data.image}
|
||||
alt={`${project.data.title} preview`}
|
||||
className="w-full h-full object-cover object-center group-hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-foreground/30">
|
||||
<span className="text-sm">No preview available</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-4 sm:p-6 space-y-3 flex flex-col flex-grow">
|
||||
<h3 className="text-lg sm:text-xl font-bold group-hover:text-blue transition-colors">
|
||||
{project.data.title}
|
||||
</h3>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.data.techStack.map(tech => (
|
||||
<span key={tech} className="text-xs px-2 py-1 rounded-full bg-purple-bright/10 text-purple-bright">
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-foreground/70 text-sm sm:text-base flex-grow">
|
||||
{project.data.description}
|
||||
</p>
|
||||
|
||||
{hasLinks && (
|
||||
<div className="flex gap-4 pt-3 border-t border-foreground/10 mt-auto">
|
||||
{project.data.githubUrl && (
|
||||
<a
|
||||
href={project.data.githubUrl}
|
||||
className="text-sm text-blue hover:text-blue-bright
|
||||
transition-colors z-10"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
View Source
|
||||
</a>
|
||||
)}
|
||||
{project.data.demoUrl && (
|
||||
<a
|
||||
href={project.data.demoUrl}
|
||||
className="text-sm text-green hover:text-green-bright
|
||||
transition-colors z-10"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Live Link
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
49
src/src/components/projects/project-list.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import { ProjectCard } from "@/components/projects/project-card";
|
||||
|
||||
interface ProjectListProps {
|
||||
projects: CollectionEntry<"projects">[];
|
||||
}
|
||||
|
||||
export function ProjectList({ projects }: ProjectListProps) {
|
||||
const latestProjects = projects.slice(0, 3);
|
||||
const otherProjects = projects.slice(3);
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-6xl mx-auto pt-24 sm:pt-32">
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-blue mb-12 text-center px-4 leading-relaxed">
|
||||
Here's what I've been <br className="sm:hidden" />
|
||||
building lately
|
||||
</h1>
|
||||
|
||||
<div className="px-4 mb-16">
|
||||
<h2 className="text-xl font-bold text-foreground/90 mb-6">
|
||||
Featured Projects
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 auto-rows-fr justify-items-center">
|
||||
{latestProjects.map(project => (
|
||||
<div key={project.slug} className="w-full max-w-md">
|
||||
<ProjectCard project={project} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{otherProjects.length > 0 && (
|
||||
<div className="px-4 pb-8">
|
||||
<h2 className="text-xl font-bold text-foreground/90 mb-6">
|
||||
All Projects
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 auto-rows-fr justify-items-center">
|
||||
{otherProjects.map(project => (
|
||||
<div key={project.slug} className="w-full max-w-md">
|
||||
<ProjectCard project={project} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
270
src/src/components/resume/index.tsx
Normal file
@@ -0,0 +1,270 @@
|
||||
import React from "react";
|
||||
import {
|
||||
FileDown,
|
||||
Github,
|
||||
Linkedin,
|
||||
Globe
|
||||
} from "lucide-react";
|
||||
|
||||
const resumeData = {
|
||||
name: "Timothy Pidashev",
|
||||
title: "Software Engineer",
|
||||
contact: {
|
||||
email: "contact@timmypidashev.dev",
|
||||
phone: "+1 (360) 409-0357",
|
||||
location: "Camas, WA",
|
||||
linkedin: "linkedin.com/in/timothy-pidashev-4353812b8/",
|
||||
github: "github.com/timmypidashev"
|
||||
},
|
||||
summary: "Experienced software engineer with a passion for building scalable web applications and solving complex problems. Specialized in React, TypeScript, and modern web technologies.",
|
||||
experience: [
|
||||
{
|
||||
title: "Office Manager & Tutor",
|
||||
company: "FHCC",
|
||||
location: "Ridgefield, WA",
|
||||
period: "2020 - Present",
|
||||
achievements: [
|
||||
"Tutored Python, JavaScript, and HTML to students in grades 4-10, successfully fostering early programming skills",
|
||||
"Designed and deployed a full-stack CRUD application to manage organizational operations",
|
||||
"Engineered and implemented building-wide networking infrastructure and managed multiple service deployments",
|
||||
"Maintained student records and administrative paperwork."
|
||||
]
|
||||
}
|
||||
],
|
||||
contractWork: [
|
||||
{
|
||||
title: "Revive Auto Parts",
|
||||
type: "Full-Stack Development & Maintenance",
|
||||
startDate: "2024",
|
||||
url: "https://reviveauto.parts",
|
||||
responsibilities: [
|
||||
"Maintain and optimize website performance and security",
|
||||
"Implement new features and functionality as needed",
|
||||
"Provide 24/7 monitoring and emergency support"
|
||||
],
|
||||
achievements: [
|
||||
"Designed and built the entire application from the ground up, including auth",
|
||||
"Engineered a tagging system to optimize search results by keywords and relativity",
|
||||
"Implemented a filter provider to further narrow down search results and enchance the user experience",
|
||||
"Created a smooth and responsive infinitely scrollable listings page",
|
||||
"Automated deployment & testing processes reducing downtime by 60%"
|
||||
]
|
||||
}
|
||||
],
|
||||
education: [
|
||||
{
|
||||
degree: "B.S. Computer Science",
|
||||
school: "Clark College",
|
||||
location: "Vancouver, WA",
|
||||
period: "Graduating 2026",
|
||||
achievements: []
|
||||
}
|
||||
],
|
||||
skills: {
|
||||
technical: ["JavaScript", "TypeScript", "React", "Node.js", "Python", "SQL", "AWS", "Docker", "C", "Go", "Rust"],
|
||||
soft: ["Leadership", "Problem Solving", "Communication", "Agile Methodologies"]
|
||||
},
|
||||
certifications: [
|
||||
{
|
||||
name: "AWS Certified Solutions Architect",
|
||||
issuer: "Amazon Web Services",
|
||||
date: "2022"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const Resume = () => {
|
||||
const handleDownloadPDF = () => {
|
||||
window.open("/timothy-pidashev-resume.pdf", "_blank");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-6 md:px-8 pt-24 pb-16">
|
||||
<div className="space-y-16">
|
||||
{/* Header */}
|
||||
<header className="text-center space-y-6">
|
||||
<h1 className="text-6xl font-bold text-aqua-bright">{resumeData.name}</h1>
|
||||
<h2 className="text-3xl text-foreground/80">{resumeData.title}</h2>
|
||||
<div className="flex flex-col md:flex-row justify-center gap-4 md:gap-6 text-foreground/60 text-lg">
|
||||
<a href={`mailto:${resumeData.contact.email}`} className="hover:text-foreground transition-colors duration-200">
|
||||
{resumeData.contact.email}
|
||||
</a>
|
||||
<span className="hidden md:inline">•</span>
|
||||
<a href={`tel:${resumeData.contact.phone}`} className="hover:text-foreground transition-colors duration-200">
|
||||
{resumeData.contact.phone}
|
||||
</a>
|
||||
<span className="hidden md:inline">•</span>
|
||||
<span>{resumeData.contact.location}</span>
|
||||
</div>
|
||||
<div className="flex justify-center items-center gap-6 text-lg">
|
||||
<a href={`https://${resumeData.contact.github}`}
|
||||
target="_blank"
|
||||
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
|
||||
>
|
||||
<Github size={18} />
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
<a href={`https://${resumeData.contact.linkedin}`}
|
||||
target="_blank"
|
||||
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
|
||||
>
|
||||
<Linkedin size={18} />
|
||||
LinkedIn
|
||||
</a>
|
||||
<button
|
||||
onClick={handleDownloadPDF}
|
||||
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
|
||||
>
|
||||
<FileDown size={18} />
|
||||
Resume
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Summary */}
|
||||
<section className="space-y-4">
|
||||
<h3 className="text-3xl font-bold text-yellow-bright">Professional Summary</h3>
|
||||
<p className="text-xl leading-relaxed">{resumeData.summary}</p>
|
||||
</section>
|
||||
|
||||
{/* Experience */}
|
||||
<section className="space-y-8">
|
||||
<h3 className="text-3xl font-bold text-yellow-bright">Experience</h3>
|
||||
{resumeData.experience.map((exp, index) => (
|
||||
<div key={index} className="space-y-4">
|
||||
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
|
||||
<div>
|
||||
<h4 className="text-2xl font-semibold text-green-bright">{exp.title}</h4>
|
||||
<div className="text-foreground/60 text-lg">{exp.company} - {exp.location}</div>
|
||||
</div>
|
||||
<div className="text-foreground/60 text-lg font-medium">{exp.period}</div>
|
||||
</div>
|
||||
<ul className="list-disc pl-6 space-y-3">
|
||||
{exp.achievements.map((achievement, i) => (
|
||||
<li key={i} className="text-lg leading-relaxed">{achievement}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{/* Contract Work */}
|
||||
<section className="space-y-8">
|
||||
<h3 className="text-3xl font-bold text-yellow-bright">Contract Work</h3>
|
||||
{resumeData.contractWork.map((project, index) => (
|
||||
<div key={index} className="space-y-4">
|
||||
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h4 className="text-2xl font-semibold text-green-bright">{project.title}</h4>
|
||||
{project.url && (
|
||||
<a
|
||||
href={project.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-bright hover:text-blue transition-colors duration-200 opacity-60 hover:opacity-100"
|
||||
aria-label={`Visit ${project.title}`}
|
||||
>
|
||||
<Globe size={16} strokeWidth={1.5} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-foreground/60 text-lg">{project.type}</div>
|
||||
</div>
|
||||
<div className="text-foreground/60 text-lg font-medium">Since {project.startDate}</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{project.responsibilities && (
|
||||
<div>
|
||||
<h5 className="text-lg text-aqua font-semibold mb-2">Responsibilities</h5>
|
||||
<ul className="list-disc pl-6 space-y-3">
|
||||
{project.responsibilities.map((responsibility, i) => (
|
||||
<li key={i} className="text-lg leading-relaxed">{responsibility}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{project.achievements && (
|
||||
<div>
|
||||
<h5 className="text-lg text-aqua font-semibold mb-2">Key Achievements</h5>
|
||||
<ul className="list-disc pl-6 space-y-3">
|
||||
{project.achievements.map((achievement, i) => (
|
||||
<li key={i} className="text-lg leading-relaxed">{achievement}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{/* Education */}
|
||||
<section className="space-y-8">
|
||||
<h3 className="text-3xl font-bold text-yellow-bright">Education</h3>
|
||||
{resumeData.education.map((edu, index) => (
|
||||
<div key={index} className="space-y-4">
|
||||
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
|
||||
<div>
|
||||
<h4 className="text-2xl font-semibold text-green-bright">{edu.degree}</h4>
|
||||
<div className="text-foreground/60 text-lg">{edu.school} - {edu.location}</div>
|
||||
</div>
|
||||
<div className="text-foreground/60 text-lg font-medium">{edu.period}</div>
|
||||
</div>
|
||||
<ul className="list-disc pl-6 space-y-3">
|
||||
{edu.achievements.map((achievement, i) => (
|
||||
<li key={i} className="text-lg leading-relaxed">{achievement}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{/* Skills */}
|
||||
<section className="space-y-8">
|
||||
<h3 className="text-3xl font-bold text-yellow-bright">Skills</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-2xl font-semibold text-green-bright">Technical Skills</h4>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{resumeData.skills.technical.map((skill, index) => (
|
||||
<span key={index}
|
||||
className="border border-foreground/20 px-4 py-2 rounded-lg hover:border-foreground/40 transition-colors duration-200">
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-2xl font-semibold text-green-bright">Soft Skills</h4>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{resumeData.skills.soft.map((skill, index) => (
|
||||
<span key={index}
|
||||
className="border border-foreground/20 px-4 py-2 rounded-lg hover:border-foreground/40 transition-colors duration-200">
|
||||
{skill}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Certifications */}
|
||||
{/* Temporarily Hidden
|
||||
<section className="space-y-6 mb-16">
|
||||
<h3 className="text-3xl font-bold text-yellow-bright">Certifications</h3>
|
||||
{resumeData.certifications.map((cert, index) => (
|
||||
<div key={index} className="space-y-2">
|
||||
<h4 className="text-2xl font-semibold text-green-bright">{cert.name}</h4>
|
||||
<div className="text-foreground/60 text-lg">{cert.issuer} - {cert.date}</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Resume;
|
||||
59
src/src/content/blog/components/my-first-post/cookie.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useState } from "react";
|
||||
|
||||
const Cookie = () => {
|
||||
const [hasBite, setHasBite] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
setHasBite((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
aria-label="Click for a cookie bite"
|
||||
className="inline-flex items-center justify-center p-0 bg-transparent hover:scale-110 transition-transform"
|
||||
style={{
|
||||
verticalAlign: "middle",
|
||||
lineHeight: 0, // Ensure the button doesn't affect the text spacing
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 64 64"
|
||||
className="w-6 h-6" // Adjust size to align better with text
|
||||
fill="none"
|
||||
>
|
||||
{/* Base cookie */}
|
||||
<circle cx="32" cy="32" r="30" fill="#D2691E" />
|
||||
|
||||
{/* Bite area: uses a mask */}
|
||||
<mask id="biteMask">
|
||||
{/* Full circle mask */}
|
||||
<rect width="64" height="64" fill="white" />
|
||||
{hasBite && <circle cx="48" cy="16" r="12" fill="black" />}
|
||||
</mask>
|
||||
|
||||
{/* Cookie with bite mask applied */}
|
||||
<circle cx="32" cy="32" r="30" fill="#D2691E" mask="url(#biteMask)" />
|
||||
|
||||
{/* Chocolate chips */}
|
||||
<circle cx="20" cy="24" r="3" fill="#3E2723" />
|
||||
<circle cx="40" cy="18" r="3" fill="#3E2723" />
|
||||
<circle cx="28" cy="40" r="3" fill="#3E2723" />
|
||||
<circle cx="44" cy="36" r="3" fill="#3E2723" />
|
||||
<circle cx="24" cy="48" r="3" fill="#3E2723" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<p className="text-lg">
|
||||
On your way out, don't forget to grab a cookie!{" "}
|
||||
<Cookie />
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
25
src/src/content/blog/my-first-post.mdx
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: My First Post!
|
||||
description: A quick introduction
|
||||
author: Timothy Pidashev
|
||||
tags: [greeting]
|
||||
date: 2025-01-09
|
||||
image: "/blog/my-first-post/thumbnail.png"
|
||||
imagePosition: "center 30%"
|
||||
---
|
||||
|
||||
import Cookie from "@/content/blog/components/my-first-post/cookie";
|
||||
|
||||
Hello, my name is Timothy Pidashev! I’m an aspiring software engineer currently expanding my technical horizons!
|
||||
|
||||
Wait… that was a bit too formal. This is, after all, a blog post. Let me try that again.
|
||||
|
||||
Hey 👋, my name is Timothy! I’m a software nerd who loves building cool tech! Phew, that was rough. Jokes aside,
|
||||
I’m happy you're here, dear reader. I have a lot planned for my little blog, and I hope the next few posts will
|
||||
be useful to many nerds out there!
|
||||
|
||||
As a little spoiler of what’s to come, the next post will be a complete dive into modifying and corebooting a
|
||||
Thinkpad T440p for the ultimate secure dev laptop! I’ve been personally daily-driving mine for the last two
|
||||
years and can’t wait to show it off, so stay tuned!
|
||||
|
||||
<Cookie />
|
||||
28
src/src/content/config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { defineCollection, z } from "astro:content";
|
||||
|
||||
export const collections = {
|
||||
blog: defineCollection({
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
author: z.string(),
|
||||
tags: z.array(z.string()),
|
||||
date: z.coerce.date().transform((date) => {
|
||||
return new Date(date.setUTCHours(12, 0, 0, 0));
|
||||
}),
|
||||
image: z.string().optional(),
|
||||
imagePosition: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
projects: defineCollection({
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
githubUrl: z.string().url().optional(),
|
||||
demoUrl: z.string().url().optional(),
|
||||
techStack: z.array(z.string()),
|
||||
date: z.string(),
|
||||
image: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
42
src/src/content/projects/darkbox.mdx
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: "Darkbox"
|
||||
description: "My gruvbox theme, with a pure black background"
|
||||
githubUrl: "https://github.com/timmypidashev/darkbox.nvim"
|
||||
techStack: ["Neovim", "Lua"]
|
||||
date: "2025-01-05"
|
||||
image: "/projects/darkbox/thumbnail.jpeg"
|
||||
---
|
||||
|
||||
## Overview
|
||||
My gruvbox theme, with a pure black background
|
||||
|
||||
## Key Features
|
||||
* **Pure Black Background**: Preventing eyestrain during prolonged screen time.
|
||||
|
||||
* **Gruvbox Inspired**: Only the very best warm colorscheme, revamped for contrast.
|
||||
|
||||
* **Neovim First**: First class support for neovim, with treesitter support built in.
|
||||
|
||||
## Development Highlights
|
||||
First, this project could not have been possible without the help of previous theme projects
|
||||
such as [gruvbox.nvim](https://github.com/ellisonleao/gruvbox.nvim). Major kudos to everyone
|
||||
who helped write that plugin. Using existing code as reference from this project dramatically
|
||||
sped up development and made matters easy.
|
||||
|
||||
On top of that, working with lua was delightful, and there were no roadblocks or glaring
|
||||
issues which prevented me from completion. Neovim's tight integration with lua is a game
|
||||
changer compared to something like [VimScript](https://vimdoc.sourceforge.net/htmldoc/usr_41.html).
|
||||
Overall, I found the whole experience simply perfect.
|
||||
|
||||
## Challenges & Roadblocks
|
||||
One thing I am not proud of, is how contrast has turned out. At the moment, the gruvbox dark
|
||||
colorscheme that inspired darkbox, isn't taking into account the contrast change from the
|
||||
background `#282828` to `#000000`. This, can show up as a bit washed out to the careful eye in certain
|
||||
code blocks where multiple colors clash together. I intend to work on this in a future release.
|
||||
The fix will be to darken the color contrast to more closely match to the pure black background.
|
||||
|
||||
## Summary
|
||||
This is it! This is the theme I am going to use for most likely the rest of my programming life.
|
||||
I have always had a gruvbox themed desktop, but no background image. This resulted in an interesting
|
||||
combo, and where the spark for this idea came from. I'm a bit biased, but I love it and hope
|
||||
someone else does too. The github link can be found [here](https://github.com/timmypidashev/darkbox.nvim) :D
|
||||
49
src/src/content/projects/fhccenter.mdx
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: "Fhccenter"
|
||||
description: "Website made for a private school"
|
||||
demoUrl: "https://fhccenter.org"
|
||||
techStack: ["Nextjs", "Typescript", "Prisma"]
|
||||
date: "2024-10-03"
|
||||
image: "/projects/fhccenter/thumbnail.jpeg"
|
||||
---
|
||||
|
||||
## Overview
|
||||
A private learning center website that provides information about the organization,
|
||||
student registrations, a newsletter, and full admin login with administrative tools.
|
||||
|
||||
## Key Features
|
||||
* **Server Components**: React Server Components power efficient data flow and server-side
|
||||
rendering.
|
||||
|
||||
* **JWT Authentication**: Built using a custom authentication provider, the entire site is
|
||||
secured with a jwt token schema, including refresh tokens.
|
||||
|
||||
* **Elegant Components**: Constructed using Shadcn and Framer motion, component animations
|
||||
and feel are seamless.
|
||||
|
||||
## Development Highlights
|
||||
Building this site was quite an interesting feat. This was my first foray into react server
|
||||
components, and while they aren't my preferred method of building applications, It was a great
|
||||
experience to fully dive in and make use of them. This was also my first time using Nextjs's
|
||||
new app router, and coming from the page router I really liked the experience. Building out
|
||||
new pages and components was just a pure flow of thought, and I found myself trying to keep up
|
||||
with the ideas coming out of my head rather than coming upon development barriers or softblocks.
|
||||
|
||||
## Challenges & Roadblocks
|
||||
One of the only real challenges I didn't anticipate was caching. In Nextjs 14, caching is handled
|
||||
very poorly, and I found myself struggling to figure out workarounds when handling dynamic data
|
||||
such as the newsletter on the site. Full cache routes were extremely tricky to understand, but
|
||||
after reading some troubleshooting guides it was full sailing ahead.
|
||||
|
||||
Another tricky but fun challenge was implementing authentication using react server components.
|
||||
Since I had never used them up to this point, going about setting up a jwt schema while also trying
|
||||
to ensure api routes don't accidentally leak was tricky, and something I still to this day don't really
|
||||
like when it comes to server components. The fact that a single line of code is responsible for separating
|
||||
server from client with nothing preventing a dev from putting all those components wherever they please can
|
||||
cause quite a mess, so following good file organization conventions was instrumental.
|
||||
|
||||
## Summary
|
||||
Overall, I had a pleasant time working on [fhccenter](https://fhccenter.org). After learning all the new
|
||||
features like server components and the app router, I spent more time messing around with css styling and
|
||||
getting the site to look exactly the way I wanted it rather than debugging silly mistakes, which was
|
||||
refreshing coming from other frameworks.
|
||||
58
src/src/content/projects/iridescent.mdx
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: "Iridescent"
|
||||
description: "An open-source graphics engine"
|
||||
githubUrl: "https://github.com/timmypidashev/iridescent"
|
||||
techStack: ["Cmake", "Glad", "Imgui"]
|
||||
date: "2024-05-03"
|
||||
image: "/projects/iridescent/thumbnail.jpeg"
|
||||
---
|
||||
|
||||
## Overview
|
||||
A open-source graphics engine concept created for my highschool senior project.
|
||||
Built to expand my understanding of the low level programming world, and further
|
||||
my reach of graphics and c++ programming.
|
||||
|
||||
## Key Features
|
||||
* **Dockable Windows**: A quality of life feature utilizing imgui which allows
|
||||
to efficiently use monitor space, moving logging and certain tools into their own
|
||||
windows outside of the engines viewspace.
|
||||
|
||||
* **Layer Stack**: Deterministic and fast stack which determines the order in which
|
||||
all layers of the application are drawn, from the ui all the way to the polygons making
|
||||
up the scene.
|
||||
|
||||
* **Input Polling**: A realtime implementation polling all input events on the keyboard
|
||||
and mouse for an extremely low latency between input events and scene events.
|
||||
|
||||
* **Detailed Logging**: A detailed logging system which logs events from the scene all the
|
||||
way down to the system.
|
||||
|
||||
* **Shading**: The crème de la crème of the renderer, adding shading to the entire scene.
|
||||
|
||||
## Development Highlights
|
||||
Some of the best highlights for me during this project was quite literally every time I managed
|
||||
to fix a compilation issue. The exhileration after each successful step forward was just mesmerizing,
|
||||
and I knew the moment I began my obsession wouldn't end until the engine was in a complete state.
|
||||
Since I was going into this blind during my senior year at highschool, I knew almost nothing about
|
||||
c++ or the build systems that accompany it, so learning all of that at once was both a breath of fresh
|
||||
air and a frustration. However, after several days of work, I became very comfortable with cmake, my build
|
||||
system of choice, and most basic concepts of c++, allowing me to begin working on the engine itself.
|
||||
It was at this point where I started hitting some real knowledge barriers and ended up going to multiple
|
||||
resources to learn and understand the inner workings of a graphics renderer. Resources such as the
|
||||
[Hazel Engine Series](https://www.youtube.com/watch?v=JxIZbV_XjAs&list=PLlrATfBNZ98dC-V-N3m0Go4deliWHPFwT),
|
||||
[Learn C++ Website](https://www.learncpp.com/learn-cpp-site-index/), and [OpenGL Tutorials](https://www.opengl.org/sdk/docs/tutorials/)
|
||||
helped me tremendously here.
|
||||
|
||||
## Challenges & Roadblocks
|
||||
Some of the biggest challenges I faced was overcoming the knowledge barrier I had on the inner workings
|
||||
of graphics rendering and low level programming overall. Thankfuly, being stubborn and obsessed can
|
||||
sometimes help, and in this case after numerous attempts, I began meticoulouly learning every aspect
|
||||
of the engine as I developed it, and over time it began paying off, snowballing into a very fun experience.
|
||||
Some food for thought, after you overcome the biggest barrier in your journey, the rest seems laughably easy.
|
||||
|
||||
## Summary
|
||||
Looking back, working on Iridescent was some of the most fun I have ever had programming as of yet,
|
||||
and I am still craving for the next project to top this one. It was only by working on this engine
|
||||
that I realized just how much I love working on the little details, obsessing over each little system
|
||||
in the engine. Definitely something I will have to do again, maybe write a little game, but thats for
|
||||
future me to decide :D
|
||||
179
src/src/content/projects/reviveauto.mdx
Normal file
@@ -0,0 +1,179 @@
|
||||
---
|
||||
title: "Revive Auto Parts"
|
||||
description: "A car parts listing site built for a client"
|
||||
demoUrl: "https://reviveauto.parts"
|
||||
techStack: ["Tanstack", "React Query", "Fastapi"]
|
||||
date: "2025-01-04"
|
||||
image: "/projects/reviveauto/thumbnail.jpeg"
|
||||
---
|
||||
|
||||
## Overview
|
||||
A car parts listing website built to provide an intuitive and efficient experience
|
||||
for users searching for automotive parts. This project was commissioned by a client
|
||||
and showcases some pretty cool modern web technologies, enabling excellent
|
||||
performance and a clean user interface.
|
||||
|
||||
## Key Features
|
||||
* **Dynamic & Simple Routing**: Powered by TanStack Router, enabling seamless navigation
|
||||
and deep-linking for product categories and details.
|
||||
|
||||
* **Real-Time Data Fetching**: Utilized React Query to handle server state and caching,
|
||||
ensuring users have up-to-date information.
|
||||
|
||||
* **Infinite Scroll**: Implemented React Query's infinite scrolling and memoization
|
||||
optimization techniques to ensure fast and seamless scrolling through listings.
|
||||
|
||||
* **Dynamic Filters**: Created dynamic filters which are editable on the admin panel,
|
||||
allowing for a high level of customization.
|
||||
|
||||
* **Responsive Design**: Built with Tailwind and Shadcn for a fully responsive layout,
|
||||
providing a consistent experience across desktop and mobile devices.
|
||||
|
||||
* **Optimized Performance**: Leveraged Vite for fast builds and optimized code-splitting,
|
||||
improving page load times drastically.
|
||||
|
||||
## Development Highlights
|
||||
I had numerous highlights and *aha* moments when developing this site. One of these has to
|
||||
be the site layout, built with [shadcn/ui](https://ui.shadcn.com) components. I had used
|
||||
this component library in a previous site, but I had yet to grasp just how powerful this
|
||||
collection of components is. We truly do stand on the shoulders of giants, and using this
|
||||
library not only allowed me to very quickly prototype a design, but to then flesh it out
|
||||
without having to dive into the weeds of UI development.
|
||||
|
||||
Another great highlight has to be [Tanstack Router](https://tanstack.com/router/latest).
|
||||
As a seasoned developer, I have had many opportunities to try a lot of different routers
|
||||
across several frameworks. As many have before me, we stumble onto the nextjs router, and
|
||||
tend not to look back. However, tanstack did something I did not expect, and it takes routing
|
||||
to the next level. With TanStack, the routes folder is solely focused on defining routes and
|
||||
layouts, providing a cleaner, more modular structure. This is a stark contrast to Next.js's
|
||||
approach, where the app directory can quickly become convoluted by mixing route definitions
|
||||
with server-side logic, API calls, and other concerns. Anybody who has built a Nextjs project
|
||||
bigger than a To-Do app can likely relate to the mental pain that is trying to find a route or
|
||||
endpoint in your app router when its nested and hidden away four or five directories deep.
|
||||
|
||||
Another memorable highlight was writing my backend in Python using [Fastapi](https://fastapi.tiangolo.com/).
|
||||
Sometimes, a project doesn't need a complex nodejs runtime, or an ORM built for a massive service.
|
||||
As a python enthusiast, I found the combination of fastapi & sqlmodel to be just perfect for this project,
|
||||
and defining api endpoints and schemas were quite enjoyable. As I do, I decided to roll my own authentication,
|
||||
and found Python to be a great environment in which to do so.
|
||||
|
||||
Lastly, I have to touch on React Query. The combo that is React Query and Fastapi can truly be magical.
|
||||
To truly showcase what I mean, here's an example of a query and endpoint working together:
|
||||
|
||||
```typescript
|
||||
// features/auth/services/login/queries.ts
|
||||
export function useLogin() {
|
||||
const { setCredentials } = useUserStore();
|
||||
|
||||
return useMutation<LoginResponseSchemaType, AxiosError<BackendError>, LoginRequestSchemaType>({
|
||||
mutationFn: user => login(user),
|
||||
onSuccess: data => {
|
||||
setCredentials({
|
||||
accessToken: data.access_token,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const login = backend<z.infer<typeof LoginRequestSchema>, z.infer<typeof LoginResponseSchema>>({
|
||||
method: "POST",
|
||||
path: `${BACKEND_URL}${AUTH}/login`,
|
||||
requestSchema: LoginRequestSchema,
|
||||
responseSchema: LoginResponseSchema,
|
||||
type: "public",
|
||||
});
|
||||
```
|
||||
```typescript
|
||||
// features/auth/services/login/schemas
|
||||
import { z } from "zod";
|
||||
|
||||
export const LoginRequestSchema = z.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, "Email is required!")
|
||||
.trim()
|
||||
.email({ message: "Invalid email!" })
|
||||
.toLowerCase(),
|
||||
password: z.string().trim().min(8, { message: "Password must be at least 8 characters long!" }),
|
||||
});
|
||||
export type LoginRequestSchemaType = z.infer<typeof LoginRequestSchema>;
|
||||
|
||||
export const LoginResponseSchema = z.object({
|
||||
access_token: z.string(),
|
||||
});
|
||||
export type LoginResponseSchemaType = z.infer<typeof LoginResponseSchema>;
|
||||
```
|
||||
|
||||
```python
|
||||
# routes/auth.py
|
||||
class LoginResponse(BaseModel):
|
||||
access_token: str
|
||||
|
||||
@router.post(
|
||||
"/login",
|
||||
description=
|
||||
"""
|
||||
The login endpoint.
|
||||
|
||||
Used to create access and refresh tokens for a user.
|
||||
The refresh token is set as an HTTP-only cookie.
|
||||
""",
|
||||
summary="Create access token and set refresh token as HTTP-only cookie."
|
||||
)
|
||||
async def login(
|
||||
response: Response,
|
||||
form_data: LoginSchema = Body(...),
|
||||
session: Session = Depends(get_session)
|
||||
) -> Any:
|
||||
|
||||
# Fetch user using the form_data sent by the client
|
||||
user = await service.authenticate(
|
||||
email=form_data.email,
|
||||
password=form_data.password,
|
||||
session=session
|
||||
)
|
||||
|
||||
# If service returns None, raise exception
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password",
|
||||
)
|
||||
|
||||
# Set refresh token as HTTP-only cookie
|
||||
response.set_cookie(
|
||||
key="refresh_token",
|
||||
value=create_token(user, type="refresh"),
|
||||
httponly=True,
|
||||
path="/auth/refresh",
|
||||
domain=Config.JWT_COOKIE_DOMAIN,
|
||||
expires=datetime.now(timezone.utc) + Config.JWT_REFRESH_EXPIRES
|
||||
)
|
||||
|
||||
return LoginResponse(
|
||||
access_token=create_token(user, type="access")
|
||||
)
|
||||
```
|
||||
Using Zod, the frontend is validating what a user is attempting to submit
|
||||
before calling the endpoint. If Zod does not throw an error, our frontend will
|
||||
call our backend endpoint, which also expects the right types to be present.
|
||||
Afterwards, the backend will respond back with a response that our frontend
|
||||
will also validate. This allows for a complete multi-directional
|
||||
request -> response type validation!
|
||||
|
||||
## Challenges & Roadblocks
|
||||
For the most part, there were really no challenges, say for some hiccups here and there.
|
||||
Probably the most painful parts were creating unit tests for the frontend, and scraping
|
||||
Facebook for a total of 1,384 posts for my client, who wanted the posts imported over.
|
||||
As one can imagine, that process is not simple to do manually by hand, so I wrote multiple
|
||||
python scripts using the seleneum library to fetch the posts from the sellers account, a process
|
||||
which took multiple attempts, several overnight scrapes, and lots of data sanitation afterwards.
|
||||
Other than that, everything else was an absolute joy to work on, and I finished the project in under
|
||||
15K LOC.
|
||||
|
||||
## Summary
|
||||
Sometimes, building a CRUD app can be lots of fun, no matter how many times you've done it before.
|
||||
All it takes is adding something new to the stack, and striving to improve the code you write. This
|
||||
project was exactly that, a mix of new technologies working together to power a fairly neat site,
|
||||
which can be viewed [here](https://reviveauto.parts). Maybe there is a car part there which the reader
|
||||
might need. As always, thanks for the read :)
|
||||
36
src/src/content/projects/web.mdx
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: "Website"
|
||||
description: "My personal website and blog"
|
||||
githubUrl: "https://github.com/timmypidashev/web"
|
||||
demoUrl: "https://timmypidashev.dev"
|
||||
techStack: ["Astro", "Typescript", "MDX"]
|
||||
date: "2024-10-03"
|
||||
image: "/projects/web/thumbnail.jpeg"
|
||||
---
|
||||
|
||||
## Overview
|
||||
A portfolio website and resume for yours truly.
|
||||
|
||||
## Development Highlights
|
||||
Building the second version of my website was a dream. This was my first time using Astro,
|
||||
and wow, just wow! For starters, The entire site is ~1,500 LOC (excluding MDX). That is
|
||||
small for the amount of things I was able to accomplish. That aside, Astro's simplicity is
|
||||
unmatched. Coming from React-based frameworks, the ability to just import react components
|
||||
and have them work out of the box was unexpected. And the astro layout files! They are so
|
||||
easy to look at, it just turns on some kind of code ocd within my head. I can't stop admiring
|
||||
it. All this to say, Astro was the perfect choice for my website.
|
||||
|
||||
## Challenges & Roadblocks
|
||||
Although the development process was extremely simple and fun, there really were no challenges,
|
||||
which allowed me to express my creativity to my fullest. Building out Conway's game of life as
|
||||
a canvas background simulation wasn't the first idea I had, and that whole endeavour started as a
|
||||
growing vines simulation similar to that of the old Poptropica flash game loading screens, all the way
|
||||
to a full fishtank simulation like Asciiquarium. But, they just never felt final to me, and this simulation
|
||||
was simple to implement and most importantly very lightweight compared to the other simulations I was
|
||||
working on.
|
||||
|
||||
## Summary
|
||||
The result is deeply satisfying - a personal website that authentically reflects my style.
|
||||
It captures my minimalist desktop aesthetic, down to the font choices and clean layout.
|
||||
After many hours of careful work, I am very proud of what I have created for myself and hope
|
||||
the reader agrees :)
|
||||
38
src/src/content/thinkpad-t440p-coreboot-guide.mdx
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
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"
|
||||
---
|
||||
|
||||
> **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.
|
||||
> Simply run the following command in your terminal to get started:
|
||||
>
|
||||
> ```
|
||||
> curl -fsSL https://timmypidashev.dev/scripts/run.sh | sh -s -- -t coreboot-t440p
|
||||
> ```
|
||||
> NOTE: This script supports Arch, Debian, Fedora, Gentoo, and Nix linux distributions!
|
||||
|
||||
## 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.
|
||||
|
||||
## 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
|
||||
86
src/src/content/thinkpad-t440p-modification-guide.mdx
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
title: Thinkpad T440p Modification Guide
|
||||
description: You purchased a T440p, now what?
|
||||
author: Timothy Pidashev
|
||||
tags: [t440p, mods, coreboot, thinkpad]
|
||||
date: 2025-01-15
|
||||
image: "/blog/thinkpad-t440p-modification-guide/thumbnail.png"
|
||||
---
|
||||
|
||||
## The T440p
|
||||
|
||||
Whether for privacy related reasons, coreboot, or someones advice on the internet,
|
||||
you are now the proud owner of a T440p. Now what? Well, I have been daily driving
|
||||
this laptop for over two years now, and would like to share my knowledge on this
|
||||
lovely machine. If followed properly, this guide should help any privacy seeking
|
||||
individual or programmer how to setup the "reasonably" perfect T440p.
|
||||
|
||||
## Buying the Right Model
|
||||
|
||||
Although the T440p comes in various configurations and specs, when searching for
|
||||
one online there are two things to consider.
|
||||
|
||||
1. Online Marketplace
|
||||
* Purchasing from the right marketplace is important to consider, and while trusted
|
||||
vendors like Amazon might be preferred, consider Ebay or AliExpress.
|
||||
|
||||
* I personally have only purchased my thinkpad's on Ebay, as there are generally more listings
|
||||
available from companies reselling retired units, usually at a steep discount.
|
||||
|
||||
|
||||
2. Dedicated GPU
|
||||
* The T440p motherboard comes in two different varieties, one with
|
||||
a dGPU and the other without. There is only one dGPU model, which is the NVIDIA GT 730M.
|
||||
Featuring 2GB of VRAM, it will work, however if your looking for longer battery life and
|
||||
an easier coreboot config should you choose to coreboot, I would recommend sticking to
|
||||
a non dGPU variant.
|
||||
|
||||
* Finding a dGPU variant is quite difficult, as many online
|
||||
sellers don't always list the motherboard spec, making things quite the guessing game.
|
||||
When I was shopping for one, my strategy was to purchase the dGPU motherboard on its own,
|
||||
and then a T440p laptop listed with a dead motherboard, as I was going to swap it out anyways.
|
||||
|
||||
3. Quality
|
||||
* Finding the perfect T440p is hard, and you will likely end up purchasing one that looks ok
|
||||
in pictures, but comes with a cracked palmrest or front panel. Consider purchasing one which
|
||||
looks good, and then replacing any cracked or aged parts should you choose to do so in the future.
|
||||
|
||||
* T440p plastics are aged. Although this machine is an absolute brick, which can probably be thrown
|
||||
at the ground without any major damage, it will definitely chip and crack. I myself have replaced my
|
||||
palm rest/keyboard cover thrice, as every half a year or so I will open the laptop in the morning to
|
||||
find that my careless "throw it in the backpack" has finally cracked the palmrest yet again.
|
||||
|
||||
## Screen
|
||||
|
||||
When it comes to the screen, you really don't want to get one of poor quality, especially since the
|
||||
lousy 1366x768 panel is not great nowadays. Generally, I would recommend going for an ips 1080p panel,
|
||||
as this is generally most the most supported. I purchased this panel from amazon for ~$60USD and have
|
||||
never looked back.
|
||||
|
||||
## Keyboard
|
||||
|
||||
## Trackpad
|
||||
|
||||
## Battery
|
||||
|
||||
## CPU
|
||||
|
||||
The T440p has a trick up its sleeve. The processor can be swapped out and replaced, allowing for an upgrade!
|
||||
There are many models out there, however some aren't recommended due to thermal constraints, so finding the
|
||||
right balance can be tough.
|
||||
|
||||
## RAM
|
||||
|
||||
## Storage
|
||||
|
||||
## WLAN
|
||||
|
||||
## WAN
|
||||
|
||||
## MISC
|
||||
|
||||
1. Fingerprint Reader
|
||||
|
||||
2. Disc Reader
|
||||
|
||||
3. Webcam & Microphone
|
||||
9
src/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
|
||||
declare namespace App {
|
||||
interface Locals {
|
||||
user: import("./lib/user").User | null;
|
||||
session: import("./lib/session").Session | null;
|
||||
}
|
||||
}
|
||||
68
src/src/layouts/content.astro
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
import "@/style/globals.css";
|
||||
import { ClientRouter } from "astro:transitions";
|
||||
import Header from "@/components/header";
|
||||
import Footer from "@/components/footer";
|
||||
import Background from "@/components/background";
|
||||
export interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
const { title, description } = Astro.props;
|
||||
const ogImage = "https://timmypidashev.dev/og-image.jpg";
|
||||
---
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<!-- OpenGraph -->
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:image" content={ogImage} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<!-- Basic meta description for search engines -->
|
||||
<meta name="description" content={description} />
|
||||
<!-- Also used in OpenGraph for social media sharing -->
|
||||
<meta property="og:description" content={description} />
|
||||
<link rel="icon" type="image/jpeg" href="/me.jpeg" />
|
||||
<ClientRouter
|
||||
defaultTransition={false}
|
||||
handleFocus={false}
|
||||
/>
|
||||
<style>
|
||||
::view-transition-new(:root) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
::view-transition-old(:root) {
|
||||
animation: 90ms ease-out both fade-out;
|
||||
}
|
||||
@keyframes fade-out {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-foreground min-h-screen flex flex-col">
|
||||
<Header client:load />
|
||||
<main class="flex-1 flex flex-col">
|
||||
<div class="max-w-5xl mx-auto pt-12 px-4 py-8 flex-1">
|
||||
<Background layout="content" position="right" client:only="react" transition:persist />
|
||||
<slot />
|
||||
<Background layout="content" position="left" client:only="react" transition:persist />
|
||||
</div>
|
||||
</main>
|
||||
<div class="mt-auto">
|
||||
<Footer client:load transition:persist />
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener("astro:after-navigation", () => {
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
50
src/src/layouts/index.astro
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
const { content } = Astro.props;
|
||||
|
||||
import "@/style/globals.css";
|
||||
|
||||
import { ClientRouter } from "astro:transitions";
|
||||
|
||||
import Header from "@/components/header";
|
||||
import Footer from "@/components/footer";
|
||||
import Background from "@/components/background";
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const { title, description } = Astro.props;
|
||||
const ogImage = "https://timmypidashev.dev/og-image.jpg";
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<!-- OpenGraph -->
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:image" content={ogImage} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<!-- Basic meta description for search engines -->
|
||||
<meta name="description" content={description} />
|
||||
<!-- Also used in OpenGraph for social media sharing -->
|
||||
<meta property="og:description" content={description} />
|
||||
<link rel="icon" type="image/jpeg" href="/me.jpeg" />
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
<ClientRouter />
|
||||
</head>
|
||||
<body class="bg-background text-foreground">
|
||||
<Header client:load />
|
||||
<main transition:animate="fade">
|
||||
<Background layout="index" client:only="react" transition:persist />
|
||||
<slot />
|
||||
</main>
|
||||
<Footer client:load transition:persist fixed=true />
|
||||
</body>
|
||||
</html>
|
||||
14
src/src/lib/getPostData.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import readingTime from "reading-time";
|
||||
|
||||
type Post = {
|
||||
title: string
|
||||
file: string
|
||||
rawContent: () => string
|
||||
}
|
||||
|
||||
export default function getPostData(post: Post) {
|
||||
return {
|
||||
slug: post.file.split('/').pop().split('.').shift(),
|
||||
readingTime: readingTime(post.rawContent()).text,
|
||||
}
|
||||
}
|
||||
59
src/src/lib/structuredData.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { type Article, type Person, type WebSite, type WithContext } from "schema-dts";
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
|
||||
export const blogWebsite: WithContext<WebSite> = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
url: `${import.meta.env.SITE}/blog/`,
|
||||
name: "Timothy Pidsashev - Blog",
|
||||
description: "Timothy Pidsashev's blog",
|
||||
inLanguage: "en_US",
|
||||
};
|
||||
|
||||
export const mainWebsite: WithContext<WebSite> = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
url: import.meta.env.SITE,
|
||||
name: "Timothy Pidashev - Personal website",
|
||||
description: "Timothy Pidashev's contact page, portfolio and blog",
|
||||
inLanguage: "en_US",
|
||||
};
|
||||
|
||||
export const personSchema: WithContext<Person> = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Person",
|
||||
name: "Timothy Pidashev",
|
||||
url: "https://timmypidashev.dev",
|
||||
sameAs: [
|
||||
"https://github.com/timmypidashev",
|
||||
"https://www.linkedin.com/in/timothy-pidashev-4353812b8",
|
||||
],
|
||||
jobTitle: "Software Engineer",
|
||||
worksFor: {
|
||||
"@type": "Organization",
|
||||
name: "Fathers House Christian Center",
|
||||
url: "https://fhccenter.org",
|
||||
},
|
||||
};
|
||||
|
||||
export function getArticleSchema(post: CollectionEntry<"blog">) {
|
||||
const articleStructuredData: WithContext<Article> = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
headline: post.data.title,
|
||||
url: `${import.meta.env.SITE}/blog/${post.slug}/`,
|
||||
description: post.data.excerpt,
|
||||
datePublished: post.data.date.toString(),
|
||||
publisher: {
|
||||
"@type": "Person",
|
||||
name: "Timothy Pidashev",
|
||||
url: import.meta.env.SITE,
|
||||
},
|
||||
author: {
|
||||
"@type": "Person",
|
||||
name: "Timothy Pidashev",
|
||||
url: import.meta.env.SITE,
|
||||
},
|
||||
};
|
||||
return articleStructuredData;
|
||||
}
|
||||
18
src/src/pages/404.astro
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
import IndexLayout from "@/layouts/index.astro";
|
||||
import GlitchText from "@/components/404/glitched-text";
|
||||
const title = "404 Not Found";
|
||||
---
|
||||
|
||||
<IndexLayout content={{ title: "404 | Timothy Pidashev" }}>
|
||||
<main class="min-h-screen flex flex-col items-center justify-center p-4 text-center">
|
||||
<GlitchText client:only />
|
||||
<p class="text-xl text-orange mb-8">Whoops! This page doesn't exist :(</p>
|
||||
<button
|
||||
onclick="window.history.back()"
|
||||
class="underline text-green hover:opacity-70 transition-opacity"
|
||||
>
|
||||
go back
|
||||
</button>
|
||||
</main>
|
||||
</IndexLayout>
|
||||
40
src/src/pages/about.astro
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
import "@/style/globals.css"
|
||||
import ContentLayout from "@/layouts/content.astro";
|
||||
import Intro from "@/components/about/intro";
|
||||
import AllTimeStats from "@/components/about/stats-alltime";
|
||||
import DetailedStats from "@/components/about/stats-detailed";
|
||||
import Timeline from "@/components/about/timeline";
|
||||
import CurrentFocus from "@/components/about/current-focus";
|
||||
import OutsideCoding from "@/components/about/outside-coding";
|
||||
---
|
||||
<ContentLayout
|
||||
title="About | Timothy Pidashev"
|
||||
description="A software engineer passionate about the web, open source, and building innovative solutions."
|
||||
>
|
||||
<div class="min-h-screen">
|
||||
<section class="h-screen flex items-center justify-center">
|
||||
<Intro client:load />
|
||||
</section>
|
||||
|
||||
<section class="flex items-center justify-center py-16">
|
||||
<AllTimeStats client:only="react" />
|
||||
</section>
|
||||
|
||||
<section class="flex items-center justify-center py-16">
|
||||
<DetailedStats client:only="react" />
|
||||
</section>
|
||||
|
||||
<section class="flex items-center justify-center py-16">
|
||||
<Timeline client:load />
|
||||
</section>
|
||||
|
||||
<section class="flex items-center justify-center py-16">
|
||||
<CurrentFocus client:load />
|
||||
</section>
|
||||
|
||||
<section class="flex items-center justify-center py-16">
|
||||
<OutsideCoding client:load />
|
||||
</section>
|
||||
</div>
|
||||
</ContentLayout>
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||