Initial commit, with basic home page and CICD setup
Some checks failed
ci/woodpecker/release/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/release/woodpecker Pipeline failed
This commit is contained in:
parent
e4041eec0d
commit
7ef9a86d3e
43
.woodpecker.yaml
Normal file
43
.woodpecker.yaml
Normal file
|
|
@ -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
|
||||||
32
Dockerfile
Normal file
32
Dockerfile
Normal file
|
|
@ -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"]
|
||||||
204
app/page.tsx
204
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() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
<main className="min-h-screen bg-[#0a0a0a] text-white p-6 md:p-12 lg:p-24">
|
||||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
{/* Header section */}
|
||||||
<Image
|
<header className="mb-12">
|
||||||
className="dark:invert"
|
<h1 className="text-4xl font-bold tracking-tight">George W.</h1>
|
||||||
src="/next.svg"
|
<p className="text-neutral-400 mt-2">Senior Full Stack Engineer & Tech Lead</p>
|
||||||
alt="Next.js logo"
|
</header>
|
||||||
width={100}
|
|
||||||
height={20}
|
{/* Bento Grid */}
|
||||||
priority
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 auto-rows-[180px]">
|
||||||
/>
|
|
||||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
{/* About Me - Large Card */}
|
||||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
<motion.div
|
||||||
To get started, edit the page.tsx file.
|
whileHover={{ y: -5 }}
|
||||||
</h1>
|
className="md:col-span-2 md:row-span-2 p-8 rounded-3xl bg-neutral-900 border border-neutral-800 flex flex-col justify-between"
|
||||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
>
|
||||||
Looking for a starting point or more instructions? Head over to{" "}
|
<div>
|
||||||
<a
|
<h2 className="text-2xl font-semibold mb-4">The Architect</h2>
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
<p className="text-neutral-400 leading-relaxed">
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
Engineering high-scale web systems and mobile experiences.
|
||||||
>
|
Passionate about self-hosting, clean architecture, and performance.
|
||||||
Templates
|
</p>
|
||||||
</a>{" "}
|
</div>
|
||||||
or the{" "}
|
<div className="flex gap-2 text-xs font-mono text-neutral-500">
|
||||||
<a
|
<span>#NextJS</span> <span>#Flutter</span> <span>#Docker</span>
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
</div>
|
||||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
</motion.div>
|
||||||
>
|
|
||||||
Learning
|
{/* Live Pulse Card */}
|
||||||
</a>{" "}
|
<motion.div
|
||||||
center.
|
whileHover={{ y: -5 }}
|
||||||
|
className="group md:col-span-2 p-6 rounded-3xl bg-neutral-900 border border-neutral-800 flex flex-col justify-center relative overflow-hidden transition-all duration-300 min-h-[180px]"
|
||||||
|
>
|
||||||
|
{/* 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 */}
|
||||||
|
<div className="flex items-center justify-between w-full group-hover:opacity-0 group-hover:pointer-events-none transition-opacity duration-300">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
|
||||||
|
<span className="font-medium text-white">Hetzner Node-01</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<p className="text-sm text-neutral-500">System Status:</p>
|
||||||
|
<img
|
||||||
|
src="https://status.georgew.dev/api/status-page/dashboard/badge"
|
||||||
|
alt="Overall Status"
|
||||||
|
className="h-5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Activity className="text-neutral-700 w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hover View: Friendly Names */}
|
||||||
|
<div className="absolute inset-0 p-6 opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-center bg-neutral-900/95 backdrop-blur-sm">
|
||||||
|
<h4 className="text-[10px] font-mono text-neutral-500 mb-3 uppercase tracking-[0.2em]">Service Registry</h4>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
|
{monitors.map((m) => (
|
||||||
|
<div key={m.id} className="flex items-center justify-between bg-neutral-800/40 p-2 rounded-lg border border-neutral-700/30">
|
||||||
|
<span className="text-[11px] font-medium text-neutral-300 truncate mr-2">{m.name}</span>
|
||||||
|
<div className="flex gap-1 shrink-0">
|
||||||
|
<img src={`https://status.georgew.dev/api/badge/${m.id}/status`} className="h-3" alt="up" />
|
||||||
|
<img src={`https://status.georgew.dev/api/badge/${m.id}/avg-response/24`} className="h-3 opacity-60" alt="ms" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Project One */}
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
className="md:col-span-1 p-6 rounded-3xl bg-[#1a1a1a] border border-neutral-800 flex flex-col justify-end group cursor-pointer"
|
||||||
|
>
|
||||||
|
<Globe className="mb-auto text-blue-400" />
|
||||||
|
<h3 className="font-semibold mt-4">Prod Website</h3>
|
||||||
|
<p className="text-xs text-neutral-500">Case Study 01</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Project Two */}
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
className="md:col-span-1 p-6 rounded-3xl bg-[#1a1a1a] border border-neutral-800 flex flex-col justify-end cursor-pointer"
|
||||||
|
>
|
||||||
|
<Smartphone className="mb-auto text-purple-400" />
|
||||||
|
<h3 className="font-semibold mt-4">Mobile App</h3>
|
||||||
|
<p className="text-xs text-neutral-500">Active Dev</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Game Teaser / The Lab */}
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
className="md:col-span-2 p-6 rounded-3xl bg-gradient-to-br from-[#111] to-[#1a1a1a] border border-neutral-800 flex items-center gap-6"
|
||||||
|
>
|
||||||
|
<div className="p-4 rounded-2xl bg-neutral-800">
|
||||||
|
<Gamepad2 className="w-8 h-8 text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold italic">The Forge</h3>
|
||||||
|
<p className="text-sm text-neutral-500">Indie Game Dev & Prototypes</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Deployment Footer */}
|
||||||
|
<footer className="mt-12 pt-8 border-t border-neutral-900 flex flex-col md:flex-row justify-between items-center gap-4 text-[10px] text-neutral-600 font-mono tracking-wider uppercase">
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p>Pipeline Status</p>
|
||||||
|
<img
|
||||||
|
src="https://ci.georgew.dev/api/badges/11/status.svg"
|
||||||
|
alt="Build Status"
|
||||||
|
className="h-3 grayscale opacity-50 hover:opacity-100 hover:grayscale-0 transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:block w-[1px] h-3 bg-neutral-800" />
|
||||||
|
<p>Engine: Next.js 15 (Standalone)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 bg-neutral-900/50 px-3 py-1 rounded-full border border-neutral-800/50">
|
||||||
|
<div className="w-1 h-1 rounded-full bg-blue-500" />
|
||||||
|
<p className="text-neutral-400">
|
||||||
|
Deploy: {process.env.NEXT_PUBLIC_APP_VERSION || 'v1.0.0-dev'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
</footer>
|
||||||
<a
|
</main>
|
||||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Deploy Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Documentation
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
15
docker-compose.yaml
Normal file
15
docker-compose.yaml
Normal file
|
|
@ -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
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
output: 'standalone'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
|
||||||
53
package-lock.json
generated
53
package-lock.json
generated
|
|
@ -8,6 +8,8 @@
|
||||||
"name": "portfolio",
|
"name": "portfolio",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"framer-motion": "^12.29.2",
|
||||||
|
"lucide-react": "^0.563.0",
|
||||||
"next": "16.1.4",
|
"next": "16.1.4",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
|
|
@ -3585,6 +3587,33 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
|
@ -4833,6 +4862,15 @@
|
||||||
"yallist": "^3.0.2"
|
"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": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
|
|
@ -4900,6 +4938,21 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@
|
||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"framer-motion": "^12.29.2",
|
||||||
|
"lucide-react": "^0.563.0",
|
||||||
"next": "16.1.4",
|
"next": "16.1.4",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3"
|
"react-dom": "19.2.3"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue