Dev (#38)
* ✨ 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:
55
.github/workflows/linter.yml
vendored
Normal file
55
.github/workflows/linter.yml
vendored
Normal 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 }}
|
||||||
62
.github/workflows/main.yml
vendored
62
.github/workflows/main.yml
vendored
@@ -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."
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user