mirror of
https://github.com/timmypidashev/web.git
synced 2026-04-14 11:03:50 +00:00
Compare commits
33 Commits
v2.1.1
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f7922bbae | ||
|
95081b8b77
|
|||
|
40b6359d8f
|
|||
|
d61080722d
|
|||
|
5117218a1a
|
|||
|
f355373ba1
|
|||
|
384cb82efb
|
|||
|
7ff6f6542b
|
|||
|
9ad08dc85d
|
|||
|
12631dbd42
|
|||
|
1758dc3153
|
|||
|
9496030d41
|
|||
|
30f264a6bb
|
|||
|
7992fcbd49
|
|||
|
60a9fb0339
|
|||
|
6711de5eb6
|
|||
|
f2b4660300
|
|||
|
97608e983c
|
|||
|
ce812e8466
|
|||
|
d44988b39c
|
|||
|
d885ea4e6b
|
|||
|
6cfa4c5b7d
|
|||
|
f2e85dc6d8
|
|||
|
fce17d397e
|
|||
|
7cc954ae07
|
|||
|
c6aa014d29
|
|||
|
a9cbbb7e8e
|
|||
|
788eb84488
|
|||
|
4fc5a07249
|
|||
|
aca5d53bd1
|
|||
|
f1af80afaf
|
|||
|
257000e81d
|
|||
|
8b30228c4a
|
@@ -35,14 +35,13 @@ RUN pnpm run build
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
|
||||
# Install serve
|
||||
RUN npm install -g http-server
|
||||
|
||||
# Copy built files
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
|
||||
# Expose port 3000
|
||||
EXPOSE 3000
|
||||
|
||||
# Deployment command
|
||||
CMD ["http-server", "dist", "-a", "127.0.0.1", "-p", "3000"]
|
||||
CMD ["node", "./dist/server/entry.mjs"]
|
||||
|
||||
2
Makefile
2
Makefile
@@ -1,6 +1,6 @@
|
||||
PROJECT_NAME := "timmypidashev.dev"
|
||||
PROJECT_AUTHORS := "Timothy Pidashev (timmypidashev) <pidashev.tim@gmail.com>"
|
||||
PROJECT_VERSION := "v1.0.2"
|
||||
PROJECT_VERSION := "v2.1.1"
|
||||
PROJECT_LICENSE := "MIT"
|
||||
PROJECT_SOURCES := "https://github.com/timmypidashev/web"
|
||||
PROJECT_REGISTRY := "ghcr.io/timmypidashev"
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
|
||||

