8 Commits

Author SHA1 Message Date
denshooter
edd8dc58ab Merge branch 'dev' into production
All checks were successful
CI / CD / test-build (push) Successful in 10m15s
CI / CD / deploy-dev (push) Has been skipped
CI / CD / deploy-production (push) Successful in 23s
2026-04-17 09:50:38 +02:00
denshooter
f17f0031a1 Merge branch 'dev' into production 2026-04-16 14:39:54 +02:00
denshooter
4d5dc1f8f9 Merge branch 'dev' into production
All checks were successful
CI / CD / test-build (push) Successful in 10m15s
CI / CD / deploy-dev (push) Has been skipped
CI / CD / deploy-production (push) Successful in 24s
2026-04-15 15:53:22 +02:00
denshooter
8397e5acf2 Merge dev into production
All checks were successful
CI / CD / test-build (push) Successful in 10m19s
CI / CD / deploy-dev (push) Has been skipped
CI / CD / deploy-production (push) Successful in 23s
2026-04-09 18:02:37 +02:00
denshooter
5bcaade558 Merge dev into production
All checks were successful
CI / CD / test-build (push) Successful in 10m14s
CI / CD / deploy-dev (push) Has been skipped
CI / CD / deploy-production (push) Successful in 23s
2026-04-09 17:23:29 +02:00
denshooter
aee811309b fix: scroll to top on locale switch and remove dashes from hero text
All checks were successful
CI / CD / test-build (push) Successful in 10m15s
CI / CD / deploy-dev (push) Has been skipped
CI / CD / deploy-production (push) Successful in 1m53s
- HeaderClient: track locale prop changes with useRef and call
  window.scrollTo on switch to reliably reset scroll position
- messages/en.json + de.json: replace em dash with comma and remove
  hyphens from Self-Hoster/Full-Stack in hero description

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 14:37:56 +01:00
denshooter
48a29cd872 fix: pass locale explicitly to Hero and force-dynamic on locale-sensitive API routes
All checks were successful
CI / CD / test-build (push) Successful in 10m11s
CI / CD / deploy-dev (push) Has been skipped
CI / CD / deploy-production (push) Successful in 1m28s
- Hero.tsx: pass locale prop directly to getTranslations instead of
  relying on setRequestLocale async storage, which can be lost during
  Next.js RSC streaming
- book-reviews route: replace revalidate=300 with force-dynamic to
  prevent cached English responses being served to German locale requests
- content/page route: add runtime=nodejs and force-dynamic (was missing
  both, violating CLAUDE.md API route conventions)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 13:43:26 +01:00
denshooter
c95fc3101b chore: merge branch 'dev' into 'production' (Release: Design Overhaul & Admin Redesign)
All checks were successful
CI / CD / test-build (push) Successful in 10m9s
CI / CD / deploy-dev (push) Has been skipped
CI / CD / deploy-production (push) Successful in 24s
2026-03-08 13:18:26 +01:00
19 changed files with 37 additions and 666 deletions

View File

