diff --git a/.woodpecker.yaml b/.woodpecker.yaml
new file mode 100644
index 0000000..9e8e810
--- /dev/null
+++ b/.woodpecker.yaml
@@ -0,0 +1,43 @@
+variables:
+ - &app_name "portfolio"
+
+when:
+ - event: release
+
+steps:
+ build-and-push:
+ image: woodpeckerci/plugin-docker-buildx
+ privileged: true
+ settings:
+ build_args:
+ - APP_VERSION=${CI_COMMIT_TAG}
+ platforms: linux/amd64
+ registry: git.georgew.dev
+ repo: git.georgew.dev/georgew/${CI_REPO_NAME}
+ tags:
+ - latest
+ - ${CI_COMMIT_TAG##v}
+ dockerfile: Dockerfile
+ username:
+ from_secret: FORGEJO_USER
+ password:
+ from_secret: FORGEJO_TOKEN
+
+ deploy:
+ image: docker:28-cli
+ privileged: true
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ - /home/george:/home/george
+ environment:
+ APP_NAME: *app_name
+ FORGEJO_USER:
+ from_secret: FORGEJO_USER
+ FORGEJO_TOKEN:
+ from_secret: FORGEJO_TOKEN
+ commands:
+ - echo $FORGEJO_TOKEN | docker login git.georgew.dev -u $FORGEJO_USER --password-stdin
+ - mkdir -p /home/george/$APP_NAME
+ - cp docker-compose.yaml /home/george/$APP_NAME/
+ - docker compose -p $APP_NAME -f /home/george/$APP_NAME/docker-compose.yaml pull
+ - docker compose -p $APP_NAME -f /home/george/$APP_NAME/docker-compose.yaml up -d --force-recreate --remove-orphans
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..d67a19b
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,32 @@
+FROM node:20-alpine AS deps
+RUN apk add --no-cache libc6-compat
+WORKDIR /app
+COPY package.json package-lock.json ./
+RUN npm ci
+
+FROM node:20-alpine AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+ARG APP_VERSION=v0.0.0
+ENV NEXT_PUBLIC_APP_VERSION=$APP_VERSION
+ENV NEXT_TELEMETRY_DISABLED 1
+RUN npm run build
+
+
+FROM node:20-alpine AS runner
+WORKDIR /app
+ENV NODE_ENV production
+ENV NEXT_TELEMETRY_DISABLED 1
+
+RUN addgroup --system --gid 1001 nodejs
+RUN adduser --system --uid 1001 nextjs
+
+COPY --from=builder /app/public ./public
+COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
+
+USER nextjs
+EXPOSE 3000
+ENV PORT 3000
+CMD ["node", "server.js"]
\ No newline at end of file
diff --git a/app/page.tsx b/app/page.tsx
index 295f8fd..f4dfa86 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,65 +1,151 @@
-import Image from "next/image";
+"use client";
+import { motion } from "framer-motion";
+import { Server, Globe, Smartphone, Gamepad2, Activity } from "lucide-react";
export default function Home() {
return (
-
-
-
-
-
- To get started, edit the page.tsx file.
-
-
- Looking for a starting point or more instructions? Head over to{" "}
-
- Templates
- {" "}
- or the{" "}
-
- Learning
- {" "}
- center.
+
+ {/* Header section */}
+
+
+ {/* Bento Grid */}
+
+
+ {/* About Me - Large Card */}
+
+
+
The Architect
+
+ Engineering high-scale web systems and mobile experiences.
+ Passionate about self-hosting, clean architecture, and performance.
+
+
+
+ #NextJS #Flutter #Docker
+
+
+
+ {/* Live Pulse Card */}
+
+ {/* The Monitor Map (Easily editable) */}
+ {(() => {
+ const monitors = [
+ { id: 2, name: "Datasaur" },
+ { id: 6, name: "Audiobookshelf" },
+ { id: 7, name: "Woodpecker CI" },
+ { id: 8, name: "Forgejo Git" },
+ { id: 9, name: "Server dashboard" },
+ { id: 10, name: "Ratoong" },
+ ];
+
+ return (
+ <>
+ {/* Default View */}
+
+
+
+
+
System Status:
+

+
+
+
+
+
+ {/* Hover View: Friendly Names */}
+
+
Service Registry
+
+ {monitors.map((m) => (
+
+
{m.name}
+
+

+

+
+
+ ))}
+
+
+ >
+ );
+ })()}
+
+
+ {/* Project One */}
+
+
+ Prod Website
+ Case Study 01
+
+
+ {/* Project Two */}
+
+
+ Mobile App
+ Active Dev
+
+
+ {/* Game Teaser / The Lab */}
+
+
+
+
+
+
The Forge
+
Indie Game Dev & Prototypes
+
+
+
+
+
+ {/* Deployment Footer */}
+
-
+
+
);
-}
+}
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..0ad3c65
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,15 @@
+services:
+ app:
+ image: git.georgew.dev/georgew/portfolio:latest
+ container_name: portfolio_app
+ restart: unless-stopped
+ ports:
+ - "3000:3000"
+ environment:
+ - NODE_ENV=production
+ networks:
+ - web_traffic
+
+networks:
+ web_traffic:
+ external: true
\ No newline at end of file
diff --git a/next.config.ts b/next.config.ts
index e9ffa30..b9f4418 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,7 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
- /* config options here */
+ output: 'standalone'
};
export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
index 67a4d01..e17a53b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,8 @@
"name": "portfolio",
"version": "0.1.0",
"dependencies": {
+ "framer-motion": "^12.29.2",
+ "lucide-react": "^0.563.0",
"next": "16.1.4",
"react": "19.2.3",
"react-dom": "19.2.3"
@@ -3585,6 +3587,33 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/framer-motion": {
+ "version": "12.29.2",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.29.2.tgz",
+ "integrity": "sha512-lSNRzBJk4wuIy0emYQ/nfZ7eWhqud2umPKw2QAQki6uKhZPKm2hRQHeQoHTG9MIvfobb+A/LbEWPJU794ZUKrg==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.29.2",
+ "motion-utils": "^12.29.2",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -4833,6 +4862,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "0.563.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz",
+ "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -4900,6 +4938,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/motion-dom": {
+ "version": "12.29.2",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.29.2.tgz",
+ "integrity": "sha512-/k+NuycVV8pykxyiTCoFzIVLA95Nb1BFIVvfSu9L50/6K6qNeAYtkxXILy/LRutt7AzaYDc2myj0wkCVVYAPPA==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.29.2"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.29.2",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz",
+ "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
diff --git a/package.json b/package.json
index 0bd0e85..dbe54e1 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,8 @@
"lint": "eslint"
},
"dependencies": {
+ "framer-motion": "^12.29.2",
+ "lucide-react": "^0.563.0",
"next": "16.1.4",
"react": "19.2.3",
"react-dom": "19.2.3"