|
||||
<img src=".github/preview.jpeg" title="Preview"/>
|
||||
|
||||
@@ -21,7 +21,7 @@ services:
|
||||
command: --interval 120 --cleanup --label-enable
|
||||
|
||||
timmypidashev.dev:
|
||||
container_name: timmypidashev
|
||||
container_name: timmypidashev.dev
|
||||
image: ghcr.io/timmypidashev/timmypidashev.dev:latest
|
||||
networks:
|
||||
- proxy_network
|
||||
|
||||
@@ -10,6 +10,10 @@ import sitemap from "@astrojs/sitemap";
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
output: "server",
|
||||
server: {
|
||||
host: true,
|
||||
port: 3000,
|
||||
},
|
||||
adapter: node({
|
||||
mode: "standalone",
|
||||
}),
|
||||
|
||||
@@ -8,32 +8,36 @@
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/react": "^4.2.4",
|
||||
"@astrojs/react": "^4.4.0",
|
||||
"@astrojs/tailwind": "^6.0.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@types/react": "^18.3.20",
|
||||
"@types/react-dom": "^18.3.6",
|
||||
"astro": "^5.7.4",
|
||||
"astro": "^5.14.1",
|
||||
"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",
|
||||
"@astrojs/mdx": "^4.3.6",
|
||||
"@astrojs/node": "^9.4.4",
|
||||
"@astrojs/rss": "^4.0.12",
|
||||
"@astrojs/sitemap": "^3.6.0",
|
||||
"@giscus/react": "^3.1.0",
|
||||
"@pilcrowjs/object-parser": "^0.0.4",
|
||||
"@react-hook/intersection-observer": "^3.1.2",
|
||||
"@rehype-pretty/transformers": "^0.13.2",
|
||||
"arctic": "^3.6.0",
|
||||
"lucide-react": "^0.468.0",
|
||||
"marked": "^15.0.8",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-responsive": "^10.0.1",
|
||||
"reading-time": "^1.5.0",
|
||||
"rehype-pretty-code": "^0.14.1",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"schema-dts": "^1.1.5",
|
||||
"typewriter-effect": "^2.21.0"
|
||||
"shiki": "^3.12.2",
|
||||
"typewriter-effect": "^2.21.0",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
1115
src/pnpm-lock.yaml
generated
1115
src/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
src/public/blog/breaking-the-chromebook-cage/thumbnail.png
Normal file
BIN
src/public/blog/breaking-the-chromebook-cage/thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 235 KiB |
65
src/public/pgp.asc
Normal file
65
src/public/pgp.asc
Normal file
@@ -0,0 +1,65 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=T4Wx
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -18,6 +18,8 @@ interface Cell {
|
||||
transitioning: boolean;
|
||||
transitionComplete: boolean;
|
||||
rippleEffect: number; // For ripple animation
|
||||
rippleStartTime: number; // When ripple started
|
||||
rippleDistance: number; // Distance from ripple center
|
||||
}
|
||||
|
||||
interface Grid {
|
||||
@@ -42,16 +44,19 @@ interface BackgroundProps {
|
||||
position?: 'left' | 'right';
|
||||
}
|
||||
|
||||
const CELL_SIZE = 25;
|
||||
const CELL_SIZE_MOBILE = 15;
|
||||
const CELL_SIZE_DESKTOP = 25;
|
||||
const TARGET_FPS = 60; // Target frame rate
|
||||
const CYCLE_TIME = 3000; // 3 seconds per full cycle, regardless of FPS
|
||||
const TRANSITION_SPEED = 0.05;
|
||||
const SCALE_SPEED = 0.05;
|
||||
const 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 RIPPLE_SPEED = 0.02; // Speed of ripple propagation
|
||||
const RIPPLE_ELEVATION_FACTOR = 4; // Height of ripple wave
|
||||
const ELEVATION_FACTOR = 8; // Max height for 3D effect - reduced for more subtle effect
|
||||
|
||||
const Background: React.FC<BackgroundProps> = ({
|
||||
layout = 'index',
|
||||
@@ -60,7 +65,8 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const gridRef = useRef<Grid>();
|
||||
const animationFrameRef = useRef<number>();
|
||||
const frameCount = useRef(0);
|
||||
const lastUpdateTimeRef = useRef<number>(0);
|
||||
const lastCycleTimeRef = useRef<number>(0);
|
||||
const resizeTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const mouseRef = useRef<MousePosition>({
|
||||
x: -1000,
|
||||
@@ -83,11 +89,18 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
return colors[Math.floor(Math.random() * colors.length)];
|
||||
};
|
||||
|
||||
const getCellSize = () => {
|
||||
// Check if we're on mobile based on screen width
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
return isMobile ? CELL_SIZE_MOBILE : CELL_SIZE_DESKTOP;
|
||||
};
|
||||
|
||||
const calculateGridDimensions = (width: number, height: number) => {
|
||||
const 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);
|
||||
const cellSize = getCellSize();
|
||||
const cols = Math.floor(width / cellSize);
|
||||
const rows = Math.floor(height / cellSize);
|
||||
const offsetX = Math.floor((width - (cols * cellSize)) / 2);
|
||||
const offsetY = Math.floor((height - (rows * cellSize)) / 2);
|
||||
return { cols, rows, offsetX, offsetY };
|
||||
};
|
||||
|
||||
@@ -114,7 +127,9 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
targetElevation: 0,
|
||||
transitioning: false,
|
||||
transitionComplete: false,
|
||||
rippleEffect: 0
|
||||
rippleEffect: 0,
|
||||
rippleStartTime: 0,
|
||||
rippleDistance: 0
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -224,7 +239,7 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
};
|
||||
|
||||
const createRippleEffect = (grid: Grid, centerX: number, centerY: number) => {
|
||||
const maxDistance = Math.max(grid.cols, grid.rows) / 2;
|
||||
const currentTime = Date.now();
|
||||
|
||||
for (let i = 0; i < grid.cols; i++) {
|
||||
for (let j = 0; j < grid.rows; j++) {
|
||||
@@ -237,15 +252,9 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
|
||||
// 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.rippleStartTime = currentTime + distance * 100; // Delayed start based on distance
|
||||
cell.rippleDistance = distance;
|
||||
cell.rippleEffect = 0;
|
||||
}, 300 + distance * 50);
|
||||
}, distance * 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,51 +281,51 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const updateCellAnimations = (grid: Grid) => {
|
||||
const updateCellAnimations = (grid: Grid, deltaTime: number) => {
|
||||
const mouseX = mouseRef.current.x;
|
||||
const mouseY = mouseRef.current.y;
|
||||
const cellSize = getCellSize();
|
||||
|
||||
// Adjust transition speeds based on time
|
||||
const transitionFactor = TRANSITION_SPEED * (deltaTime / (1000 / TARGET_FPS));
|
||||
const scaleFactor = SCALE_SPEED * (deltaTime / (1000 / TARGET_FPS));
|
||||
|
||||
for (let i = 0; i < grid.cols; i++) {
|
||||
for (let j = 0; j < grid.rows; j++) {
|
||||
const cell = grid.cells[i][j];
|
||||
|
||||
// Smooth transitions
|
||||
cell.opacity += (cell.targetOpacity - cell.opacity) * TRANSITION_SPEED;
|
||||
cell.scale += (cell.targetScale - cell.scale) * SCALE_SPEED;
|
||||
cell.elevation += (cell.targetElevation - cell.elevation) * SCALE_SPEED;
|
||||
cell.opacity += (cell.targetOpacity - cell.opacity) * transitionFactor;
|
||||
cell.scale += (cell.targetScale - cell.scale) * scaleFactor;
|
||||
cell.elevation += (cell.targetElevation - cell.elevation) * scaleFactor;
|
||||
|
||||
// Apply mouse interaction
|
||||
const cellCenterX = grid.offsetX + i * CELL_SIZE + CELL_SIZE / 2;
|
||||
const cellCenterY = grid.offsetY + j * CELL_SIZE + CELL_SIZE / 2;
|
||||
const cellCenterX = grid.offsetX + i * cellSize + cellSize / 2;
|
||||
const cellCenterY = grid.offsetY + j * cellSize + cellSize / 2;
|
||||
const dx = cellCenterX - mouseX;
|
||||
const dy = cellCenterY - mouseY;
|
||||
const distanceToMouse = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// Color wave effect based on mouse position
|
||||
// 3D hill 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);
|
||||
// Calculate height based on distance - peak at center, gradually decreasing
|
||||
const influenceFactor = Math.cos((distanceToMouse / MOUSE_INFLUENCE_RADIUS) * Math.PI / 2);
|
||||
// Only positive elevation (growing upward)
|
||||
cell.targetElevation = ELEVATION_FACTOR * influenceFactor * influenceFactor; // squared for more pronounced effect
|
||||
|
||||
// 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
|
||||
// Slight color shift as cells rise
|
||||
const colorShift = influenceFactor * COLOR_SHIFT_AMOUNT * 0.5;
|
||||
cell.color = [
|
||||
Math.min(255, Math.max(0, cell.baseColor[0] + 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))
|
||||
Math.min(255, Math.max(0, cell.baseColor[0] + colorShift)),
|
||||
Math.min(255, Math.max(0, cell.baseColor[1] + colorShift)),
|
||||
Math.min(255, Math.max(0, cell.baseColor[2] + colorShift))
|
||||
] as [number, number, number];
|
||||
|
||||
// 3D elevation effect when mouse is close
|
||||
cell.targetElevation = ELEVATION_FACTOR * influenceFactor;
|
||||
} else {
|
||||
// Gradually return to base color when mouse is away
|
||||
// Gradually return to base color and zero elevation when mouse is away
|
||||
cell.color[0] += (cell.baseColor[0] - cell.color[0]) * 0.1;
|
||||
cell.color[1] += (cell.baseColor[1] - cell.color[1]) * 0.1;
|
||||
cell.color[2] += (cell.baseColor[2] - cell.color[2]) * 0.1;
|
||||
|
||||
// Reset elevation when mouse moves away
|
||||
cell.targetElevation = 0;
|
||||
}
|
||||
|
||||
@@ -339,9 +348,34 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Gradually decrease ripple effect
|
||||
if (cell.rippleEffect > 0) {
|
||||
cell.rippleEffect = Math.max(0, cell.rippleEffect - RIPPLE_SPEED);
|
||||
// Handle ripple animation
|
||||
if (cell.rippleStartTime > 0) {
|
||||
const elapsedTime = Date.now() - cell.rippleStartTime;
|
||||
if (elapsedTime > 0) {
|
||||
// Calculate ripple progress (0 to 1)
|
||||
const rippleProgress = elapsedTime / 1000; // 1 second for full animation
|
||||
|
||||
if (rippleProgress < 1) {
|
||||
// Create a smooth wave effect
|
||||
const wavePhase = rippleProgress * Math.PI * 2;
|
||||
const waveHeight = Math.sin(wavePhase) * Math.exp(-rippleProgress * 4);
|
||||
|
||||
// Apply wave height to cell elevation only if it's not being overridden by mouse
|
||||
if (distanceToMouse >= MOUSE_INFLUENCE_RADIUS) {
|
||||
cell.rippleEffect = waveHeight;
|
||||
cell.targetElevation = RIPPLE_ELEVATION_FACTOR * waveHeight;
|
||||
} else {
|
||||
cell.rippleEffect = waveHeight * 0.3; // Reduced effect when mouse is influencing
|
||||
}
|
||||
} else {
|
||||
// Reset ripple effects
|
||||
cell.rippleEffect = 0;
|
||||
cell.rippleStartTime = 0;
|
||||
if (distanceToMouse >= MOUSE_INFLUENCE_RADIUS) {
|
||||
cell.targetElevation = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -354,6 +388,7 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
const cellSize = getCellSize();
|
||||
|
||||
mouseRef.current.isDown = true;
|
||||
mouseRef.current.lastClickTime = Date.now();
|
||||
@@ -361,8 +396,8 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
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);
|
||||
const cellX = Math.floor((mouseX - grid.offsetX) / cellSize);
|
||||
const cellY = Math.floor((mouseY - grid.offsetY) / cellSize);
|
||||
|
||||
if (cellX >= 0 && cellX < grid.cols && cellY >= 0 && cellY < grid.rows) {
|
||||
mouseRef.current.cellX = cellX;
|
||||
@@ -381,13 +416,37 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!canvasRef.current) return;
|
||||
if (!canvasRef.current || !gridRef.current) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const cellSize = getCellSize();
|
||||
|
||||
mouseRef.current.x = e.clientX - rect.left;
|
||||
mouseRef.current.y = e.clientY - rect.top;
|
||||
|
||||
// Drawing functionality - place cells while dragging
|
||||
if (mouseRef.current.isDown) {
|
||||
const grid = gridRef.current;
|
||||
|
||||
// Calculate which cell the mouse is over
|
||||
const cellX = Math.floor((mouseRef.current.x - grid.offsetX) / cellSize);
|
||||
const cellY = Math.floor((mouseRef.current.y - grid.offsetY) / cellSize);
|
||||
|
||||
// Only draw if we're on a new cell
|
||||
if (cellX !== mouseRef.current.cellX || cellY !== mouseRef.current.cellY) {
|
||||
mouseRef.current.cellX = cellX;
|
||||
mouseRef.current.cellY = cellY;
|
||||
|
||||
// Spawn cell at this position if it's empty
|
||||
if (cellX >= 0 && cellX < grid.cols && cellY >= 0 && cellY < grid.rows) {
|
||||
const cell = grid.cells[cellX][cellY];
|
||||
if (!cell.alive && !cell.transitioning) {
|
||||
spawnCellAtPosition(grid, cellX, cellY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
@@ -439,12 +498,15 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
const ctx = setupCanvas(canvas, displayWidth, displayHeight);
|
||||
if (!ctx) return;
|
||||
|
||||
frameCount.current = 0;
|
||||
lastUpdateTimeRef.current = 0;
|
||||
lastCycleTimeRef.current = 0;
|
||||
|
||||
const cellSize = getCellSize();
|
||||
|
||||
// Only initialize new grid if one doesn't exist or dimensions changed
|
||||
if (!gridRef.current ||
|
||||
gridRef.current.cols !== Math.floor(displayWidth / CELL_SIZE) ||
|
||||
gridRef.current.rows !== Math.floor(displayHeight / CELL_SIZE)) {
|
||||
gridRef.current.cols !== Math.floor(displayWidth / cellSize) ||
|
||||
gridRef.current.rows !== Math.floor(displayHeight / cellSize)) {
|
||||
gridRef.current = initGrid(displayWidth, displayHeight);
|
||||
}
|
||||
}, 250);
|
||||
@@ -467,18 +529,52 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
canvas.addEventListener('mouseup', handleMouseUp, { signal });
|
||||
canvas.addEventListener('mouseleave', handleMouseLeave, { signal });
|
||||
|
||||
const animate = () => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
// Tab is hidden
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
animationFrameRef.current = undefined;
|
||||
}
|
||||
} else {
|
||||
// Tab is visible again
|
||||
if (!animationFrameRef.current) {
|
||||
// Reset timing references to prevent catching up
|
||||
lastUpdateTimeRef.current = performance.now();
|
||||
lastCycleTimeRef.current = performance.now();
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
if (signal.aborted) return;
|
||||
|
||||
frameCount.current++;
|
||||
|
||||
if (gridRef.current) {
|
||||
// Every CYCLE_FRAMES, compute the next state
|
||||
if (frameCount.current % CYCLE_FRAMES === 0) {
|
||||
computeNextState(gridRef.current);
|
||||
// Initialize timing if first frame
|
||||
if (!lastUpdateTimeRef.current) {
|
||||
lastUpdateTimeRef.current = currentTime;
|
||||
lastCycleTimeRef.current = currentTime;
|
||||
}
|
||||
|
||||
updateCellAnimations(gridRef.current);
|
||||
// Calculate time since last frame
|
||||
const deltaTime = currentTime - lastUpdateTimeRef.current;
|
||||
|
||||
// Limit delta time to prevent large jumps when tab becomes active again
|
||||
const clampedDeltaTime = Math.min(deltaTime, 100);
|
||||
|
||||
lastUpdateTimeRef.current = currentTime;
|
||||
|
||||
// Calculate time since last cycle update
|
||||
const cycleElapsed = currentTime - lastCycleTimeRef.current;
|
||||
|
||||
if (gridRef.current) {
|
||||
// Check if it's time for the next life cycle
|
||||
if (cycleElapsed >= CYCLE_TIME) {
|
||||
computeNextState(gridRef.current);
|
||||
lastCycleTimeRef.current = currentTime;
|
||||
}
|
||||
|
||||
updateCellAnimations(gridRef.current, clampedDeltaTime);
|
||||
}
|
||||
|
||||
// Draw frame
|
||||
@@ -487,8 +583,9 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
|
||||
if (gridRef.current) {
|
||||
const grid = gridRef.current;
|
||||
const cellSize = CELL_SIZE * 0.8;
|
||||
const roundness = cellSize * 0.2;
|
||||
const cellSize = getCellSize();
|
||||
const displayCellSize = cellSize * 0.8;
|
||||
const roundness = displayCellSize * 0.2;
|
||||
|
||||
for (let i = 0; i < grid.cols; i++) {
|
||||
for (let j = 0; j < grid.rows; j++) {
|
||||
@@ -496,71 +593,38 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
// 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);
|
||||
// Base opacity
|
||||
ctx.globalAlpha = cell.opacity * 0.9;
|
||||
|
||||
const scaledSize = cellSize * cell.scale;
|
||||
const xOffset = (cellSize - scaledSize) / 2;
|
||||
const yOffset = (cellSize - scaledSize) / 2;
|
||||
const scaledSize = displayCellSize * cell.scale;
|
||||
const xOffset = (displayCellSize - scaledSize) / 2;
|
||||
const yOffset = (displayCellSize - scaledSize) / 2;
|
||||
|
||||
// Apply 3D elevation effect
|
||||
const elevationOffset = cell.elevation;
|
||||
|
||||
const x = grid.offsetX + i * CELL_SIZE + (CELL_SIZE - cellSize) / 2 + xOffset;
|
||||
const y = grid.offsetY + j * CELL_SIZE + (CELL_SIZE - cellSize) / 2 + yOffset - elevationOffset;
|
||||
const x = grid.offsetX + i * cellSize + (cellSize - displayCellSize) / 2 + xOffset;
|
||||
const y = grid.offsetY + j * cellSize + (cellSize - displayCellSize) / 2 + yOffset - elevationOffset;
|
||||
const scaledRoundness = roundness * cell.scale;
|
||||
|
||||
// Draw shadow for 3D effect if cell has elevation
|
||||
if (elevationOffset > 1) {
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
|
||||
// Draw shadow for 3D effect when cell is elevated
|
||||
if (elevationOffset > 0.5) {
|
||||
ctx.fillStyle = `rgba(0, 0, 0, ${0.2 * (elevationOffset / ELEVATION_FACTOR)})`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + scaledRoundness, y + elevationOffset + 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.moveTo(x + scaledRoundness, y + elevationOffset * 1.1);
|
||||
ctx.lineTo(x + scaledSize - scaledRoundness, y + elevationOffset * 1.1);
|
||||
ctx.quadraticCurveTo(x + scaledSize, y + elevationOffset * 1.1, x + scaledSize, y + elevationOffset * 1.1 + scaledRoundness);
|
||||
ctx.lineTo(x + scaledSize, y + elevationOffset * 1.1 + scaledSize - scaledRoundness);
|
||||
ctx.quadraticCurveTo(x + scaledSize, y + elevationOffset * 1.1 + scaledSize, x + scaledSize - scaledRoundness, y + elevationOffset * 1.1 + scaledSize);
|
||||
ctx.lineTo(x + scaledRoundness, y + elevationOffset * 1.1 + scaledSize);
|
||||
ctx.quadraticCurveTo(x, y + elevationOffset * 1.1 + scaledSize, x, y + elevationOffset * 1.1 + scaledSize - scaledRoundness);
|
||||
ctx.lineTo(x, y + elevationOffset * 1.1 + scaledRoundness);
|
||||
ctx.quadraticCurveTo(x, y + elevationOffset * 1.1, x + scaledRoundness, y + elevationOffset * 1.1);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// Draw main cell with original color
|
||||
// Draw main cell
|
||||
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + scaledRoundness, y);
|
||||
@@ -574,9 +638,9 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
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})`;
|
||||
// Draw highlight on elevated cells
|
||||
if (elevationOffset > 0.5) {
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${0.1 * (elevationOffset / ELEVATION_FACTOR)})`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + scaledRoundness, y);
|
||||
ctx.lineTo(x + scaledSize - scaledRoundness, y);
|
||||
@@ -588,23 +652,7 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
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();
|
||||
}
|
||||
// No need for separate ripple drawing since the elevation handles the 3D ripple effect
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -615,11 +663,13 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
animationFrameRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange, { signal });
|
||||
window.addEventListener('resize', handleResize, { signal });
|
||||
animate();
|
||||
animate(performance.now());
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
@@ -645,7 +695,8 @@ const Background: React.FC<BackgroundProps> = ({
|
||||
<div className={getContainerClasses()}>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="w-full h-full bg-black cursor-pointer"
|
||||
className="w-full h-full bg-black"
|
||||
style={{ cursor: 'default' }} // Changed from cursor-pointer to default
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm pointer-events-none" />
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ export const Comments = () => {
|
||||
emitMetadata="0"
|
||||
inputPosition="bottom"
|
||||
lang="en"
|
||||
loading="lazy"
|
||||
loading="eager"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -12,8 +12,8 @@ export default function Footer({ fixed = false }) {
|
||||
));
|
||||
|
||||
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">
|
||||
<footer className={`w-full font-bold pointer-events-none ${fixed ? "fixed bottom-0 left-0 right-0" : ""}`}>
|
||||
<div className="flex flex-row px-2 py-1 text-lg lg:px-6 lg:py-1.5 lg:text-3xl md:text-2xl justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20 pointer-events-none [&_a]:pointer-events-auto">
|
||||
{footerLinks}
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -87,16 +87,19 @@ export default function Header() {
|
||||
fixed z-50 top-0 left-0 right-0
|
||||
font-bold
|
||||
transition-transform duration-300
|
||||
pointer-events-none
|
||||
${visible ? "translate-y-0" : "-translate-y-full"}
|
||||
`}
|
||||
>
|
||||
<div className={`
|
||||
w-full flex flex-row items-center justify-center
|
||||
pointer-events-none
|
||||
${!isIndexPage ? 'bg-black md:bg-transparent' : ''}
|
||||
`}>
|
||||
<div className={`
|
||||
w-full md:w-auto flex flex-row pt-1 px-2 text-lg lg:text-3xl md:text-2xl
|
||||
items-center justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20 md:py-2
|
||||
pointer-events-none [&_a]:pointer-events-auto
|
||||
${!isIndexPage ? 'bg-black md:px-20' : ''}
|
||||
`}>
|
||||
{headerLinks}
|
||||
|
||||
@@ -72,8 +72,8 @@ export default function Hero() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen">
|
||||
<div className="text-4xl font-bold text-center">
|
||||
<div className="flex justify-center items-center min-h-screen pointer-events-none">
|
||||
<div className="text-4xl font-bold text-center pointer-events-none [&_a]:pointer-events-auto">
|
||||
<Typewriter
|
||||
options={typewriterOptions}
|
||||
onInit={handleInit}
|
||||
|
||||
264
src/src/components/mdx/command.tsx
Normal file
264
src/src/components/mdx/command.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Terminal, Copy, Check } from 'lucide-react';
|
||||
// Import all required icons from react-icons
|
||||
import { FaDebian, FaFedora } from 'react-icons/fa6';
|
||||
import { SiGentoo, SiNixos, SiArchlinux } from 'react-icons/si';
|
||||
|
||||
// Component for multi-line command sequences
|
||||
const CommandSequence = ({
|
||||
commands,
|
||||
description,
|
||||
shell = "bash"
|
||||
}) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
// Join the commands with newlines for copying
|
||||
const fullCommandText = Array.isArray(commands)
|
||||
? commands.join('\n')
|
||||
: commands;
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(fullCommandText)
|
||||
.then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-md overflow-hidden border border-foreground/20 bg-background my-4" style={{ maxWidth: '95vw' }}>
|
||||
{/* Header with Terminal Icon and Copy Button */}
|
||||
<div className="bg-background border-b border-foreground/20 text-foreground p-2 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Terminal size={20} className="mr-2 text-yellow-bright" />
|
||||
<div className="text-sm font-comic-code">
|
||||
{description || "Terminal Commands"}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="bg-background hover:bg-foreground/10 text-foreground text-xs px-2 py-1 rounded flex items-center"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check size={14} className="mr-1 text-green-bright" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy size={14} className="mr-1 text-foreground/70" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Command Display */}
|
||||
<div className="text-foreground p-3 overflow-x-auto">
|
||||
<div className="font-comic-code text-sm">
|
||||
{Array.isArray(commands)
|
||||
? commands.map((cmd, index) => (
|
||||
<div key={index} className="flex items-start mb-2 last:mb-0">
|
||||
<span className="text-orange-bright mr-2 flex-shrink-0">$</span>
|
||||
<span className="text-purple-bright overflow-x-auto whitespace-nowrap">
|
||||
{cmd}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
: (
|
||||
<div className="flex items-start">
|
||||
<span className="text-orange-bright mr-2 flex-shrink-0">$</span>
|
||||
<span className="text-purple-bright overflow-x-auto whitespace-nowrap">
|
||||
{commands}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Original Commands component with tabs for different distros
|
||||
const Commands = ({
|
||||
commandId,
|
||||
description,
|
||||
archCommand,
|
||||
debianCommand,
|
||||
fedoraCommand,
|
||||
gentooCommand,
|
||||
nixCommand
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState('arch');
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const distros = [
|
||||
{
|
||||
id: 'arch',
|
||||
name: 'Arch',
|
||||
icon: SiArchlinux,
|
||||
command: archCommand || 'echo "No command specified for Arch"'
|
||||
},
|
||||
{
|
||||
id: 'debian',
|
||||
name: 'Debian/Ubuntu',
|
||||
icon: FaDebian,
|
||||
command: debianCommand || 'echo "No command specified for Debian/Ubuntu"'
|
||||
},
|
||||
{
|
||||
id: 'fedora',
|
||||
name: 'Fedora',
|
||||
icon: FaFedora,
|
||||
command: fedoraCommand || 'echo "No command specified for Fedora"'
|
||||
},
|
||||
{
|
||||
id: 'gentoo',
|
||||
name: 'Gentoo',
|
||||
icon: SiGentoo,
|
||||
command: gentooCommand || 'echo "No command specified for Gentoo"'
|
||||
},
|
||||
{
|
||||
id: 'nix',
|
||||
name: 'NixOS',
|
||||
icon: SiNixos,
|
||||
command: nixCommand || 'echo "No command specified for NixOS"'
|
||||
}
|
||||
];
|
||||
|
||||
const copyToClipboard = (text) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
.then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-md overflow-hidden border border-foreground/20 bg-background my-4" style={{ maxWidth: '95vw' }}>
|
||||
{/* Header with Terminal Icon and Copy Button */}
|
||||
<div className="bg-background border-b border-foreground/20 text-foreground p-2 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Terminal size={20} className="mr-2 text-yellow-bright" />
|
||||
<div className="text-sm font-comic-code">
|
||||
{description || "Terminal Command"}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => copyToClipboard(distros.find(d => d.id === activeTab).command)}
|
||||
className="bg-background hover:bg-foreground/10 text-foreground text-xs px-2 py-1 rounded flex items-center"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check size={14} className="mr-1 text-green-bright" />
|
||||
<span>Copied</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy size={14} className="mr-1 text-foreground/70" />
|
||||
<span>Copy</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex flex-wrap border-b border-foreground/20 bg-background">
|
||||
{distros.map((distro) => {
|
||||
const IconComponent = distro.icon;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={distro.id}
|
||||
className={`px-3 py-2 text-sm font-medium flex items-center ${
|
||||
activeTab === distro.id
|
||||
? 'bg-background border-b-2 border-blue-bright text-blue-bright'
|
||||
: 'text-foreground/80 hover:text-foreground hover:bg-foreground/5'
|
||||
}`}
|
||||
onClick={() => setActiveTab(distro.id)}
|
||||
>
|
||||
<span className="mr-1 inline-flex items-center">
|
||||
<IconComponent size={16} />
|
||||
</span>
|
||||
{distro.name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Command Display with Horizontal Scrolling */}
|
||||
<div className="text-foreground p-3 overflow-x-auto">
|
||||
<div className="flex items-center font-comic-code text-sm whitespace-nowrap">
|
||||
<span className="text-orange-bright mr-2">$</span>
|
||||
<span className="text-purple-bright">
|
||||
{distros.find(d => d.id === activeTab).command}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Single command component
|
||||
const Command = ({
|
||||
command,
|
||||
description,
|
||||
shell = "bash"
|
||||
}) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(command)
|
||||
.then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-md overflow-hidden border border-foreground/20 bg-background my-4" style={{ maxWidth: '95vw' }}>
|
||||
{/* Header with Terminal Icon and Copy Button */}
|
||||
<div className="bg-background border-b border-foreground/20 text-foreground p-2 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Terminal size={20} className="mr-2 text-yellow-bright" />
|
||||
<div className="text-sm font-comic-code">
|
||||
{description || "Terminal Command"}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="bg-background hover:bg-foreground/10 text-foreground text-xs px-2 py-1 rounded flex items-center"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check size={14} className="mr-1 text-green-bright" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy size={14} className="mr-1 text-foreground/70" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Command Display with Horizontal Scrolling */}
|
||||
<div className="text-foreground p-3 overflow-x-auto">
|
||||
<div className="flex items-center font-comic-code text-sm whitespace-nowrap">
|
||||
<span className="text-orange-bright mr-2">$</span>
|
||||
<span className="text-purple-bright">
|
||||
{command}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Commands, Command, CommandSequence };
|
||||
68
src/src/components/mdx/video.tsx
Normal file
68
src/src/components/mdx/video.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
// src/components/mdx/Video.tsx
|
||||
import React, { useRef } from "react";
|
||||
import { Play } from "lucide-react";
|
||||
|
||||
type VideoProps = {
|
||||
url: string;
|
||||
title: string;
|
||||
attribution?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function Video({ url, title, attribution, className }: VideoProps) {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const overlayRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handlePlay = () => {
|
||||
if (!videoRef.current || !overlayRef.current) return;
|
||||
|
||||
// Show browser native controls on play
|
||||
videoRef.current.controls = true;
|
||||
videoRef.current.play();
|
||||
|
||||
// Hide the overlay
|
||||
overlayRef.current.style.display = "none";
|
||||
};
|
||||
|
||||
return (
|
||||
<figure className={`w-full ${className ?? ""}`}>
|
||||
<div className="relative w-full bg-background rounded-lg overflow-hidden">
|
||||
<video
|
||||
ref={videoRef}
|
||||
className="w-full h-auto bg-black cursor-pointer rounded-lg block"
|
||||
preload="metadata"
|
||||
playsInline
|
||||
title={title}
|
||||
>
|
||||
<source src={url} type="video/mp4" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
|
||||
{/* Big overlay play button */}
|
||||
<button
|
||||
ref={overlayRef}
|
||||
onClick={handlePlay}
|
||||
className="absolute inset-0 flex items-center justify-center bg-background/90 text-foreground hover:text-yellow-bright transition"
|
||||
aria-label={`Play ${title}`}
|
||||
>
|
||||
<Play size={64} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Title + attribution */}
|
||||
<figcaption className="mt-2 text-xs text-foreground flex justify-between items-center">
|
||||
<span>{title}</span>
|
||||
{attribution && (
|
||||
<a
|
||||
href={attribution}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-blue-bright"
|
||||
>
|
||||
{attribution}
|
||||
</a>
|
||||
)}
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
9
src/src/content/blog/breaking-the-chromebook-cage.mdx
Normal file
9
src/src/content/blog/breaking-the-chromebook-cage.mdx
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: Breaking the Chromebook Cage
|
||||
description: From breaking Chromebooks as a student to breaking Chromebooks to stop students from breaking Chromebooks
|
||||
author: Timothy Pidashev
|
||||
tags: ["uefi", "coreboot", "firmware", "chromebooks"]
|
||||
date: 2025-09-15
|
||||
image: "/blog/breaking-the-chromebook-cage/thumbnail.png"
|
||||
isDraft: true
|
||||
---
|
||||
@@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
|
||||
interface T440pAdProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Advertisement: React.FC<T440pAdProps> = ({ className = '' }) => {
|
||||
return (
|
||||
<div className={`bg-gradient-to-br from-blue-50 to-indigo-100 border-2 border-blue-200 rounded-lg p-6 my-8 shadow-lg ${className}`}>
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Icon/Logo placeholder */}
|
||||
<div className="flex-shrink-0 w-12 h-12 bg-blue-600 rounded-lg flex items-center justify-center">
|
||||
<svg
|
||||
className="w-8 h-8 text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<h3 className="text-xl font-bold text-gray-900">
|
||||
Custom Corebooted ThinkPad T440p
|
||||
</h3>
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
Available Now
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-700 mb-4">
|
||||
Skip the technical complexity and get a professionally corebooted ThinkPad T440p
|
||||
built to your specifications. Each laptop is carefully modified and tested to ensure
|
||||
optimal performance and reliability.
|
||||
</p>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 mb-2">✨ What's Included:</h4>
|
||||
<ul className="text-sm text-gray-700 space-y-1">
|
||||
<li>• Coreboot firmware pre-installed</li>
|
||||
<li>• IPS 1080p display upgrade</li>
|
||||
<li>• RAM options: 4GB, 8GB, or 16GB</li>
|
||||
<li>• CPU choice available</li>
|
||||
<li>• Battery upgrade option</li>
|
||||
<li>• Thorough testing & quality assurance</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 mb-2">🔧 Benefits:</h4>
|
||||
<ul className="text-sm text-gray-700 space-y-1">
|
||||
<li>• Faster boot times</li>
|
||||
<li>• Open-source BIOS</li>
|
||||
<li>• Enhanced security</li>
|
||||
<li>• No proprietary firmware</li>
|
||||
<li>• Full hardware control</li>
|
||||
<li>• Professional installation</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 pt-4 border-t border-blue-200">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold text-blue-600">$500</span>
|
||||
<span className="text-sm text-gray-600">USD (base configuration)</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<a
|
||||
href="https://ebay.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8.5 2A1.5 1.5 0 007 3.5v1A1.5 1.5 0 008.5 6h7A1.5 1.5 0 0017 4.5v-1A1.5 1.5 0 0015.5 2h-7zM10 3h4v1h-4V3z"/>
|
||||
<path d="M6 7.5A1.5 1.5 0 017.5 6h9A1.5 1.5 0 0118 7.5v9a1.5 1.5 0 01-1.5 1.5h-9A1.5 1.5 0 016 16.5v-9z"/>
|
||||
</svg>
|
||||
Order on eBay
|
||||
</a>
|
||||
|
||||
<button className="inline-flex items-center px-4 py-2 border border-blue-600 text-blue-600 font-medium rounded-lg hover:bg-blue-50 transition-colors">
|
||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Ask Questions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Advertisement;
|
||||
308
src/src/content/blog/thinkpad-t440p-coreboot-guide.mdx
Normal file
308
src/src/content/blog/thinkpad-t440p-coreboot-guide.mdx
Normal file
@@ -0,0 +1,308 @@
|
||||
---
|
||||
title: Thinkpad T440p Coreboot Guide
|
||||
description: The definitive guide on corebooting a Thinkpad T440p
|
||||
author: Timothy Pidashev
|
||||
tags: [t440p, coreboot, thinkpad]
|
||||
date: 2025-01-15
|
||||
image: "/blog/thinkpad-t440p-coreboot-guide/thumbnail.png"
|
||||
isDraft: true
|
||||
---
|
||||
|
||||
import { Commands, Command, CommandSequence } from "@/components/mdx/command";
|
||||
import Advertisement from '@/content/blog/components/thinkpad-t440p-coreboot-guide/advertisement';
|
||||
|
||||
> **Interactive Script Available!**
|
||||
> Want to skip the manual steps in this guide?
|
||||
> I've created an interactive script that can automate the entire process step by step as you follow along.
|
||||
> This script supports Arch, Debian, Fedora, Gentoo, and Nix!
|
||||
|
||||
<Command
|
||||
description="Interactive script"
|
||||
command="curl -fsSL https://timmypidashev.dev/scripts/run.sh | sh -s -- -t coreboot-t440p"
|
||||
client:load
|
||||
/>
|
||||
|
||||
Don't pipe anyone's scripts to **sh** blindly, including mine - <a href="https://github.com/timmypidashev/scripts" target="_blank" rel="noopener noreferrer">audit the source</a>.
|
||||
|
||||
## Getting Started
|
||||
The Thinkpad T440p is a powerful and versatile laptop that can be further enhanced by installing coreboot,
|
||||
an open-source BIOS replacement. This guide will walk you through the process of corebooting your T440p,
|
||||
including flashing the BIOS chip and installing the necessary software.
|
||||
|
||||
## What You'll Need
|
||||
|
||||
Before getting started corebooting your T440p, make sure you have the following:
|
||||
|
||||
- **Thinkpad T440p**: This guide is specifically for the T440p model.
|
||||
- **CH341A Programmer**: This is a USB device used to flash the BIOS chip.
|
||||
- **Screwdriver**: A torx screwdriver is needed to open the laptop.
|
||||
|
||||
## Installing Dependencies
|
||||
|
||||
Install the following programs. These will be needed to compile coreboot and flash the BIOS.
|
||||
|
||||
<Commands
|
||||
description="Install prerequisite packages"
|
||||
archCommand="sudo pacman -S base-devel curl git gcc-ada ncurses zlib nasm sharutils unzip flashrom"
|
||||
debianCommand="sudo apt install build-essential curl git gnat libncurses-dev zlib1g-dev nasm sharutils unzip flashrom"
|
||||
fedoraCommand="sudo dnf install @development-tools curl git gcc-gnat ncurses-devel zlib-devel nasm sharutils unzip flashrom"
|
||||
gentooCommand="sudo emerge --ask sys-devel/base-devel net-misc/curl dev-vcs/git sys-devel/gcc ncurses dev-libs/zlib dev-lang/nasm app-arch/sharutils app-arch/unzip sys-apps/flashrom"
|
||||
nixCommand="nix-env -i stdenv curl git gcc gnat ncurses zlib nasm sharutils unzip flashrom"
|
||||
client:load
|
||||
/>
|
||||
|
||||
## Disassembling the Laptop
|
||||
1. **Power off your laptop**: Make sure your T440p is completely powered off and unplugged from any power source.
|
||||
2. **Remove the battery**: Flip the laptop over and remove the battery by sliding the latch to the unlock position and lifting it out.
|
||||
3. **Unscrew the back panel**: Use a torx screwdriver to remove the screws securing the back panel.
|
||||
|
||||
## Locating the EEPROM Chips
|
||||
|
||||
In order to flash the laptop, you will need to have access to two EEPROM chips located next to the sodimm RAM.
|
||||
|
||||

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

|
||||
|
||||
After the flasher is ready, connect it to your machine and ensure its ready to use:
|
||||
|
||||
<Command
|
||||
description="Ensure the CH341A flasher is being detected"
|
||||
command="flashrom --programmer ch341a_spi"
|
||||
/>
|
||||
|
||||
Flashrom should report that programmer initialization was a success.
|
||||
|
||||
## Extracting Original BIOS
|
||||
|
||||
To begin, first create a clean directory where all work to coreboot
|
||||
the T440p will be done.
|
||||
|
||||
<Command
|
||||
description="Create a directory where all work will be done"
|
||||
command="mkdir ~/t440p-coreboot"
|
||||
client:load
|
||||
/>
|
||||
|
||||
Next, extract the original rom from both EEPROM chips. This is
|
||||
done by attaching the programmer to the correct chip and running
|
||||
the subsequent commands. It may take longer than expected, and
|
||||
ensuring the bios was properly extracted is important before proceeding
|
||||
further.
|
||||
|
||||
<CommandSequence
|
||||
commands={[
|
||||
"sudo flashrom --programmer ch341a_spi -r 4mb_backup1.bin",
|
||||
"sudo flashrom --programmer ch341a_spi -r 4mb_backup2.bin",
|
||||
"diff 4mb_backup1.bin 4mb_backup2.bin"
|
||||
]}
|
||||
description="Backup and verify 4MB chip"
|
||||
client:load
|
||||
/>
|
||||
|
||||
<CommandSequence
|
||||
commands={[
|
||||
"sudo flashrom --programmer ch341a_spi -r 8mb_backup1.bin",
|
||||
"sudo flashrom --programmer ch341a_spi -r 8mb_backup2.bin",
|
||||
"diff 8mb_backup1.bin 8mb_backup2.bin"
|
||||
]}
|
||||
description="Backup and verify 8MB chip"
|
||||
client:load
|
||||
/>
|
||||
|
||||
If the diff checks pass, combine both files into one ROM.
|
||||
|
||||
<Command
|
||||
description="Combine 4MB & 8MB into one ROM"
|
||||
command="cat 8mb_backup_1.bin 4mb_backup1.bin > t440p-original.rom"
|
||||
client:load
|
||||
/>
|
||||
|
||||
## Building Required Tools
|
||||
|
||||
Now that the original bios has been successfuly extracted, it is time
|
||||
to clone the coreboot repository and build every tool needed to build
|
||||
a new bios image.
|
||||
|
||||
<CommandSequence
|
||||
commands={[
|
||||
"git clone https://review.coreboot.org/coreboot ~/t440p-coreboot/coreboot",
|
||||
"cd ~/t440p-coreboot/coreboot",
|
||||
"git checkout e1e762716cf925c621d58163133ed1c3e006a903",
|
||||
"git submodule update --init --checkout"
|
||||
]}
|
||||
description="Clone coreboot and checkout to the correct commit"
|
||||
client:load
|
||||
/>
|
||||
|
||||
We will need to build `idftool`, which will be used to export all necessary blobs
|
||||
from our original bios, and `cbfstool`, which will be used to extract __mrc.bin__(a blob
|
||||
from a haswell chromebook peppy image).
|
||||
|
||||
<Command
|
||||
description="Build util/ifdtool"
|
||||
command="cd ~/t440p-coreboot/coreboot/util/ifdtool && make"
|
||||
client:load
|
||||
/>
|
||||
|
||||
<Command
|
||||
description="Build util/cbfstool"
|
||||
command="cd ~/t440p-coreboot/coreboot/ && make -C util/cbfstool"
|
||||
client:load
|
||||
/>
|
||||
|
||||
## Exporting Firmware Blobs
|
||||
|
||||
Once the necessary tools have been built, we can export the
|
||||
3 flash regions from our original bios image.
|
||||
|
||||
<CommandSequence
|
||||
commands={[
|
||||
"cd ~/t440p-coreboot/coreboot/util/ifdtool",
|
||||
"./ifdtool -x ~/t440p-coreboot/t440p-original.rom",
|
||||
"mv flashregion_0_flashdescriptor.bin ~/t440p-coreboot/ifd.bin",
|
||||
"mv flashregion_2_intel_me.bin ~/t440p-coreboot/me.bin",
|
||||
"mv flashregion_3_gbe.bin ~/t440p-coreboot/gbe.bin"
|
||||
]}
|
||||
description="Export firmware blobs"
|
||||
client:load
|
||||
/>
|
||||
|
||||
## Obtaining mrc.bin
|
||||
|
||||
In order to obtain __mrc.bin__, we need the chromeos peppy image.
|
||||
This can be pulled by running the `crosfirmware.sh` script found in util/chromeos.
|
||||
|
||||
<Command
|
||||
description="Download peppy chromeos image"
|
||||
command="cd ~/t440p-coreboot/coreboot/util/chromeos && ./crosfirmware.sh peppy"
|
||||
client:load
|
||||
/>
|
||||
|
||||
|
||||
We can now obtain __mrc.bin__ using cbfstool to extract the blob from the image.
|
||||
|
||||
<CommandSequence
|
||||
commands={[
|
||||
"cd ~/t440p-coreboot/coreboot/util/chromeos",
|
||||
"../cbfstool/cbfstool coreboot-*.bin extract -f mrc.bin -n mrc.bin -r RO_SECTION",
|
||||
"mv mrc.bin ~/t440p-coreboot/mrc.bin"
|
||||
]}
|
||||
description="Extract mrc.bin using cbfstool"
|
||||
client:load
|
||||
/>
|
||||
|
||||
## Configuring Coreboot
|
||||
|
||||
Configuring coreboot is really where most of your time will be spent. To help out,
|
||||
I've created several handy configs that should suit most use cases, and can be easily
|
||||
tweaked to your liking. Here is a list of whats available:
|
||||
|
||||
1. GRUB2
|
||||
|
||||
This configuration features GRUB2 as the bootloader, and contains 3 secondary payloads,
|
||||
which the user can opt in/out of:
|
||||
|
||||
* memtest built in
|
||||
* nvramcui built in
|
||||
* coreinfo built in
|
||||
|
||||
This configuration also includes the dGPU option rom as well for T440p's featuring the gt730m on board.
|
||||
|
||||
2. SeaBIOS
|
||||
|
||||
3. edk2
|
||||
|
||||
> NOTE: Show the user how to choose the appropriate config, as well as building a custom config below.
|
||||
|
||||
## Building and Flashing
|
||||
|
||||
After configuring coreboot, it is time to build and flash it onto your unsuspecting T440p :D
|
||||
|
||||
<CommandSequence
|
||||
commands={[
|
||||
"cd ~/t440p-coreboot/coreboot",
|
||||
"make crossgcc-i386 CPUS=$(nproc)",
|
||||
"make"
|
||||
]}
|
||||
description="Build coreboot"
|
||||
client:load
|
||||
/>
|
||||
|
||||
Once the coreboot build has completed, split the built ROM for the 8MB(bottom) chip & 4MB(top) chip.
|
||||
|
||||
<CommandSequence
|
||||
commands={[
|
||||
"cd ~/t440p-coreboot/coreboot/build",
|
||||
"dd if=coreboot.rom of=bottom.rom bs=1M count=8",
|
||||
"dd if=coreboot.rom of=top.rom bs=1M skin=8"
|
||||
]}
|
||||
description="Split the built ROM for both EEPROM chips"
|
||||
client:load
|
||||
/>
|
||||
|
||||
Now flash the new bios onto your thinkpad!
|
||||
|
||||
<Command
|
||||
description="Flash the 4MB chip"
|
||||
command="sudo flashrom --programmer ch341a_spi -w top.rom"
|
||||
/>
|
||||
|
||||
<Command
|
||||
description="Flash the 8MB chip"
|
||||
command="sudo flashrom --programmer ch341a_spi -w bottom.rom"
|
||||
/>
|
||||
|
||||
Thats it! If done properly, your thinkpad should now boot!
|
||||
|
||||
## Reverting to Original
|
||||
|
||||
If for some reason you feel the need to revert back, or your T440p can't boot,
|
||||
here are the steps needed to flash the original image back.
|
||||
|
||||
### Can't Boot
|
||||
|
||||
<CommandSequence
|
||||
commands={[
|
||||
"cd ~/t440p-coreboot/",
|
||||
"dd if=t440p-original.rom of=bottom.rom bs=1M count=8",
|
||||
"dd if=t440p-original.rom of=top.rom bs=1M skip=8"
|
||||
]}
|
||||
description="Split original bios image for both EEPROM chips"
|
||||
client:load
|
||||
/>
|
||||
|
||||
<Command
|
||||
description="Flash the 4MB chip"
|
||||
command="sudo flashrom --programmer ch341a_spi -w top.rom"
|
||||
/>
|
||||
|
||||
<Command
|
||||
description="Flash the 8MB chip"
|
||||
command="sudo flashrom --programmer ch341a_spi -w bottom.rom"
|
||||
/>
|
||||
|
||||
### Can Boot
|
||||
|
||||
<CommandSequence
|
||||
commands={[
|
||||
"sudo sed -i '/GRUB_CMDLINE_LINUX_DEFAULT/ s/\"/ iomem=relaxed\"/2' /etc/default/grub",
|
||||
"sudo grub-mkconfig -o /boot/grub/grub.cfg",
|
||||
]}
|
||||
description="Set kernel flag iomem=relaxed and update grub config"
|
||||
client:load
|
||||
/>
|
||||
|
||||
Reboot to apply `iomem=relaxed`
|
||||
|
||||
<Command
|
||||
description="Flash the original bios"
|
||||
command="sudo flashrom -p internal:laptop=force_I_want_a_brick -r ~/t440p-coreboot/t440p-original.rom"
|
||||
/>
|
||||
|
||||
And that about wraps it up! If you liked the guide, leave a reaction or comment any changes or fixes
|
||||
I should make below. Your feedback is greatly appreciated!
|
||||
@@ -12,6 +12,7 @@ export const collections = {
|
||||
}),
|
||||
image: z.string().optional(),
|
||||
imagePosition: z.string().optional(),
|
||||
isDraft: z.boolean().optional()
|
||||
}),
|
||||
}),
|
||||
projects: defineCollection({
|
||||
@@ -22,7 +23,7 @@ export const collections = {
|
||||
demoUrl: z.string().url().optional(),
|
||||
techStack: z.array(z.string()),
|
||||
date: z.string(),
|
||||
image: z.string().optional(),
|
||||
}),
|
||||
image: z.string().optional()
|
||||
}),
|
||||
})
|
||||
};
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,86 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,16 +1,20 @@
|
||||
---
|
||||
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>
|
||||
@@ -52,7 +56,9 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
|
||||
<main class="flex-1 flex flex-col">
|
||||
<div class="max-w-5xl mx-auto pt-12 px-4 py-8 flex-1">
|
||||
<Background layout="content" position="right" client:only="react" transition:persist />
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
<Background layout="content" position="left" client:only="react" transition:persist />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
70
src/src/layouts/resource.astro
Normal file
70
src/src/layouts/resource.astro
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
import "@/style/globals.css";
|
||||
import { ClientRouter } from "astro:transitions";
|
||||
|
||||
import Header from "@/components/header";
|
||||
import Footer from "@/components/footer";
|
||||
import Background from "@/components/background";
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const { title, description } = Astro.props;
|
||||
const ogImage = "https://timmypidashev.dev/og-image.jpg";
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{title}</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<!-- OpenGraph -->
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:image" content={ogImage} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<!-- Basic meta description for search engines -->
|
||||
<meta name="description" content={description} />
|
||||
<!-- Also used in OpenGraph for social media sharing -->
|
||||
<meta property="og:description" content={description} />
|
||||
<link rel="icon" type="image/jpeg" href="/me.jpeg" />
|
||||
<ClientRouter
|
||||
defaultTransition={false}
|
||||
handleFocus={false}
|
||||
/>
|
||||
<style>
|
||||
::view-transition-new(:root) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
::view-transition-old(:root) {
|
||||
animation: 90ms ease-out both fade-out;
|
||||
}
|
||||
@keyframes fade-out {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-foreground min-h-screen flex flex-col">
|
||||
<main class="flex-1 flex flex-col">
|
||||
<div class="max-w-5xl mx-auto pt-12 px-4 py-8 flex-1">
|
||||
<Background layout="content" position="right" client:only="react" transition:persist />
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
<Background layout="content" position="left" client:only="react" transition:persist />
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
document.addEventListener("astro:after-navigation", () => {
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -13,10 +13,10 @@ const { slug } = Astro.params;
|
||||
const posts = await getCollection("blog");
|
||||
const post = posts.find(post => post.slug === slug);
|
||||
|
||||
if (!post) {
|
||||
if (!post || post.data.isDraft === true) {
|
||||
return new Response(null, {
|
||||
status: 404,
|
||||
statusText: 'Not found'
|
||||
statusText: "Not found"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ const jsonLd = {
|
||||
>
|
||||
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
|
||||
<div class="relative max-w-8xl mx-auto">
|
||||
<article class="prose prose-lg mx-auto max-w-4xl">
|
||||
<article class="prose prose-invert prose-lg mx-auto max-w-4xl">
|
||||
{post.data.image && (
|
||||
<div class="-mx-4 sm:mx-0 mb-8">
|
||||
<Image
|
||||
@@ -97,7 +97,7 @@ const jsonLd = {
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div class="prose prose-invert max-w-none">
|
||||
<div class="prose prose-invert prose-lg max-w-none">
|
||||
<Content />
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
export const prerender = true;
|
||||
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
import ContentLayout from "@/layouts/content.astro";
|
||||
import { Comments } from "@/components/blog/comments";
|
||||
|
||||
@@ -59,7 +60,7 @@ const { Content } = await project.render();
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="prose prose-invert max-w-none">
|
||||
<div class="prose prose-invert prose-lg max-w-none">
|
||||
<Content />
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -27,3 +27,38 @@
|
||||
body {
|
||||
font-family: "ComicRegular";
|
||||
}
|
||||
|
||||
code[data-line-numbers] {
|
||||
counter-reset: line;
|
||||
}
|
||||
|
||||
code[data-line-numbers] > [data-line]::before {
|
||||
counter-increment: line;
|
||||
content: counter(line);
|
||||
|
||||
/* Other styling */
|
||||
display: inline-block;
|
||||
width: 0.75rem;
|
||||
margin-right: 2rem;
|
||||
text-align: right;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
|
||||
code[data-line-numbers-max-digits="2"] > [data-line]::before {
|
||||
width: 1.25rem;
|
||||
}
|
||||
|
||||
code[data-line-numbers-max-digits="3"] > [data-line]::before {
|
||||
width: 1.75rem;
|
||||
}
|
||||
|
||||
code[data-line-numbers-max-digits="4"] > [data-line]::before {
|
||||
width: 2.25rem;
|
||||
}
|
||||
|
||||
code {
|
||||
overflow-x: scroll !important;
|
||||
max-width: calc(82vw - 2rem) !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
@@ -41,10 +41,15 @@ module.exports = {
|
||||
"draw-line": {
|
||||
"0%": { "stroke-dashoffset": "100" },
|
||||
"100%": { "stroke-dashoffset": "0" }
|
||||
},
|
||||
"fade-in": {
|
||||
"0%": { opacity: "0" },
|
||||
"100%": { opacity: "1" }
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
"draw-line": "draw-line 0.6s ease-out forwards"
|
||||
"draw-line": "draw-line 0.6s ease-out forwards",
|
||||
"fade-in": "fade-in 0.3s ease-in-out forwards"
|
||||
},
|
||||
typography: (theme) => ({
|
||||
DEFAULT: {
|
||||
@@ -91,6 +96,56 @@ module.exports = {
|
||||
transition: "all 0.2s ease-in-out",
|
||||
},
|
||||
|
||||
// Code
|
||||
'code:not([data-language])': {
|
||||
color: theme('colors.purple.bright'),
|
||||
backgroundColor: '#282828',
|
||||
padding: '0',
|
||||
borderRadius: '0.25rem',
|
||||
fontFamily: 'Comic Code, monospace',
|
||||
fontWeight: '400',
|
||||
fontSize: 'inherit', // Match the parent text size
|
||||
'&::before': { content: 'none' },
|
||||
'&::after': { content: 'none' },
|
||||
},
|
||||
|
||||
'pre': {
|
||||
backgroundColor: '#282828',
|
||||
color: theme("colors.foreground"),
|
||||
borderRadius: '0.5rem',
|
||||
overflow: 'visible', // This allows the copy button to be positioned outside
|
||||
position: 'relative', // For the copy button positioning
|
||||
marginTop: '1.5rem', // Space for the copy button and language label
|
||||
fontSize: 'inherit', // Match the parent font size
|
||||
},
|
||||
|
||||
'pre code': {
|
||||
display: 'block',
|
||||
fontFamily: 'Comic Code, monospace',
|
||||
fontSize: '1em', // This will inherit from the prose-lg setting
|
||||
padding: '0',
|
||||
overflow: 'auto', // Enable horizontal scrolling
|
||||
whiteSpace: 'pre',
|
||||
'&::before': { content: 'none' },
|
||||
'&::after': { content: 'none' },
|
||||
},
|
||||
|
||||
|
||||
'[data-rehype-pretty-code-fragment]:nth-of-type(2) pre': {
|
||||
'[data-line]::before': {
|
||||
content: 'counter(line)',
|
||||
counterIncrement: 'line',
|
||||
display: 'inline-block',
|
||||
width: '1rem',
|
||||
marginRight: '1rem',
|
||||
textAlign: 'right',
|
||||
color: '#86e1fc',
|
||||
},
|
||||
'[data-highlighted-line]::before': {
|
||||
color: '#86e1fc',
|
||||
},
|
||||
},
|
||||
|
||||
// Bold
|
||||
strong: {
|
||||
color: theme("colors.orange.bright"),
|
||||
@@ -118,81 +173,6 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
|
||||
// Code
|
||||
'code:not([data-language])': {
|
||||
color: theme('colors.purple.bright'),
|
||||
backgroundColor: '#282828',
|
||||
padding: '0.2em 0.4em',
|
||||
borderRadius: '0.25rem',
|
||||
fontFamily: 'Comic Code, monospace',
|
||||
fontWeight: '400',
|
||||
'&::before': { content: 'none' },
|
||||
'&::after': { content: 'none' },
|
||||
},
|
||||
|
||||
'pre code': {
|
||||
display: 'grid', // This ensures line backgrounds stretch full width
|
||||
minWidth: '100%',
|
||||
fontFamily: 'Comic Code, monospace',
|
||||
fontSize: '0.875rem', // text-sm
|
||||
lineHeight: '1.7142857', // leading-6
|
||||
padding: '1rem', // p-4
|
||||
'&::before': { content: 'none' },
|
||||
'&::after': { content: 'none' },
|
||||
},
|
||||
|
||||
'.highlighted': {
|
||||
backgroundColor: theme('colors.foreground/5'),
|
||||
paddingLeft: '1rem',
|
||||
paddingRight: '1rem',
|
||||
marginLeft: '-1rem',
|
||||
marginRight: '-1rem',
|
||||
},
|
||||
|
||||
'.word': {
|
||||
backgroundColor: theme('colors.foreground/20'),
|
||||
padding: '0.2em',
|
||||
borderRadius: '0.25rem',
|
||||
},
|
||||
|
||||
|
||||
code: {
|
||||
color: theme("colors.purple.bright"),
|
||||
backgroundColor: "#282828", // A dark gray that works with black
|
||||
padding: "0.2em 0.4em",
|
||||
borderRadius: "0.25rem",
|
||||
fontWeight: "400",
|
||||
"&::before": {
|
||||
content: "\"\"",
|
||||
},
|
||||
"&::after": {
|
||||
content: "\"\"",
|
||||
},
|
||||
},
|
||||
|
||||
// Inline code
|
||||
"code::before": {
|
||||
content: "\"\"",
|
||||
},
|
||||
"code::after": {
|
||||
content: "\"\"",
|
||||
},
|
||||
|
||||
// Pre
|
||||
pre: {
|
||||
backgroundColor: "#282828",
|
||||
color: theme("colors.foreground"),
|
||||
code: {
|
||||
backgroundColor: "transparent",
|
||||
padding: "0",
|
||||
color: "inherit",
|
||||
fontSize: "inherit",
|
||||
fontWeight: "inherit",
|
||||
"&::before": { content: "none" },
|
||||
"&::after": { content: "none" },
|
||||
},
|
||||
},
|
||||
|
||||
// Horizontal rules
|
||||
hr: {
|
||||
borderColor: theme("colors.foreground"),
|
||||
@@ -226,6 +206,13 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
lg: {
|
||||
css: {
|
||||
"pre code": {
|
||||
fontSize: "1rem",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user