@@ -62,18 +62,9 @@ jobs:
CONTAINER_NAME="portfolio-app-dev" CONTAINER_NAME="portfolio-app-dev"
HEALTH_PORT="3001" HEALTH_PORT="3001"
IMAGE_NAME="${{ env.DOCKER_IMAGE }}:dev" IMAGE_NAME="${{ env.DOCKER_IMAGE }}:dev"
BOT_CONTAINER="portfolio-discord-bot-dev"
BOT_IMAGE="portfolio-discord-bot:dev"
# Build discord-bot image # Check for existing container
echo "🏗️ Building discord-bot image..."
DOCKER_BUILDKIT=1 docker build \
-t $BOT_IMAGE \
./discord-presence-bot
# Check for existing containers
EXISTING_CONTAINER=$(docker ps -aq -f name=$CONTAINER_NAME || echo "") EXISTING_CONTAINER=$(docker ps -aq -f name=$CONTAINER_NAME || echo "")
EXISTING_BOT=$(docker ps -aq -f name=$BOT_CONTAINER || echo "")
# Ensure networks exist # Ensure networks exist
echo "🌐 Ensuring networks exist..." echo "🌐 Ensuring networks exist..."
@@ -87,15 +78,13 @@ jobs:
echo "⚠️ Production database not reachable, app will use fallbacks" echo "⚠️ Production database not reachable, app will use fallbacks"
fi fi
# Stop and remove existing containers # Stop and remove existing container
for C in $EXISTING_CONTAINER $EXISTING_BOT; do if [ ! -z "$EXISTING_CONTAINER" ]; then
if [ ! -z "$C" ]; then echo "🛑 Stopping existing container..."
echo "🛑 Stopping existing container $C..." docker stop $EXISTING_CONTAINER 2>/dev/null || true
docker stop $C 2>/dev/null || true docker rm $EXISTING_CONTAINER 2>/dev/null || true
docker rm $C 2>/dev/null || true sleep 3
sleep 3 fi
fi
done
# Ensure port is free # Ensure port is free
PORT_CONTAINER=$(docker ps -a --format "{{.ID}}\t{{.Ports}}" | grep -E "(:${HEALTH_PORT}->)" | awk '{print $1}' | head -1 || echo "") PORT_CONTAINER=$(docker ps -a --format "{{.ID}}\t{{.Ports}}" | grep -E "(:${HEALTH_PORT}->)" | awk '{print $1}' | head -1 || echo "")
@@ -106,18 +95,7 @@ jobs:
sleep 3 sleep 3
fi fi
# Start discord-bot container # Start new container
echo "🤖 Starting discord-bot container..."
docker run -d \
--name $BOT_CONTAINER \
--restart unless-stopped \
--network portfolio_net \
-e DISCORD_BOT_TOKEN="${DISCORD_BOT_TOKEN}" \
-e DISCORD_USER_ID="${DISCORD_USER_ID:-172037532370862080}" \
-e BOT_PORT=3001 \
$BOT_IMAGE
# Start new portfolio container
echo "🆕 Starting new dev container..." echo "🆕 Starting new dev container..."
docker run -d \ docker run -d \
--name $CONTAINER_NAME \ --name $CONTAINER_NAME \
@@ -181,8 +159,6 @@ jobs:
N8N_API_KEY: ${{ vars.N8N_API_KEY || '' }} N8N_API_KEY: ${{ vars.N8N_API_KEY || '' }}
DIRECTUS_URL: ${{ vars.DIRECTUS_URL || 'https://cms.dk0.dev' }} DIRECTUS_URL: ${{ vars.DIRECTUS_URL || 'https://cms.dk0.dev' }}
DIRECTUS_STATIC_TOKEN: ${{ secrets.DIRECTUS_STATIC_TOKEN || '' }} DIRECTUS_STATIC_TOKEN: ${{ secrets.DIRECTUS_STATIC_TOKEN || '' }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN || '' }}
DISCORD_USER_ID: ${{ vars.DISCORD_USER_ID || '172037532370862080' }}
- name: Cleanup - name: Cleanup
run: docker image prune -f run: docker image prune -f
@@ -233,12 +209,10 @@ jobs:
export ADMIN_SESSION_SECRET="${ADMIN_SESSION_SECRET}" export ADMIN_SESSION_SECRET="${ADMIN_SESSION_SECRET}"
export DIRECTUS_URL="${DIRECTUS_URL}" export DIRECTUS_URL="${DIRECTUS_URL}"
export DIRECTUS_STATIC_TOKEN="${DIRECTUS_STATIC_TOKEN}" export DIRECTUS_STATIC_TOKEN="${DIRECTUS_STATIC_TOKEN}"
export DISCORD_BOT_TOKEN="${DISCORD_BOT_TOKEN}"
export DISCORD_USER_ID="${DISCORD_USER_ID:-172037532370862080}"
# Start new containers via compose # Start new container via compose
echo "🆕 Starting new production containers..." echo "🆕 Starting new production container..."
docker compose -f $COMPOSE_FILE up -d --build portfolio discord-bot docker compose -f $COMPOSE_FILE up -d portfolio
# Wait for health # Wait for health
echo "⏳ Waiting for container to be healthy..." echo "⏳ Waiting for container to be healthy..."
@@ -300,8 +274,6 @@ jobs:
N8N_API_KEY: ${{ vars.N8N_API_KEY || '' }} N8N_API_KEY: ${{ vars.N8N_API_KEY || '' }}
DIRECTUS_URL: ${{ vars.DIRECTUS_URL || 'https://cms.dk0.dev' }} DIRECTUS_URL: ${{ vars.DIRECTUS_URL || 'https://cms.dk0.dev' }}
DIRECTUS_STATIC_TOKEN: ${{ secrets.DIRECTUS_STATIC_TOKEN || '' }} DIRECTUS_STATIC_TOKEN: ${{ secrets.DIRECTUS_STATIC_TOKEN || '' }}
DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN || '' }}
DISCORD_USER_ID: ${{ vars.DISCORD_USER_ID || '172037532370862080' }}
- name: Cleanup - name: Cleanup
run: docker image prune -f run: docker image prune -f

