*  chore: update CI workflow to include testing and multi-arch build (#29)

*  chore: remove unused dependencies from package-lock.json and updated to a better local dev environment (#30)

*  test: add unit tests

*  test: add unit tests for whole project

*  feat: add whatwg-fetch for improved fetch support

*  chore: update Node.js version to 22 in workflow

*  refactor: update types and improve email handling tests

*  refactor: remove unused imports

*  fix: normalize image name to lowercase in workflows

*  fix: ensure Docker image names are consistently lowercase

*  chore: update

*  chore: update base URL to use secret variable

*  chore: update to login to ghcr

*  fix: add missing 'fi' to close if statement in workflow

* D branch 1 (#32)

* full upgrade (#31)

*  chore: update CI workflow to include testing and multi-arch build (#29)

*  chore: remove unused dependencies from package-lock.json and updated to a better local dev environment (#30)

*  test: add unit tests

*  test: add unit tests for whole project

*  feat: add whatwg-fetch for improved fetch support

*  chore: update Node.js version to 22 in workflow

*  refactor: update types and improve email handling tests

*  refactor: remove unused imports

*  fix: normalize image name to lowercase in workflows

*  fix: ensure Docker image names are consistently lowercase

*  chore: update

*  chore: update base URL to use secret variable

*  chore: update to login to ghcr

*  fix: add missing 'fi' to close if statement in workflow

* 🚀 fix: update Docker run commands to use specific network

* D branch 1 (#34)

* full upgrade (#31)

*  chore: update CI workflow to include testing and multi-arch build (#29)

*  chore: remove unused dependencies from package-lock.json and updated to a better local dev environment (#30)

*  test: add unit tests

*  test: add unit tests for whole project

*  feat: add whatwg-fetch for improved fetch support

*  chore: update Node.js version to 22 in workflow

*  refactor: update types and improve email handling tests

*  refactor: remove unused imports

*  fix: normalize image name to lowercase in workflows

*  fix: ensure Docker image names are consistently lowercase

*  chore: update

*  chore: update base URL to use secret variable

*  chore: update to login to ghcr

*  fix: add missing 'fi' to close if statement in workflow

* 🚀 fix: update Docker run commands to use specific network

*  fix: add error handling for invalid project data

* D branch 2 (#35)

* full upgrade (#31)

*  chore: update CI workflow to include testing and multi-arch build (#29)

*  chore: remove unused dependencies from package-lock.json and updated to a better local dev environment (#30)

*  test: add unit tests

*  test: add unit tests for whole project

*  feat: add whatwg-fetch for improved fetch support

*  chore: update Node.js version to 22 in workflow

*  refactor: update types and improve email handling tests

*  refactor: remove unused imports

*  fix: normalize image name to lowercase in workflows

*  fix: ensure Docker image names are consistently lowercase

*  chore: update

*  chore: update base URL to use secret variable

*  chore: update to login to ghcr

*  fix: add missing 'fi' to close if statement in workflow

*  fix: format code for better readability in Contact and Footer components

* D branch 2 (#36)

* full upgrade (#31)

*  chore: update CI workflow to include testing and multi-arch build (#29)

*  chore: remove unused dependencies from package-lock.json and updated to a better local dev environment (#30)

*  test: add unit tests

*  test: add unit tests for whole project

*  feat: add whatwg-fetch for improved fetch support

*  chore: update Node.js version to 22 in workflow

*  refactor: update types and improve email handling tests

*  refactor: remove unused imports

*  fix: normalize image name to lowercase in workflows

*  fix: ensure Docker image names are consistently lowercase

*  chore: update

*  chore: update base URL to use secret variable

*  chore: update to login to ghcr

*  fix: add missing 'fi' to close if statement in workflow

*  fix: format code for better readability in Contact and Footer components

* 🚀 fix: update Docker commands and remove hardcoded API URL

* Update main.yml

* Update main.yml

* Update main.yml

* D branch 1 (#37)

* full upgrade (#31)

*  chore: update CI workflow to include testing and multi-arch build (#29)

*  chore: remove unused dependencies from package-lock.json and updated to a better local dev environment (#30)

*  test: add unit tests

*  test: add unit tests for whole project

*  feat: add whatwg-fetch for improved fetch support

*  chore: update Node.js version to 22 in workflow

*  refactor: update types and improve email handling tests

*  refactor: remove unused imports

*  fix: normalize image name to lowercase in workflows

*  fix: ensure Docker image names are consistently lowercase

*  chore: update

*  chore: update base URL to use secret variable

*  chore: update to login to ghcr

*  fix: add missing 'fi' to close if statement in workflow

*  feat: display base URL in Hero component

* Update main.yml

* Update next.config.ts

* next.config.ts aktualisieren

* Update main.yml

*  chore: refactor environment variable handling in workflow

*  chore: update GitHub Actions workflow for improved security and caching

* 🚀 chore: update Trivy action version and enhance config

*  chore: update GitHub Actions workflows and add linter

* 🚫 chore: remove Docker image vulnerability scan step

*  chore: update environment variable logging in workflow

*  chore: add dynamic environment for deployment jobs

* 🚀 chore: set deployment environment to GitHub ref name

* 🎉 chore: remove environment variable exposure in CI/CD

*  chore: remove sensitive environment variable logging and update variable references

*  chore: log environment variables for debugging purposes

*  chore: create .env file for environment variables setup

*  feat: copy .env file to Docker image for config

*  refactor: update environment variables to public scope

*  chore: remove environment variable from Hero component

*  fix: update environment variable references in workflow

*  chore: add folder structure display to workflow steps

*  chore: reorder CI steps for improved workflow clarity

*  fix: remove unnecessary console logs and correct base URL variable
This commit is contained in:
denshooter
2025-02-17 09:58:58 +01:00
committed by GitHub
parent 180b9aa9f8
commit 0cbec0bb19
22 changed files with 642 additions and 526 deletions

55
.github/workflows/linter.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: Lint Code Base
on:
push:
branches:
- dev
- preview
- production
paths:
- 'app/**'
- 'public/**'
- 'styles/**'
- 'Dockerfile'
- 'docker-compose.yml'
- '.github/workflows/**'
- 'next.config.ts'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'tailwind.config.ts'
pull_request:
branches:
- dev
- preview
- production
paths:
- 'app/**'
- 'public/**'
- 'styles/**'
- 'Dockerfile'
- 'docker-compose.yml'
- '.github/workflows/**'
- 'next.config.ts'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'tailwind.config.ts'
jobs:
build:
name: Check and Lint Code Base
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Lint Code Base
uses: github/super-linter@v4
env:
VALIDATE_ALL_CODEBASE: false
DEFAULT_BRANCH: production
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -6,6 +6,18 @@ on:
- production - production
- dev - dev
- preview - preview
paths:
- 'app/**'
- 'public/**'
- 'styles/**'
- 'Dockerfile'
- 'docker-compose.yml'
- '.github/workflows/main.yml'
- 'next.config.ts'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
- 'tailwind.config.ts'
jobs: jobs:
test_and_build: test_and_build:
@@ -15,10 +27,32 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Node - name: Set up Node
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: '22' node-version: '22'
- name: Cache Node.js modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Create env file
run: |
touch .env
echo "NEXT_PUBLIC_BASE_URL=${{ vars.NEXT_PUBLIC_BASE_URL }}" >> .env
echo "NEXT_PUBLIC_GHOST_API_URL=${{ vars.NEXT_PUBLIC_GHOST_API_URL }}" >> .env
echo "NEXT_PUBLIC_GHOST_API_KEY=${{ secrets.NEXT_PUBLIC_GHOST_API_KEY }}" >> .env
echo "NEXT_PUBLIC_MY_EMAIL=${{ vars.NEXT_PUBLIC_MY_EMAIL }}" >> .env
echo "NEXT_PUBLIC_MY_PASSWORD=${{ secrets.NEXT_PUBLIC_MY_PASSWORD }}" >> .env
cat .env
- name: Show folder structure
run: |
ls -la
- name: Install Dependencies - name: Install Dependencies
run: npm install run: npm install
@@ -32,14 +66,12 @@ jobs:
- name: Build and Push Multi-Arch Docker Image - name: Build and Push Multi-Arch Docker Image
run: | run: |
IMAGE_NAME="ghcr.io/${{ github.repository_owner }}/my-nextjs-app:${{ github.ref_name }}" IMAGE_NAME="ghcr.io/${{ github.repository_owner }}/my-nextjs-app:${{ github.ref_name }}"
IMAGE_NAME=$(echo "$IMAGE_NAME" | tr '[:upper:]' '[:lower:]')
docker buildx create --use docker buildx create --use
docker buildx build \ docker buildx build \
--platform linux/arm64,linux/amd64 \ --platform linux/arm64 \
-t "$IMAGE_NAME" \ -t "$IMAGE_NAME" \
--push \ --push \
. .
deploy: deploy:
runs-on: self-hosted runs-on: self-hosted
needs: test_and_build needs: test_and_build
@@ -82,19 +114,19 @@ jobs:
docker rm -f "$NEW_CONTAINER_NAME" || true docker rm -f "$NEW_CONTAINER_NAME" || true
fi fi
echo "Deploying $CONTAINER_NAME with $IMAGE_NAME"
# Start new container on a temporary internal port # Start new container on a temporary internal port
docker run -d --name "$NEW_CONTAINER_NAME" -p 40000:3000 \ docker run -d --name "$NEW_CONTAINER_NAME" --network big-bear-ghost_ghost-network -p 40000:3000 \
-e GHOST_API_KEY="${{ secrets.GHOST_API_KEY }}" \
-e NEXT_PUBLIC_BASE_URL="${{ secrets.NEXT_PUBLIC_BASE_URL }}" \
-e MY_EMAIL="${{ secrets.MY_EMAIL }}" \
-e MY_PASSWORD="${{ secrets.MY_PASSWORD }}" \
-e GHOST_API_URL="${{ secrets.GHOST_API_URL }}" \
"$IMAGE_NAME" "$IMAGE_NAME"
# Wait for the new container to start # Wait for the new container to start
sleep 10 sleep 10
if [ "$(docker inspect --format='{{.State.Running}}' $NEW_CONTAINER_NAME)" = "true" ]; then # Debugging: Check if the environment variables are set correctly
docker exec "$NEW_CONTAINER_NAME" printenv
if [ "$(docker inspect --format='{{.State.Running}}' "$NEW_CONTAINER_NAME")" = "true" ]; then
# Stop/remove the old container # Stop/remove the old container
if [ "$(docker ps -aq -f name=$CONTAINER_NAME)" ]; then if [ "$(docker ps -aq -f name=$CONTAINER_NAME)" ]; then
docker stop "$CONTAINER_NAME" || true docker stop "$CONTAINER_NAME" || true
@@ -104,12 +136,8 @@ jobs:
# Replace the new container with final name/port # Replace the new container with final name/port
docker stop "$NEW_CONTAINER_NAME" || true docker stop "$NEW_CONTAINER_NAME" || true
docker rm "$NEW_CONTAINER_NAME" || true docker rm "$NEW_CONTAINER_NAME" || true
docker run -d --name "$CONTAINER_NAME" -p $PORT:3000 \
-e GHOST_API_KEY="${{ secrets.GHOST_API_KEY }}" \ docker run -d --name "$CONTAINER_NAME" --network big-bear-ghost_ghost-network -p $PORT:3000 \
-e NEXT_PUBLIC_BASE_URL="${{ secrets.NEXT_PUBLIC_BASE_URL }}" \
-e MY_EMAIL="${{ secrets.MY_EMAIL }}" \
-e MY_PASSWORD="${{ secrets.MY_PASSWORD }}" \
-e GHOST_API_URL="${{ secrets.GHOST_API_URL }}" \
"$IMAGE_NAME" "$IMAGE_NAME"
else else
echo "New container failed to start." echo "New container failed to start."

View File

@@ -13,6 +13,9 @@ RUN npm install
# Copy the application code # Copy the application code
COPY . . COPY . .
# Copy the .env file
COPY .env .env
# Build the Next.js application # Build the Next.js application
RUN npm run build RUN npm run build

View File

@@ -10,8 +10,8 @@ jest.mock('next/server', () => ({
beforeEach(() => { beforeEach(() => {
nodemailermock.mock.reset(); nodemailermock.mock.reset();
process.env.MY_EMAIL = 'test@dki.one'; process.env.NEXT_PUBLIC_MY_EMAIL = 'test@dki.one';
process.env.MY_PASSWORD = 'test-password'; process.env.NEXT_PUBLIC_MY_PASSWORD = 'test-password';
}); });
describe('POST /api/email', () => { describe('POST /api/email', () => {
@@ -35,8 +35,8 @@ describe('POST /api/email', () => {
}); });
it('should return an error if EMAIL or PASSWORD is missing', async () => { it('should return an error if EMAIL or PASSWORD is missing', async () => {
delete process.env.MY_EMAIL; delete process.env.NEXT_PUBLIC_MY_EMAIL;
delete process.env.MY_PASSWORD; delete process.env.NEXT_PUBLIC_MY_PASSWORD;
const mockRequest = { const mockRequest = {
json: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({

View File

@@ -10,8 +10,8 @@ jest.mock('next/server', () => ({
describe('GET /api/fetchAllProjects', () => { describe('GET /api/fetchAllProjects', () => {
beforeAll(() => { beforeAll(() => {
process.env.GHOST_API_URL = 'http://localhost:2368'; process.env.NEXT_PUBLIC_GHOST_API_URL = 'http://localhost:2368';
process.env.GHOST_API_KEY = 'some-key'; process.env.NEXT_PUBLIC_GHOST_API_KEY = 'some-key';
global.fetch = mockFetch({ global.fetch = mockFetch({
posts: [ posts: [
{ {

View File

@@ -10,8 +10,9 @@ jest.mock('next/server', () => ({
describe('GET /api/fetchProject', () => { describe('GET /api/fetchProject', () => {
beforeAll(() => { beforeAll(() => {
process.env.GHOST_API_URL = 'http://localhost:2368'; process.env.NEXT_PUBLIC_GHOST_API_URL = 'http://localhost:2368';
process.env.GHOST_API_KEY = 'some-key'; process.env.NEXT_PUBLIC_GHOST_API_KEY = 'some-key';
global.fetch = mockFetch({ global.fetch = mockFetch({
posts: [ posts: [
{ {

View File

@@ -7,8 +7,8 @@ jest.mock('next/server', () => ({
describe('GET /api/sitemap', () => { describe('GET /api/sitemap', () => {
beforeAll(() => { beforeAll(() => {
process.env.GHOST_API_URL = 'http://localhost:2368'; process.env.NEXT_PUBLIC_GHOST_API_URL = 'http://localhost:2368';
process.env.GHOST_API_KEY = 'test-api-key'; process.env.NEXT_PUBLIC_GHOST_API_KEY = 'test-api-key';
process.env.NEXT_PUBLIC_BASE_URL = 'https://dki.one'; process.env.NEXT_PUBLIC_BASE_URL = 'https://dki.one';
global.fetch = mockFetch({ global.fetch = mockFetch({
posts: [ posts: [

View File

@@ -6,8 +6,8 @@ import { mockFetch } from '@/app/__tests__/__mocks__/mock-fetch';
describe('Projects', () => { describe('Projects', () => {
beforeAll(() => { beforeAll(() => {
process.env.GHOST_API_URL = 'http://localhost:2368'; process.env.NEXT_PUBLIC_GHOST_API_URL = 'http://localhost:2368';
process.env.GHOST_API_KEY = 'some-key'; process.env.NEXT_PUBLIC_GHOST_API_KEY = 'some-key';
global.fetch = mockFetch({ global.fetch = mockFetch({
posts: [ posts: [
{ {

View File

@@ -13,8 +13,8 @@ jest.mock('next/navigation', () => ({
describe('ProjectDetails', () => { describe('ProjectDetails', () => {
beforeAll(() => { beforeAll(() => {
process.env.GHOST_API_URL = 'http://localhost:2368'; process.env.NEXT_PUBLIC_GHOST_API_URL = 'http://localhost:2368';
process.env.GHOST_API_KEY = 'some-key'; process.env.NEXT_PUBLIC_GHOST_API_KEY = 'some-key';
global.fetch = mockFetch({ global.fetch = mockFetch({
posts: [ posts: [
{ {

View File

@@ -11,8 +11,8 @@ export async function POST(request: NextRequest) {
}; };
const { email, name, message } = body; const { email, name, message } = body;
const user = process.env.MY_EMAIL ?? ""; const user = process.env.NEXT_PUBLIC_MY_EMAIL ?? "";
const pass = process.env.MY_PASSWORD ?? ""; const pass = process.env.NEXT_PUBLIC_MY_PASSWORD ?? "";
if (!user || !pass) { if (!user || !pass) {
console.error("Missing email/password environment variables"); console.error("Missing email/password environment variables");

View File

@@ -2,8 +2,8 @@ import { NextResponse } from "next/server";
export const runtime = "nodejs"; // Force Node runtime export const runtime = "nodejs"; // Force Node runtime
const GHOST_API_URL = process.env.GHOST_API_URL || "http://192.168.179.31:2368"; const GHOST_API_URL = process.env.NEXT_PUBLIC_GHOST_API_URL;
const GHOST_API_KEY = process.env.GHOST_API_KEY; const GHOST_API_KEY = process.env.NEXT_PUBLIC_GHOST_API_KEY;
export async function GET() { export async function GET() {
try { try {
@@ -15,6 +15,11 @@ export async function GET() {
return NextResponse.json([]); return NextResponse.json([]);
} }
const posts = await response.json(); const posts = await response.json();
if (!posts || !posts.posts) {
console.error("Invalid posts data");
return NextResponse.json([]);
}
return NextResponse.json(posts); return NextResponse.json(posts);
} catch (error) { } catch (error) {
console.error("Failed to fetch posts from Ghost:", error); console.error("Failed to fetch posts from Ghost:", error);

View File

@@ -2,8 +2,8 @@ import { NextResponse } from "next/server";
export const runtime = "nodejs"; // Force Node runtime export const runtime = "nodejs"; // Force Node runtime
const GHOST_API_URL = process.env.GHOST_API_URL || "http://192.168.179.31:2368"; const GHOST_API_URL = process.env.NEXT_PUBLIC_GHOST_API_URL;
const GHOST_API_KEY = process.env.GHOST_API_KEY; const GHOST_API_KEY = process.env.NEXT_PUBLIC_GHOST_API_KEY;
export async function GET(request: Request) { export async function GET(request: Request) {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);

View File

@@ -12,8 +12,8 @@ interface ProjectsData {
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export const runtime = "nodejs"; // Force Node runtime export const runtime = "nodejs"; // Force Node runtime
const GHOST_API_URL = process.env.GHOST_API_URL || "http://192.168.179.31:2368"; const GHOST_API_URL = process.env.NEXT_PUBLIC_GHOST_API_URL;
const GHOST_API_KEY = process.env.GHOST_API_KEY; const GHOST_API_KEY = process.env.NEXT_PUBLIC_GHOST_API_KEY;
// Funktion, um die XML für die Sitemap zu generieren // Funktion, um die XML für die Sitemap zu generieren
function generateXml(sitemapRoutes: { url: string; lastModified: string }[]) { function generateXml(sitemapRoutes: { url: string; lastModified: string }[]) {
@@ -38,7 +38,7 @@ function generateXml(sitemapRoutes: { url: string; lastModified: string }[]) {
} }
export async function GET() { export async function GET() {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://dki.one"; const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;
// Statische Routen // Statische Routen
const staticRoutes = [ const staticRoutes = [

View File

@@ -54,7 +54,6 @@ export default function Contact() {
setBanner((prev) => ({ ...prev, show: false })); setBanner((prev) => ({ ...prev, show: false }));
}, 3000); }, 3000);
} }
return ( return (
<section <section
id="contact" id="contact"

View File

@@ -1,82 +1,88 @@
import Link from "next/link"; import Link from "next/link";
import {useEffect, useState} from "react"; import { useEffect, useState } from "react";
export default function Footer() { export default function Footer() {
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
setIsVisible(true); setIsVisible(true);
}, 450); // Delay to start the animation }, 450); // Delay to start the animation
}, []); }, []);
const scrollToSection = (id: string) => { const scrollToSection = (id: string) => {
const element = document.getElementById(id); const element = document.getElementById(id);
if (element) { if (element) {
element.scrollIntoView({behavior: "smooth"}); element.scrollIntoView({ behavior: "smooth" });
} }
}; };
return ( return (
<footer <footer
className={`sticky- bottom-0 p-3 bg-gradient-to-br from-white/60 to-white/30 backdrop-blur-lg rounded-2xl shadow-xl text-center text-gray-800 ${isVisible ? "animate-fly-in" : "opacity-0"}`} className={`sticky- bottom-0 p-3 bg-gradient-to-br from-white/60 to-white/30 backdrop-blur-lg rounded-2xl shadow-xl text-center text-gray-800 ${isVisible ? "animate-fly-in" : "opacity-0"}`}
> >
<div className={`flex flex-col md:flex-row items-center justify-between`}> <div className={`flex flex-col md:flex-row items-center justify-between`}>
<div className={`flex-col items-center`}> <div className={`flex-col items-center`}>
<h1 className="md:text-xl font-bold">Thank You for Visiting</h1> <h1 className="md:text-xl font-bold">Thank You for Visiting</h1>
<p className="md:mt-1 text-lg"> <p className="md:mt-1 text-lg">
Connect with me on social platforms: Connect with me on social platforms:
</p> </p>
<div className="flex justify-center items-center space-x-4 mt-4"> <div className="flex justify-center items-center space-x-4 mt-4">
<Link aria-label={"Dennis Github"} href="https://github.com/Denshooter" target="_blank"> <Link
<svg aria-label={"Dennis Github"}
className="w-10 h-10" href="https://github.com/Denshooter"
fill="currentColor" target="_blank"
viewBox="0 0 24 24" >
> <svg
<path className="w-10 h-10"
d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.387.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.757-1.333-1.757-1.09-.746.083-.73.083-.73 1.205.084 1.84 1.237 1.84 1.237 1.07 1.835 2.807 1.305 3.492.997.108-.774.42-1.305.763-1.605-2.665-.305-5.466-1.332-5.466-5.93 0-1.31.467-2.38 1.235-3.22-.123-.303-.535-1.527.117-3.18 0 0 1.008-.322 3.3 1.23.957-.266 1.98-.4 3-.405 1.02.005 2.043.14 3 .405 2.29-1.552 3.297-1.23 3.297-1.23.653 1.653.24 2.877.118 3.18.77.84 1.233 1.91 1.233 3.22 0 4.61-2.803 5.62-5.475 5.92.43.37.823 1.1.823 2.22v3.293c0 .32.218.694.825.577C20.565 21.8 24 17.3 24 12c0-6.63-5.37-12-12-12z"/> fill="currentColor"
</svg> viewBox="0 0 24 24"
</Link> >
<Link aria-label={"Dennis Linked In"} href="https://linkedin.com/in/dkonkol" target="_blank"> <path d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.387.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.757-1.333-1.757-1.09-.746.083-.73.083-.73 1.205.084 1.84 1.237 1.84 1.237 1.07 1.835 2.807 1.305 3.492.997.108-.774.42-1.305.763-1.605-2.665-.305-5.466-1.332-5.466-5.93 0-1.31.467-2.38 1.235-3.22-.123-.303-.535-1.527.117-3.18 0 0 1.008-.322 3.3 1.23.957-.266 1.98-.4 3-.405 1.02.005 2.043.14 3 .405 2.29-1.552 3.297-1.23 3.297-1.23.653 1.653.24 2.877.118 3.18.77.84 1.233 1.91 1.233 3.22 0 4.61-2.803 5.62-5.475 5.92.43.37.823 1.1.823 2.22v3.293c0 .32.218.694.825.577C20.565 21.8 24 17.3 24 12c0-6.63-5.37-12-12-12z" />
<svg </svg>
className="w-10 h-10" </Link>
fill="currentColor" <Link
viewBox="0 0 24 24" aria-label={"Dennis Linked In"}
> href="https://linkedin.com/in/dkonkol"
<path target="_blank"
d="M19 0h-14c-2.76 0-5 2.24-5 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5v-14c0-2.76-2.24-5-5-5zm-11 19h-3v-10h3v10zm-1.5-11.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm13.5 11.5h-3v-5.5c0-1.38-1.12-2.5-2.5-2.5s-2.5 1.12-2.5 2.5v5.5h-3v-10h3v1.5c.83-1.17 2.17-1.5 3.5-1.5 2.48 0 4.5 2.02 4.5 4.5v5.5z"/> >
</svg> <svg
</Link> className="w-10 h-10"
</div> fill="currentColor"
</div> viewBox="0 0 24 24"
<div className="mt-4 md:absolute md:left-1/2 md:transform md:-translate-x-1/2"> >
<button <path d="M19 0h-14c-2.76 0-5 2.24-5 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5v-14c0-2.76-2.24-5-5-5zm-11 19h-3v-10h3v10zm-1.5-11.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm13.5 11.5h-3v-5.5c0-1.38-1.12-2.5-2.5-2.5s-2.5 1.12-2.5 2.5v5.5h-3v-10h3v1.5c.83-1.17 2.17-1.5 3.5-1.5 2.48 0 4.5 2.02 4.5 4.5v5.5z" />
onClick={() => scrollToSection("about")} </svg>
className="p-4 mt-4 md:px-4 md:my-6 text-white bg-gradient-to-r from-blue-500 to-purple-500 rounded-2xl hover:from-blue-600 hover:to-purple-600 transition" </Link>
> </div>
Back to Top </div>
</button> <div className="mt-4 md:absolute md:left-1/2 md:transform md:-translate-x-1/2">
</div> <button
<div className="flex-col"> onClick={() => scrollToSection("about")}
<div className="mt-4"> className="p-4 mt-4 md:px-4 md:my-6 text-white bg-gradient-to-r from-blue-500 to-purple-500 rounded-2xl hover:from-blue-600 hover:to-purple-600 transition"
<Link >
href="/privacy-policy" Back to Top
className="text-blue-800 transition-underline" </button>
> </div>
Privacy Policy <div className="flex-col">
</Link> <div className="mt-4">
<Link <Link
href="/legal-notice" href="/privacy-policy"
className="ml-4 text-blue-800 transition-underline" className="text-blue-800 transition-underline"
> >
Legal Notice Privacy Policy
</Link> </Link>
</div> <Link
href="/legal-notice"
className="ml-4 text-blue-800 transition-underline"
>
Legal Notice
</Link>
</div>
<p className="md:mt-4">© Dennis Konkol 2025</p> <p className="md:mt-4">© Dennis Konkol 2025</p>
</div> </div>
</div> </div>
</footer> </footer>
); );
} }

View File

@@ -1,73 +1,79 @@
import Link from "next/link"; import Link from "next/link";
import {useEffect, useState} from "react"; import { useEffect, useState } from "react";
export default function Footer_Back() { export default function Footer_Back() {
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
setIsVisible(true); setIsVisible(true);
}, 450); // Delay to start the animation }, 450); // Delay to start the animation
}, []); }, []);
return ( return (
<footer <footer
className={`p-3 bg-gradient-to-br from-white/60 to-white/30 backdrop-blur-lg rounded-2xl shadow-xl text-center text-gray-800 ${isVisible ? "animate-fly-in" : "opacity-0"}`} className={`p-3 bg-gradient-to-br from-white/60 to-white/30 backdrop-blur-lg rounded-2xl shadow-xl text-center text-gray-800 ${isVisible ? "animate-fly-in" : "opacity-0"}`}
> >
<div className={`flex flex-col md:flex-row items-center justify-between`}> <div className={`flex flex-col md:flex-row items-center justify-between`}>
<div className={`flex-col items-center`}> <div className={`flex-col items-center`}>
<p className="md:mt-1 text-lg"> <p className="md:mt-1 text-lg">
Connect with me on social platforms: Connect with me on social platforms:
</p> </p>
<div className="flex justify-center items-center space-x-4 mt-4"> <div className="flex justify-center items-center space-x-4 mt-4">
<Link aria-label={"Dennis Github"} href="https://github.com/Denshooter" target="_blank"> <Link
<svg aria-label={"Dennis Github"}
className="w-10 h-10" href="https://github.com/Denshooter"
fill="currentColor" target="_blank"
viewBox="0 0 24 24" >
> <svg
<path className="w-10 h-10"
d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.387.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.757-1.333-1.757-1.09-.746.083-.73.083-.73 1.205.084 1.84 1.237 1.84 1.237 1.07 1.835 2.807 1.305 3.492.997.108-.774.42-1.305.763-1.605-2.665-.305-5.466-1.332-5.466-5.93 0-1.31.467-2.38 1.235-3.22-.123-.303-.535-1.527.117-3.18 0 0 1.008-.322 3.3 1.23.957-.266 1.98-.4 3-.405 1.02.005 2.043.14 3 .405 2.29-1.552 3.297-1.23 3.297-1.23.653 1.653.24 2.877.118 3.18.77.84 1.233 1.91 1.233 3.22 0 4.61-2.803 5.62-5.475 5.92.43.37.823 1.1.823 2.22v3.293c0 .32.218.694.825.577C20.565 21.8 24 17.3 24 12c0-6.63-5.37-12-12-12z"/> fill="currentColor"
</svg> viewBox="0 0 24 24"
</Link> >
<Link aria-label={"Dennis Linked In"} href="https://linkedin.com/in/dkonkol" target="_blank"> <path d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.387.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.387-1.333-1.757-1.333-1.757-1.09-.746.083-.73.083-.73 1.205.084 1.84 1.237 1.84 1.237 1.07 1.835 2.807 1.305 3.492.997.108-.774.42-1.305.763-1.605-2.665-.305-5.466-1.332-5.466-5.93 0-1.31.467-2.38 1.235-3.22-.123-.303-.535-1.527.117-3.18 0 0 1.008-.322 3.3 1.23.957-.266 1.98-.4 3-.405 1.02.005 2.043.14 3 .405 2.29-1.552 3.297-1.23 3.297-1.23.653 1.653.24 2.877.118 3.18.77.84 1.233 1.91 1.233 3.22 0 4.61-2.803 5.62-5.475 5.92.43.37.823 1.1.823 2.22v3.293c0 .32.218.694.825.577C20.565 21.8 24 17.3 24 12c0-6.63-5.37-12-12-12z" />
<svg </svg>
className="w-10 h-10" </Link>
fill="currentColor" <Link
viewBox="0 0 24 24" aria-label={"Dennis Linked In"}
> href="https://linkedin.com/in/dkonkol"
<path target="_blank"
d="M19 0h-14c-2.76 0-5 2.24-5 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5v-14c0-2.76-2.24-5-5-5zm-11 19h-3v-10h3v10zm-1.5-11.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm13.5 11.5h-3v-5.5c0-1.38-1.12-2.5-2.5-2.5s-2.5 1.12-2.5 2.5v5.5h-3v-10h3v1.5c.83-1.17 2.17-1.5 3.5-1.5 2.48 0 4.5 2.02 4.5 4.5v5.5z"/> >
</svg> <svg
</Link> className="w-10 h-10"
</div> fill="currentColor"
</div> viewBox="0 0 24 24"
<div className="mt-4 md:absolute md:left-1/2 md:transform md:-translate-x-1/2"> >
<Link <path d="M19 0h-14c-2.76 0-5 2.24-5 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5v-14c0-2.76-2.24-5-5-5zm-11 19h-3v-10h3v10zm-1.5-11.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm13.5 11.5h-3v-5.5c0-1.38-1.12-2.5-2.5-2.5s-2.5 1.12-2.5 2.5v5.5h-3v-10h3v1.5c.83-1.17 2.17-1.5 3.5-1.5 2.48 0 4.5 2.02 4.5 4.5v5.5z" />
href={"/"} </svg>
className="p-4 mt-4 md:px-4 md:my-6 text-white bg-gradient-to-r from-blue-500 to-purple-500 rounded-2xl hover:from-blue-600 hover:to-purple-600 transition" </Link>
> </div>
Back to main page </div>
</Link> <div className="mt-4 md:absolute md:left-1/2 md:transform md:-translate-x-1/2">
</div> <Link
<div className="flex-col"> href={"/"}
<div className="mt-4"> className="p-4 mt-4 md:px-4 md:my-6 text-white bg-gradient-to-r from-blue-500 to-purple-500 rounded-2xl hover:from-blue-600 hover:to-purple-600 transition"
<Link >
href="/privacy-policy" Back to main page
className="text-blue-800 transition-underline" </Link>
> </div>
Privacy Policy <div className="flex-col">
</Link> <div className="mt-4">
<Link <Link
href="/legal-notice" href="/privacy-policy"
className="ml-4 text-blue-800 transition-underline" className="text-blue-800 transition-underline"
> >
Legal Notice Privacy Policy
</Link> </Link>
</div> <Link
<p className="md:mt-4">© Dennis Konkol 2025</p> href="/legal-notice"
</div> className="ml-4 text-blue-800 transition-underline"
</div> >
</footer> Legal Notice
); </Link>
</div>
<p className="md:mt-4">© Dennis Konkol 2025</p>
</div>
</div>
</footer>
);
} }

View File

@@ -1,132 +1,138 @@
"use client"; "use client";
import {useEffect, useState} from "react"; import { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
export default function Header() { export default function Header() {
const [isVisible, setIsVisible] = useState(false);
const [isVisible, setIsVisible] = useState(false); useEffect(() => {
setTimeout(() => {
setIsVisible(true);
}, 50); // Delay to start the animation after Projects
}, []);
useEffect(() => { const [isSidebarOpen, setIsSidebarOpen] = useState(false);
setTimeout(() => {
setIsVisible(true);
}, 50); // Delay to start the animation after Projects
}, []);
const [isSidebarOpen, setIsSidebarOpen] = useState(false); const toggleSidebar = () => {
setIsSidebarOpen(!isSidebarOpen);
};
const toggleSidebar = () => { const scrollToSection = (id: string) => {
setIsSidebarOpen(!isSidebarOpen); const element = document.getElementById(id);
}; if (element) {
element.scrollIntoView({ behavior: "smooth" });
} else {
/*go to main page and scroll*/
window.location.href = `/#${id}`;
}
};
const scrollToSection = (id: string) => { return (
const element = document.getElementById(id); <div className={`p-4 ${isVisible ? "animate-fly-in" : "opacity-0"}`}>
if (element) { <div
element.scrollIntoView({behavior: "smooth"}); className={`fixed top-4 left-4 right-4 p-4 bg-white/45 text-gray-700 backdrop-blur-md shadow-xl rounded-2xl z-50 ${isSidebarOpen ? "transform -translate-y-full" : ""}`}
} else { >
/*go to main page and scroll*/ <header className="w-full">
window.location.href = `/#${id}`; <nav className="flex flex-row items-center px-4">
} <Link href="/" className="flex justify-start">
}; <h1 className="text-xl md:text-2xl">Dennis Konkol</h1>
</Link>
return ( <div className="flex-grow"></div>
<div className={`p-4 ${isVisible ? 'animate-fly-in' : 'opacity-0'}`}> <button
<div className="text-gray-700 hover:text-gray-900 md:hidden"
className={`fixed top-4 left-4 right-4 p-4 bg-white/45 text-gray-700 backdrop-blur-md shadow-xl rounded-2xl z-50 ${isSidebarOpen ? 'transform -translate-y-full' : ''}`}> onClick={toggleSidebar}
<header className="w-full"> aria-label={"Open menu"}
<nav className="flex flex-row items-center px-4">
<Link href="/" className="flex justify-start">
<h1 className="text-xl md:text-2xl">Dennis Konkol</h1>
</Link>
<div className="flex-grow"></div>
<button
className="text-gray-700 hover:text-gray-900 md:hidden"
onClick={toggleSidebar}
aria-label={"Open menu"}
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
<div className="hidden md:flex space-x-4 md:space-x-6">
<button
onClick={() => scrollToSection("about")}
className="relative px-4 py-2 text-gray-700 hover:text-gray-900 text-xl md:text-2xl group">
About
</button>
<button
onClick={() => scrollToSection("projects")}
className="relative px-4 py-2 text-gray-700 hover:text-gray-900 text-xl md:text-2xl group">
Projects
</button>
<button
onClick={() => scrollToSection("contact")}
className="relative pl-4 py-2 text-gray-700 hover:text-gray-900 text-xl md:text-2xl group">
Contact
</button>
</div>
</nav>
</header>
</div>
<div
className={`fixed inset-0 bg-black bg-opacity-50 transition-opacity ${isSidebarOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}
onClick={toggleSidebar}
></div>
<div
className={`fixed z-10 top-0 right-0 h-full bg-white w-1/3 transform transition-transform flex flex-col ${isSidebarOpen ? 'translate-x-0' : 'translate-x-full'}`}
> >
<button <svg
aria-label={"Close menu"} className="w-6 h-6"
className="absolute top-4 right-4 text-gray-700 hover:text-gray-900" fill="none"
onClick={toggleSidebar} stroke="currentColor"
> viewBox="0 0 24 24"
<svg xmlns="http://www.w3.org/2000/svg"
className="w-6 h-6" >
fill="none" <path
stroke="currentColor" strokeLinecap="round"
viewBox="0 0 24 24" strokeLinejoin="round"
xmlns="http://www.w3.org/2000/svg" strokeWidth="2"
> d="M4 6h16M4 12h16M4 18h16"
<path />
strokeLinecap="round" </svg>
strokeLinejoin="round" </button>
strokeWidth="2" <div className="hidden md:flex space-x-4 md:space-x-6">
d="M6 18L18 6M6 6l12 12" <button
/> onClick={() => scrollToSection("about")}
</svg> className="relative px-4 py-2 text-gray-700 hover:text-gray-900 text-xl md:text-2xl group"
</button> >
<div className="pt-8 space-y-4 flex-grow"> About
<button </button>
onClick={() => scrollToSection("about")} <button
className="w-full px-4 py-2 pt-8 text-gray-700 hover:text-gray-900 text-xl md:text-2xl group"> onClick={() => scrollToSection("projects")}
About className="relative px-4 py-2 text-gray-700 hover:text-gray-900 text-xl md:text-2xl group"
</button> >
<button Projects
onClick={() => scrollToSection("projects")} </button>
className="w-full px-4 py-2 text-gray-700 hover:text-gray-900 text-xl md:text-2xl group"> <button
Projects onClick={() => scrollToSection("contact")}
</button> className="relative pl-4 py-2 text-gray-700 hover:text-gray-900 text-xl md:text-2xl group"
<button >
onClick={() => scrollToSection("contact")} Contact
className="w-full px-4 py-2 text-gray-700 hover:text-gray-900 text-xl md:text-2xl group"> </button>
Contact
</button>
</div>
<p className="text-center text-xs text-gray-500 p-4">© 2025 Dennis</p>
</div> </div>
</nav>
</header>
</div>
<div
className={`fixed inset-0 bg-black bg-opacity-50 transition-opacity ${isSidebarOpen ? "opacity-100" : "opacity-0 pointer-events-none"}`}
onClick={toggleSidebar}
></div>
<div
className={`fixed z-10 top-0 right-0 h-full bg-white w-1/3 transform transition-transform flex flex-col ${isSidebarOpen ? "translate-x-0" : "translate-x-full"}`}
>
<button
aria-label={"Close menu"}
className="absolute top-4 right-4 text-gray-700 hover:text-gray-900"
onClick={toggleSidebar}
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<div className="pt-8 space-y-4 flex-grow">
<button
onClick={() => scrollToSection("about")}
className="w-full px-4 py-2 pt-8 text-gray-700 hover:text-gray-900 text-xl md:text-2xl group"
>
About
</button>
<button
onClick={() => scrollToSection("projects")}
className="w-full px-4 py-2 text-gray-700 hover:text-gray-900 text-xl md:text-2xl group"
>
Projects
</button>
<button
onClick={() => scrollToSection("contact")}
className="w-full px-4 py-2 text-gray-700 hover:text-gray-900 text-xl md:text-2xl group"
>
Contact
</button>
</div> </div>
); <p className="text-center text-xs text-gray-500 p-4">© 2025 Dennis</p>
} </div>
</div>
);
}

View File

@@ -1,15 +1,14 @@
// app/components/Hero.tsx import React, { useEffect, useState } from "react";
import React, {useEffect, useState} from "react";
import Image from "next/image"; import Image from "next/image";
export default function Hero() { export default function Hero() {
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
setIsVisible(true); setIsVisible(true);
}, 150); // Delay to start the animation }, 150); // Delay to start the animation
}, []); }, []);
return ( return (
<div <div

View File

@@ -1,84 +1,88 @@
import React, {useEffect, useState} from "react"; import React, { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
interface Project { interface Project {
slug: string; slug: string;
id: string; id: string;
title: string; title: string;
feature_image: string; feature_image: string;
visibility: string; visibility: string;
published_at: string; published_at: string;
updated_at: string; updated_at: string;
html: string; html: string;
reading_time: number; reading_time: number;
meta_description: string; meta_description: string;
} }
interface ProjectsData { interface ProjectsData {
posts: Project[]; posts: Project[];
} }
export default function Projects() { export default function Projects() {
const [projects, setProjects] = useState<Project[]>([]); const [projects, setProjects] = useState<Project[]>([]);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
useEffect(() => { useEffect(() => {
const fetchProjects = async () => { const fetchProjects = async () => {
try { try {
const response = await fetch("/api/fetchAllProjects"); const response = await fetch("/api/fetchAllProjects");
if (!response.ok) { if (!response.ok) {
console.error(`Failed to fetch projects: ${response.statusText}`); console.error(`Failed to fetch projects: ${response.statusText}`);
return []; return [];
} }
const projectsData = (await response.json()) as ProjectsData; const projectsData = (await response.json()) as ProjectsData;
setProjects(projectsData.posts); if (!projectsData || !projectsData.posts) {
console.error("Invalid projects data");
return;
}
setProjects(projectsData.posts);
setTimeout(() => { setTimeout(() => {
setIsVisible(true); setIsVisible(true);
}, 250); // Delay to start the animation after Hero }, 250); // Delay to start the animation after Hero
} catch (error) { } catch (error) {
console.error("Failed to fetch projects:", error); console.error("Failed to fetch projects:", error);
} }
}; };
fetchProjects(); fetchProjects();
}, []); }, []);
const numberOfProjects = projects.length; const numberOfProjects = projects.length;
return ( return (
<section <section
id="projects" id="projects"
className={`p-10 ${isVisible ? "animate-fly-in" : "opacity-0"}`} className={`p-10 ${isVisible ? "animate-fly-in" : "opacity-0"}`}
> >
<h2 className="text-3xl font-bold text-center text-gray-800">Projects</h2> <h2 className="text-3xl font-bold text-center text-gray-800">Projects</h2>
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
{projects.map((project, index) => ( {projects.map((project, index) => (
<Link <Link
key={project.id} key={project.id}
href={{ href={{
pathname: `/projects/${project.slug}`, pathname: `/projects/${project.slug}`,
query: {project: JSON.stringify(project)}, query: { project: JSON.stringify(project) },
}} }}
className="cursor-pointer" className="cursor-pointer"
> >
<div <div
className={`p-4 border shadow-lg bg-white/45 rounded-2xl animate-fly-in`} className={`p-4 border shadow-lg bg-white/45 rounded-2xl animate-fly-in`}
style={{animationDelay: `${index * 0.1}s`}} style={{ animationDelay: `${index * 0.1}s` }}
> >
<h3 className="text-2xl font-bold text-gray-800"> <h3 className="text-2xl font-bold text-gray-800">
{project.title} {project.title}
</h3> </h3>
<p className="mt-2 text-gray-500">{project.meta_description}</p> <p className="mt-2 text-gray-500">{project.meta_description}</p>
</div>
</Link>
))}
<div
className={`p-4 border shadow-lg bg-white/45 rounded-2xl animate-fly-in`}
style={{animationDelay: `${(numberOfProjects + 1) * 0.1}s`}}
>
<h3 className="text-2xl font-bold text-gray-800">More to come</h3>
<p className="mt-2 text-gray-500">...</p>
</div>
</div> </div>
</section> </Link>
); ))}
<div
className={`p-4 border shadow-lg bg-white/45 rounded-2xl animate-fly-in`}
style={{ animationDelay: `${(numberOfProjects + 1) * 0.1}s` }}
>
<h3 className="text-2xl font-bold text-gray-800">More to come</h3>
<p className="mt-2 text-gray-500">...</p>
</div>
</div>
</section>
);
} }

View File

@@ -1,12 +1,12 @@
"use client"; "use client";
import { import {
useRouter, useRouter,
useSearchParams, useSearchParams,
useParams, useParams,
usePathname, usePathname,
} from "next/navigation"; } from "next/navigation";
import {useEffect, useState} from "react"; import { useEffect, useState } from "react";
import Link from "next/link"; import Link from "next/link";
@@ -16,154 +16,156 @@ import Image from "next/image";
import "@/app/styles/ghostContent.css"; // Import the global styles import "@/app/styles/ghostContent.css"; // Import the global styles
interface Project { interface Project {
slug: string; slug: string;
id: string; id: string;
title: string; title: string;
feature_image: string; feature_image: string;
visibility: string; visibility: string;
published_at: string; published_at: string;
updated_at: string; updated_at: string;
html: string; html: string;
reading_time: number; reading_time: number;
meta_description: string; meta_description: string;
} }
const ProjectDetails = () => { const ProjectDetails = () => {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const params = useParams(); const params = useParams();
const pathname = usePathname(); const pathname = usePathname();
const [project, setProject] = useState<Project | null>(null); const [project, setProject] = useState<Project | null>(null);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
setIsVisible(true); setIsVisible(true);
}, 150); // Delay to start the animation }, 150); // Delay to start the animation
}, []); }, []);
useEffect(() => { useEffect(() => {
const projectData = searchParams.get("project"); const projectData = searchParams.get("project");
if (projectData) { if (projectData) {
setProject(JSON.parse(projectData as string)); setProject(JSON.parse(projectData as string));
// Remove the project data from the URL without reloading the page // Remove the project data from the URL without reloading the page
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.searchParams.delete("project"); url.searchParams.delete("project");
window.history.replaceState({}, "", url.toString()); window.history.replaceState({}, "", url.toString());
} }
} else { } else {
// Fetch project data based on slug from URL // Fetch project data based on slug from URL
const slug = params.slug as string; const slug = params.slug as string;
try { try {
fetchProjectData(slug); fetchProjectData(slug);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setError("Failed to fetch project data"); setError("Failed to fetch project data");
} }
}
}, [searchParams, router, params, pathname]);
const fetchProjectData = async (slug: string) => {
try {
const response = await fetch(`/api/fetchProject?slug=${slug}`);
if (!response.ok) {
setError("Failed to fetch project Data");
}
const projectData = (await response.json()) as { posts: Project[] };
if (projectData.posts.length === 0) {
setError("Project not found");
}
setProject(projectData.posts[0]);
} catch (error) {
console.error("Failed to fetch project data:", error);
setError("Project not found");
}
};
if (error) {
return (
<div className="min-h-screen flex flex-col bg-radiant">
<Header/>
<div className="flex-grow flex items-center justify-center">
<div className="text-center p-10 bg-white dark:bg-gray-700 rounded shadow-md">
<h1 className="text-6xl font-bold text-gray-800 dark:text-white">
404
</h1>
<p className="mt-4 text-xl text-gray-600 dark:text-gray-300">
{error}
</p>
<Link
href="/"
className="mt-6 inline-block text-blue-500 hover:underline"
>
Go Back Home
</Link>
</div>
</div>
<Footer_Back/>
</div>
);
} }
}, [searchParams, router, params, pathname]);
if (!project) { const fetchProjectData = async (slug: string) => {
return ( try {
<div className="min-h-screen flex flex-col bg-radiant"> const response = await fetch(`/api/fetchProject?slug=${slug}`);
<Header/> if (!response.ok) {
<div className="flex-grow flex items-center justify-center"> setError("Failed to fetch project Data");
<div }
className="loader ease-linear rounded-full border-8 border-t-8 border-gray-200 h-32 w-32"></div> const projectData = (await response.json()) as { posts: Project[] };
</div> if (
<Footer_Back/> !projectData ||
</div> !projectData.posts ||
); projectData.posts.length === 0
) {
setError("Project not found");
}
setProject(projectData.posts[0]);
} catch (error) {
console.error("Failed to fetch project data:", error);
setError("Project not found");
} }
};
const featureImageUrl = project.feature_image if (error) {
? `/api/fetchImage?url=${encodeURIComponent(project.feature_image)}`
: "";
return ( return (
<div <div className="min-h-screen flex flex-col bg-radiant">
className={`min-h-screen flex flex-col bg-radiant ${isVisible ? "animate-fly-in" : "opacity-0"}`} <Header />
> <div className="flex-grow flex items-center justify-center">
<Header/> <div className="text-center p-10 bg-white dark:bg-gray-700 rounded shadow-md">
<div className="flex-grow"> <h1 className="text-6xl font-bold text-gray-800 dark:text-white">
<div className="flex justify-center mt-14 md:mt-28 px-4 md:px-0"> 404
{featureImageUrl && ( </h1>
<div className="relative w-full max-w-4xl h-0 pb-[56.25%] rounded-2xl overflow-hidden"> <p className="mt-4 text-xl text-gray-600 dark:text-gray-300">
<Image {error}
src={featureImageUrl} </p>
alt={project.title} <Link
fill href="/"
style={{objectFit: "cover"}} className="mt-6 inline-block text-blue-500 hover:underline"
className="rounded-2xl" >
priority={true} Go Back Home
/> </Link>
</div> </div>
)}
</div>
<div className="flex items-center justify-center mt-4">
<h1 className="text-4xl md:text-6xl font-bold text-gray-600">
{project.title}
</h1>
</div>
{/* Project Content */}
<div className="p-10 pt-12">
<div
className="flex flex-col p-8 bg-gradient-to-br from-white/60 to-white/30 backdrop-blur-lg rounded-2xl shadow-xl">
<div
className="content mt-4 text-gray-600 text-lg leading-relaxed"
dangerouslySetInnerHTML={{__html: project.html}}
></div>
</div>
</div>
</div>
<Footer_Back/>
</div> </div>
<Footer_Back />
</div>
); );
}
if (!project) {
return (
<div className="min-h-screen flex flex-col bg-radiant">
<Header />
<div className="flex-grow flex items-center justify-center">
<div className="loader ease-linear rounded-full border-8 border-t-8 border-gray-200 h-32 w-32"></div>
</div>
<Footer_Back />
</div>
);
}
const featureImageUrl = project.feature_image
? `/api/fetchImage?url=${encodeURIComponent(project.feature_image)}`
: "";
return (
<div
className={`min-h-screen flex flex-col bg-radiant ${isVisible ? "animate-fly-in" : "opacity-0"}`}
>
<Header />
<div className="flex-grow">
<div className="flex justify-center mt-14 md:mt-28 px-4 md:px-0">
{featureImageUrl && (
<div className="relative w-full max-w-4xl h-0 pb-[56.25%] rounded-2xl overflow-hidden">
<Image
src={featureImageUrl}
alt={project.title}
fill
style={{ objectFit: "cover" }}
className="rounded-2xl"
priority={true}
/>
</div>
)}
</div>
<div className="flex items-center justify-center mt-4">
<h1 className="text-4xl md:text-6xl font-bold text-gray-600">
{project.title}
</h1>
</div>
{/* Project Content */}
<div className="p-10 pt-12">
<div className="flex flex-col p-8 bg-gradient-to-br from-white/60 to-white/30 backdrop-blur-lg rounded-2xl shadow-xl">
<div
className="content mt-4 text-gray-600 text-lg leading-relaxed"
dangerouslySetInnerHTML={{ __html: project.html }}
></div>
</div>
</div>
</div>
<Footer_Back />
</div>
);
}; };
export default ProjectDetails; export default ProjectDetails;

View File

@@ -3,7 +3,7 @@ import {NextResponse} from "next/server";
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
export async function GET() { export async function GET() {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "https://dki.one"; const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;
const apiUrl = `${baseUrl}/api/sitemap`; // Verwende die vollständige URL zur API const apiUrl = `${baseUrl}/api/sitemap`; // Verwende die vollständige URL zur API
try { try {

View File

@@ -1,14 +1,16 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import dotenv from "dotenv"; import dotenv from "dotenv";
import path from "path";
dotenv.config(); // Lade die .env Datei aus dem Arbeitsverzeichnis
dotenv.config({ path: path.resolve(__dirname, '.env') });
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
env: { env: {
GHOST_API_KEY: process.env.GHOST_API_KEY, NEXT_PUBLIC_GHOST_API_KEY: process.env.NEXT_PUBLIC_GHOST_API_KEY,
GHOST_API_URL: process.env.GHOST_API_URL, NEXT_PUBLIC_GHOST_API_URL: process.env.NEXT_PUBLIC_GHOST_API_URL,
MY_EMAIL: process.env.MY_EMAIL, NEXT_PUBLIC_MY_EMAIL: process.env.NEXT_PUBLIC_MY_EMAIL,
MY_PASSWORD: process.env.MY_PASSWORD, NEXT_PUBLIC_MY_PASSWORD: process.env.NEXT_PUBLIC_MY_PASSWORD,
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL, NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
}, },
}; };