View File

@@ -123,8 +123,6 @@ N8N_SECRET_TOKEN=...
N8N_API_KEY=... N8N_API_KEY=...
DATABASE_URL=postgresql://... DATABASE_URL=postgresql://...
REDIS_URL=redis://... # optional REDIS_URL=redis://... # optional
DISCORD_BOT_TOKEN=... # Discord bot token for presence bot (replaces Lanyard)
DISCORD_USER_ID=172037532370862080 # Discord user ID to track
``` ```
## Adding a CMS-managed Section ## Adding a CMS-managed Section

View File

@@ -2,19 +2,6 @@ import type { Metadata } from "next";
import HomePageServer from "../_ui/HomePageServer"; import HomePageServer from "../_ui/HomePageServer";
import { getLanguageAlternates, toAbsoluteUrl } from "@/lib/seo"; import { getLanguageAlternates, toAbsoluteUrl } from "@/lib/seo";
const localeMetadata: Record<string, { title: string; description: string }> = {
de: {
title: "Dennis Konkol Webentwickler Osnabrück",
description:
"Dennis Konkol Software Engineer & Webentwickler in Osnabrück. Webentwicklung, Fullstack-Apps, Docker, Next.js, Flutter. Projekte ansehen und Kontakt aufnehmen.",
},
en: {
title: "Dennis Konkol Web Developer Osnabrück",
description:
"Dennis Konkol Software Engineer & Web Developer in Osnabrück, Germany. Web development, fullstack apps, Docker, Next.js, Flutter.",
},
};
export async function generateMetadata({ export async function generateMetadata({
params, params,
}: { }: {
@@ -22,10 +9,7 @@ export async function generateMetadata({
}): Promise<Metadata> { }): Promise<Metadata> {
const { locale } = await params; const { locale } = await params;
const languages = getLanguageAlternates({ pathWithoutLocale: "" }); const languages = getLanguageAlternates({ pathWithoutLocale: "" });
const meta = localeMetadata[locale] ?? localeMetadata.en;
return { return {
title: meta.title,
description: meta.description,
alternates: { alternates: {
canonical: toAbsoluteUrl(`/${locale}`), canonical: toAbsoluteUrl(`/${locale}`),
languages, languages,

View File

@@ -13,12 +13,7 @@ export async function generateMetadata({
}): Promise<Metadata> { }): Promise<Metadata> {
const { locale } = await params; const { locale } = await params;
const languages = getLanguageAlternates({ pathWithoutLocale: "projects" }); const languages = getLanguageAlternates({ pathWithoutLocale: "projects" });
const isDe = locale === "de";
return { return {
title: isDe ? "Projekte Dennis Konkol" : "Projects Dennis Konkol",
description: isDe
? "Webentwicklung, Fullstack-Apps und Mobile-Projekte von Dennis Konkol. Next.js, Flutter, Docker und mehr Osnabrück."
: "Web development, fullstack apps and mobile projects by Dennis Konkol. Next.js, Flutter, Docker and more Osnabrück.",
alternates: { alternates: {
canonical: toAbsoluteUrl(`/${locale}/projects`), canonical: toAbsoluteUrl(`/${locale}/projects`),
languages, languages,

View File

@@ -31,41 +31,20 @@ export default async function HomePageServer({ locale }: HomePageServerProps) {
return ( return (
<div className="min-h-screen"> <div className="min-h-screen">
<Script <Script
id={"structured-data-person"} id={"structured-data"}
type="application/ld+json" type="application/ld+json"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: JSON.stringify({ __html: JSON.stringify({
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "Person", "@type": "Person",
name: "Dennis Konkol", name: "Dennis Konkol",
alternateName: ["dk0", "denshooter"],
url: "https://dk0.dev", url: "https://dk0.dev",
jobTitle: "Software Engineer", jobTitle: "Software Engineer",
description:
locale === "de"
? "Software Engineer & Webentwickler in Osnabrück. Webentwicklung, Fullstack-Apps, Docker, Next.js, Flutter."
: "Software Engineer & Web Developer in Osnabrück, Germany. Web development, fullstack apps, Docker, Next.js, Flutter.",
address: { address: {
"@type": "PostalAddress", "@type": "PostalAddress",
addressLocality: "Osnabrück", addressLocality: "Osnabrück",
addressRegion: "Niedersachsen", addressCountry: "Germany",
addressCountry: "DE",
}, },
knowsAbout: [
"Webentwicklung",
"Web Development",
"Next.js",
"React",
"TypeScript",
"Flutter",
"Docker",
"DevOps",
"Self-Hosting",
"CI/CD",
"Fullstack Development",
"Softwareentwicklung",
"Informatik",
],
sameAs: [ sameAs: [
"https://github.com/Denshooter", "https://github.com/Denshooter",
"https://linkedin.com/in/dkonkol", "https://linkedin.com/in/dkonkol",
@@ -73,20 +52,6 @@ export default async function HomePageServer({ locale }: HomePageServerProps) {
}), }),
}} }}
/> />
<Script
id={"structured-data-website"}
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
"@context": "https://schema.org",
"@type": "WebSite",
name: "Dennis Konkol",
alternateName: "dk0.dev",
url: "https://dk0.dev",
inLanguage: ["de", "en"],
}),
}}
/>
<Header locale={locale} /> <Header locale={locale} />
{/* Spacer to prevent navbar overlap */} {/* Spacer to prevent navbar overlap */}
<div className="h-24 md:h-32" aria-hidden="true"></div> <div className="h-24 md:h-32" aria-hidden="true"></div>

View File

@@ -214,12 +214,7 @@ export default function ActivityFeed({
))} ))}
</div> </div>
</div> </div>
<a <div className="flex gap-4 relative z-10">
href={data.music.url}
target="_blank"
rel="noopener noreferrer"
className="flex gap-4 relative z-10"
>
<div className="w-16 h-16 rounded-lg overflow-hidden shrink-0 shadow-md relative group-hover:shadow-xl transition-shadow duration-500"> <div className="w-16 h-16 rounded-lg overflow-hidden shrink-0 shadow-md relative group-hover:shadow-xl transition-shadow duration-500">
<Image <Image
src={data.music.albumArt} src={data.music.albumArt}
@@ -230,10 +225,10 @@ export default function ActivityFeed({
/> />
</div> </div>
<div className="min-w-0 flex flex-col justify-center"> <div className="min-w-0 flex flex-col justify-center">
<p className="font-bold text-[#1DB954] dark:text-[#1DB954] text-base truncate leading-tight mb-1 hover:underline">{data.music.track}</p> <p className="font-bold text-[#1DB954] dark:text-[#1DB954] text-base truncate leading-tight mb-1">{data.music.track}</p>
<p className="text-sm text-stone-600 dark:text-white/60 truncate font-medium">{data.music.artist}</p> <p className="text-sm text-stone-600 dark:text-white/60 truncate font-medium">{data.music.artist}</p>
</div> </div>
</a> </div>
{/* Subtle Spotify branding gradient */} {/* Subtle Spotify branding gradient */}
<div className="absolute top-0 right-0 w-32 h-32 bg-[#1DB954]/5 blur-[60px] rounded-full -mr-16 -mt-16 pointer-events-none" /> <div className="absolute top-0 right-0 w-32 h-32 bg-[#1DB954]/5 blur-[60px] rounded-full -mr-16 -mt-16 pointer-events-none" />
</motion.div> </motion.div>

View File

@@ -49,33 +49,23 @@ export default async function RootLayout({
export const metadata: Metadata = { export const metadata: Metadata = {
metadataBase: new URL(getBaseUrl()), metadataBase: new URL(getBaseUrl()),
title: { title: {
default: "Dennis Konkol", default: "Dennis Konkol | Portfolio",
template: "%s | dk0", template: "%s | Dennis Konkol",
}, },
description: description:
"Dennis Konkol Software Engineer & Webentwickler in Osnabrück. Webentwicklung, Fullstack-Apps, Docker, Next.js, Flutter. Portfolio mit Projekten und Kontakt.", "Portfolio of Dennis Konkol, a student and software engineer based in Osnabrück, Germany. Passionate about technology, coding, and solving real-world problems.",
keywords: [ keywords: [
"Dennis Konkol", "Dennis Konkol",
"dk0",
"denshooter",
"Webentwicklung Osnabrück",
"Webentwicklung",
"Softwareentwicklung Osnabrück",
"Website erstellen Osnabrück",
"Web Design Osnabrück",
"Informatik Osnabrück",
"Software Engineer", "Software Engineer",
"Full Stack Developer",
"Frontend Developer Osnabrück",
"Next.js",
"React",
"TypeScript",
"Flutter",
"Docker",
"Self-Hosting",
"DevOps",
"Portfolio", "Portfolio",
"Student",
"Web Development",
"Full Stack Developer",
"Osnabrück", "Osnabrück",
"Germany",
"React",
"Next.js",
"TypeScript",
], ],
authors: [{ name: "Dennis Konkol", url: "https://dk0.dev" }], authors: [{ name: "Dennis Konkol", url: "https://dk0.dev" }],
creator: "Dennis Konkol", creator: "Dennis Konkol",
@@ -92,27 +82,26 @@ export const metadata: Metadata = {
}, },
}, },
openGraph: { openGraph: {
title: "Dennis Konkol", title: "Dennis Konkol | Portfolio",
description: description:
"Software Engineer & Webentwickler in Osnabrück. Next.js, Flutter, Docker, DevOps. Projekte ansehen und Kontakt aufnehmen.", "Explore my projects and contact me for collaboration opportunities!",
url: "https://dk0.dev", url: "https://dk0.dev",
siteName: "Dennis Konkol", siteName: "Dennis Konkol Portfolio",
images: [ images: [
{ {
url: "https://dk0.dev/api/og", url: "https://dk0.dev/api/og",
width: 1200, width: 1200,
height: 630, height: 630,
alt: "Dennis Konkol", alt: "Dennis Konkol Portfolio",
}, },
], ],
locale: "de_DE", locale: "en_US",
alternateLocale: ["en_US"],
type: "website", type: "website",
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
title: "Dennis Konkol", title: "Dennis Konkol | Portfolio",
description: "Software Engineer & Webentwickler in Osnabrück.", description: "Student & Software Engineer based in Osnabrück, Germany.",
images: ["https://dk0.dev/api/og"], images: ["https://dk0.dev/api/og"],
creator: "@denshooter", creator: "@denshooter",
}, },
@@ -121,9 +110,5 @@ export const metadata: Metadata = {
}, },
alternates: { alternates: {
canonical: "https://dk0.dev", canonical: "https://dk0.dev",
languages: {
de: "https://dk0.dev/de",
en: "https://dk0.dev/en",
},
}, },
}; };

View File

@@ -1 +0,0 @@
node_modules/

View File

@@ -1,17 +0,0 @@
FROM node:25-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --only=production && npm cache clean --force
COPY index.js .
USER node
EXPOSE 3001
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:3001/presence || exit 1
CMD ["node", "index.js"]

View File

@@ -1,110 +0,0 @@
const { Client, GatewayIntentBits, ActivityType } = require("discord.js");
const http = require("http");
const TOKEN = process.env.DISCORD_BOT_TOKEN;
const TARGET_USER_ID = process.env.DISCORD_USER_ID || "172037532370862080";
const PORT = parseInt(process.env.BOT_PORT || "3001", 10);
if (!TOKEN) {
console.error("DISCORD_BOT_TOKEN is required");
process.exit(1);
}
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildPresences,
],
});
let cachedData = {
discord_status: "offline",
listening_to_spotify: false,
spotify: null,
activities: [],
};
function updatePresence(guild) {
const member = guild.members.cache.get(TARGET_USER_ID);
if (!member || !member.presence) return;
const presence = member.presence;
cachedData.discord_status = presence.status || "offline";
cachedData.activities = presence.activities
? presence.activities
.filter((a) => a.type !== ActivityType.Custom)
.map((a) => ({
name: a.name,
type: a.type,
details: a.details || null,
state: a.state || null,
assets: a.assets
? {
large_image: a.assets.largeImage || null,
large_text: a.assets.largeText || null,
small_image: a.assets.smallImage || null,
small_text: a.assets.smallText || null,
}
: null,
timestamps: a.timestamps
? {
start: a.timestamps.start?.toISOString() || null,
end: a.timestamps.end?.toISOString() || null,
}
: null,
}))
: [];
const spotifyActivity = presence.activities
? presence.activities.find((a) => a.type === ActivityType.Listening && a.name === "Spotify")
: null;
if (spotifyActivity && spotifyActivity.syncId) {
cachedData.listening_to_spotify = true;
cachedData.spotify = {
song: spotifyActivity.details || "",
artist: spotifyActivity.state ? spotifyActivity.state.replace(/; /g, "; ") : "",
album: spotifyActivity.assets?.largeText || "",
album_art_url: spotifyActivity.assets?.largeImage
? `https://i.scdn.co/image/${spotifyActivity.assets.largeImage.replace("spotify:", "")}`
: null,
track_id: spotifyActivity.syncId || null,
};
} else {
cachedData.listening_to_spotify = false;
cachedData.spotify = null;
}
}
function updateAll() {
for (const guild of client.guilds.cache.values()) {
updatePresence(guild);
}
}
client.on("ready", () => {
console.log(`Bot online as ${client.user.tag}`);
client.user.setActivity("Watching Presence", { type: ActivityType.Watching });
updateAll();
});
client.on("presenceUpdate", () => {
updateAll();
});
const server = http.createServer((req, res) => {
if (req.method === "GET" && req.url === "/presence") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ data: cachedData }));
} else {
res.writeHead(404);
res.end("Not found");
}
});
server.listen(PORT, () => {
console.log(`HTTP endpoint listening on port ${PORT}`);
});
client.login(TOKEN);

View File

@@ -1,324 +0,0 @@
{
"name": "discord-presence-bot",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "discord-presence-bot",
"version": "1.0.0",
"dependencies": {
"discord.js": "^14.18.0"
}
},
"node_modules/@discordjs/builders": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz",
"integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/formatters": "^0.6.2",
"@discordjs/util": "^1.2.0",
"@sapphire/shapeshift": "^4.0.0",
"discord-api-types": "^0.38.40",
"fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.4",
"tslib": "^2.6.3"
},
"engines": {
"node": ">=16.11.0"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/collection": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=16.11.0"
}
},
"node_modules/@discordjs/formatters": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz",
"integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
"license": "Apache-2.0",
"dependencies": {
"discord-api-types": "^0.38.33"
},
"engines": {
"node": ">=16.11.0"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/rest": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.1.tgz",
"integrity": "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/collection": "^2.1.1",
"@discordjs/util": "^1.2.0",
"@sapphire/async-queue": "^1.5.3",
"@sapphire/snowflake": "^3.5.5",
"@vladfrangu/async_event_emitter": "^2.4.6",
"discord-api-types": "^0.38.40",
"magic-bytes.js": "^1.13.0",
"tslib": "^2.6.3",
"undici": "6.24.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/rest/node_modules/@sapphire/snowflake": {
"version": "3.5.5",
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz",
"integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==",
"license": "MIT",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@discordjs/util": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
"integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
"license": "Apache-2.0",
"dependencies": {
"discord-api-types": "^0.38.33"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/ws": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz",
"integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/collection": "^2.1.0",
"@discordjs/rest": "^2.5.1",
"@discordjs/util": "^1.1.0",
"@sapphire/async-queue": "^1.5.2",
"@types/ws": "^8.5.10",
"@vladfrangu/async_event_emitter": "^2.2.4",
"discord-api-types": "^0.38.1",
"tslib": "^2.6.2",
"ws": "^8.17.0"
},
"engines": {
"node": ">=16.11.0"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/@sapphire/async-queue": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
"license": "MIT",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@sapphire/shapeshift": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
"integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"lodash": "^4.17.21"
},
"engines": {
"node": ">=v16"
}
},
"node_modules/@sapphire/snowflake": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz",
"integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==",
"license": "MIT",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@types/node": {
"version": "25.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.19.0"
}
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@vladfrangu/async_event_emitter": {
"version": "2.4.7",
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
"integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
"license": "MIT",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/discord-api-types": {
"version": "0.38.47",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.47.tgz",
"integrity": "sha512-XgXQodHQBAE6kfD7kMvVo30863iHX1LHSqNq6MGUTDwIFCCvHva13+rwxyxVXDqudyApMNAd32PGjgVETi5rjA==",
"license": "MIT",
"workspaces": [
"scripts/actions/documentation"
]
},
"node_modules/discord.js": {
"version": "14.26.3",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.3.tgz",
"integrity": "sha512-XEKtYn28YFsiJ5l4fLRyikdbo6RD5oFyqfVHQlvXz2104JhH/E8slN28dbky05w3DCrJcNVWvhVvcJCTSl/KIg==",
"license": "Apache-2.0",
"dependencies": {
"@discordjs/builders": "^1.14.1",
"@discordjs/collection": "1.5.3",
"@discordjs/formatters": "^0.6.2",
"@discordjs/rest": "^2.6.1",
"@discordjs/util": "^1.2.0",
"@discordjs/ws": "^1.2.3",
"@sapphire/snowflake": "3.5.3",
"discord-api-types": "^0.38.40",
"fast-deep-equal": "3.1.3",
"lodash.snakecase": "4.1.1",
"magic-bytes.js": "^1.13.0",
"tslib": "^2.6.3",
"undici": "6.24.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash.snakecase": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
"license": "MIT"
},
"node_modules/magic-bytes.js": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz",
"integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==",
"license": "MIT"
},
"node_modules/ts-mixer": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/undici": {
"version": "6.24.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/undici-types": {
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"license": "MIT"
},
"node_modules/ws": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

@@ -1,12 +0,0 @@
{
"name": "discord-presence-bot",
"version": "1.0.0",
"private": true,
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"discord.js": "^14.18.0"
}
}

View File

@@ -103,33 +103,6 @@ services:
memory: 128M memory: 128M
cpus: '0.1' cpus: '0.1'
discord-bot:
build:
context: ./discord-presence-bot
dockerfile: Dockerfile
container_name: portfolio-discord-bot
restart: unless-stopped
environment:
- DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}
- DISCORD_USER_ID=${DISCORD_USER_ID:-172037532370862080}
- BOT_PORT=3001
networks:
- portfolio_net
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3001/presence"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
deploy:
resources:
limits:
memory: 128M
cpus: '0.25'
reservations:
memory: 64M
cpus: '0.1'
volumes: volumes:
portfolio_data: portfolio_data:
driver: local driver: local

View File

@@ -87,33 +87,6 @@ services:
retries: 5 retries: 5
start_period: 30s start_period: 30s
discord-bot:
build:
context: ./discord-presence-bot
dockerfile: Dockerfile
container_name: portfolio-discord-bot
restart: unless-stopped
environment:
- DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}
- DISCORD_USER_ID=${DISCORD_USER_ID:-172037532370862080}
- BOT_PORT=3001
networks:
- portfolio_net
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3001/presence"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
deploy:
resources:
limits:
memory: 128M
cpus: '0.25'
reservations:
memory: 64M
cpus: '0.1'
volumes: volumes:
portfolio_data: portfolio_data:
driver: local driver: local

View File

@@ -30,10 +30,6 @@ N8N_WEBHOOK_URL=https://n8n.dk0.dev
N8N_SECRET_TOKEN=your-n8n-secret-token N8N_SECRET_TOKEN=your-n8n-secret-token
N8N_API_KEY=your-n8n-api-key N8N_API_KEY=your-n8n-api-key
# Discord Presence Bot (replaces Lanyard)
DISCORD_BOT_TOKEN=your-discord-bot-token
DISCORD_USER_ID=172037532370862080
# Directus CMS (for i18n messages & content pages) # Directus CMS (for i18n messages & content pages)
DIRECTUS_URL=https://cms.dk0.dev DIRECTUS_URL=https://cms.dk0.dev
DIRECTUS_STATIC_TOKEN=your-static-token-here DIRECTUS_STATIC_TOKEN=your-static-token-here

View File

@@ -19,7 +19,6 @@ const eslintConfig = [
"coverage/**", "coverage/**",
"scripts/**", "scripts/**",
"next-env.d.ts", "next-env.d.ts",
"discord-presence-bot/**",
], ],
}, },
...compat.extends("next/core-web-vitals", "next/typescript"), ...compat.extends("next/core-web-vitals", "next/typescript"),

View File

@@ -34,7 +34,7 @@
"f2": "Docker Swarm & CI/CD", "f2": "Docker Swarm & CI/CD",
"f3": "Self-Hosted Infrastruktur" "f3": "Self-Hosted Infrastruktur"
}, },
"description": "Ich bin Dennis Konkol, Informatik-Student und Webentwickler aus Osnabrück. Ich entwickle Fullstack-Apps mit Next.js und Flutter und betreibe meine eigene Infrastruktur mit Docker und CI/CD.", "description": "Ich bin Dennis, Student aus Osnabrück und leidenschaftlicher Selfhoster. Ich entwickle Fullstack Apps und sorge am liebsten selbst dafür, dass sie auf meiner eigenen Infrastruktur perfekt laufen.",
"ctaWork": "Meine Projekte", "ctaWork": "Meine Projekte",
"ctaContact": "Kontakt" "ctaContact": "Kontakt"
}, },

View File

@@ -35,7 +35,7 @@
"f2": "Docker Swarm & CI/CD", "f2": "Docker Swarm & CI/CD",
"f3": "Self-Hosted Infrastructure" "f3": "Self-Hosted Infrastructure"
}, },
"description": "I'm Dennis Konkol, a computer science student and web developer from Osnabrück, Germany. I build fullstack apps with Next.js and Flutter and love running my own infrastructure with Docker and CI/CD.", "description": "I'm Dennis, a student from Germany and a passionate selfhoster. I build fullstack applications and love the challenge of managing the infrastructure they run on.",
"ctaWork": "View Projects", "ctaWork": "View Projects",
"ctaContact": "Get in touch" "ctaContact": "Get in touch"
}, },

View File

@@ -93,7 +93,7 @@
}, },
{ {
"parameters": { "parameters": {
"url": "http://discord-bot:3001/presence", "url": "https://api.lanyard.rest/v1/users/172037532370862080",
"options": {} "options": {}
}, },
"type": "n8n-nodes-base.httpRequest", "type": "n8n-nodes-base.httpRequest",