From 9f4c055b2365d18d078e9761cf4ceb878bada405 Mon Sep 17 00:00:00 2001 From: denshooter <44590296+denshooter@users.noreply.github.com> Date: Tue, 18 Feb 2025 19:04:01 +0100 Subject: [PATCH 1/6] Dev (#41) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚀 refactor: simplify deployment process in workflow file * 🚀 chore: add IMAGE_NAME to GITHUB_ENV for deployment workflow * ✨ chore: simplify deployment logging in workflow file --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d797da8..f70058d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -43,8 +43,8 @@ jobs: - name: Zero-Downtime Deployment run: | CONTAINER_NAME="nextjs-$DEPLOY_ENV" - - echo "Deploying $CONTAINER_NAME with $IMAGE_NAME" + + echo "Deploying $CONTAINER_NAME" if [ "$(docker inspect --format='{{.State.Running}}' "$NEW_CONTAINER_NAME")" = "true" ]; then docker stop "$CONTAINER_NAME" || true From 4aa0ba662bb78f2aae27c08caa42bf5e2f5cf967 Mon Sep 17 00:00:00 2001 From: denshooter <44590296+denshooter@users.noreply.github.com> Date: Fri, 21 Feb 2025 16:31:14 +0100 Subject: [PATCH 2/6] dev (#42) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚀 refactor: simplify deployment process in workflow file * 🚀 chore: add IMAGE_NAME to GITHUB_ENV for deployment workflow * ✨ chore: simplify deployment logging in workflow file * 🚀 fix: correct container name in deployment script logic --- .github/workflows/deploy.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f70058d..ef428fb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -46,11 +46,13 @@ jobs: echo "Deploying $CONTAINER_NAME" - if [ "$(docker inspect --format='{{.State.Running}}' "$NEW_CONTAINER_NAME")" = "true" ]; then + if [ "$(docker inspect --format='{{.State.Running}}' "$CONTAINER_NAME")" = "true" ]; then docker stop "$CONTAINER_NAME" || true docker rm "$CONTAINER_NAME" || true + fi - docker run -d --name "$CONTAINER_NAME" -p $PORT:3000 "$IMAGE_NAME" + docker run -d --name "$CONTAINER_NAME" -p $PORT:3000 "$IMAGE_NAME" + if [ "$(docker inspect --format='{{.State.Running}}' "$CONTAINER_NAME")" = "true" ]; then echo "Deployment erfolgreich!" else echo "Neuer Container konnte nicht gestartet werden!" From c120e50171c657d0787b31a7b31ee98bbe7c1793 Mon Sep 17 00:00:00 2001 From: denshooter <44590296+denshooter@users.noreply.github.com> Date: Fri, 21 Feb 2025 16:52:04 +0100 Subject: [PATCH 3/6] Dev (#43) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚀 refactor: simplify deployment process in workflow file * 🚀 chore: add IMAGE_NAME to GITHUB_ENV for deployment workflow * ✨ chore: simplify deployment logging in workflow file * 🚀 fix: correct container name in deployment script logic * 🚀 refactor: rename job and streamline deployment steps --- .github/workflows/deploy.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ef428fb..5d5d227 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -34,14 +34,11 @@ jobs: - name: Log in to GHCR run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin - - name: Pull Docker Image + - name: Pull & Deploy Docker Image run: | IMAGE_NAME="ghcr.io/${{ github.repository_owner }}/my-nextjs-app:${{ github.event.workflow_run.head_branch }}" IMAGE_NAME=$(echo "$IMAGE_NAME" | tr '[:upper:]' '[:lower:]') docker pull "$IMAGE_NAME" - - - name: Zero-Downtime Deployment - run: | CONTAINER_NAME="nextjs-$DEPLOY_ENV" echo "Deploying $CONTAINER_NAME" From a31754d178f64563e389382d6e8d15921a81e8fb Mon Sep 17 00:00:00 2001 From: denshooter <44590296+denshooter@users.noreply.github.com> Date: Fri, 21 Feb 2025 17:07:56 +0100 Subject: [PATCH 4/6] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 99336a5..9370f93 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Dennis Konkol's Portfolio +# Dennis Konkol's Portfolio Website This is a [Next.js](https://nextjs.org) project bootstrapped with [ `create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). From 9088a7cd324933d24ee0eb158df0842eb7e647db Mon Sep 17 00:00:00 2001 From: denshooter <44590296+denshooter@users.noreply.github.com> Date: Sat, 22 Feb 2025 22:29:23 +0100 Subject: [PATCH 5/6] Dev (#44) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚀 refactor: simplify deployment process in workflow file * 🚀 chore: add IMAGE_NAME to GITHUB_ENV for deployment workflow * ✨ chore: simplify deployment logging in workflow file * 🚀 fix: correct container name in deployment script logic * 🚀 refactor: rename job and streamline deployment steps * Update README.md * ✨ fix: prevent multiple form submissions in Contact component * ✨ feat: honeypot and timestamp checks to form submission * ✨ refactor: simplify contact form and improve UI elements --- Dockerfile | 3 - app/__tests__/components/Contact.test.tsx | 32 ++-- app/components/Contact.tsx | 178 +++++++++++++++++----- 3 files changed, 163 insertions(+), 50 deletions(-) diff --git a/Dockerfile b/Dockerfile index 24ba3e3..8ffb440 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,9 +19,6 @@ COPY .env .env # Build the Next.js application RUN npm run build -# Set environmental variable for production mode -ENV NODE_ENV=production - # Expose the port the app runs on EXPOSE 3000 diff --git a/app/__tests__/components/Contact.test.tsx b/app/__tests__/components/Contact.test.tsx index a853957..5c13912 100644 --- a/app/__tests__/components/Contact.test.tsx +++ b/app/__tests__/components/Contact.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import Contact from '@/app/components/Contact'; import '@testing-library/jest-dom'; @@ -10,20 +10,34 @@ global.fetch = jest.fn(() => ) as jest.Mock; describe('Contact', () => { + beforeAll(() => { + jest.useFakeTimers('modern'); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + it('renders the contact form', () => { render(); - expect(screen.getByPlaceholderText('Name')).toBeInTheDocument(); - expect(screen.getByPlaceholderText('Email')).toBeInTheDocument(); - expect(screen.getByPlaceholderText('Message')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Your Name')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('you@example.com')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Your Message...')).toBeInTheDocument(); + expect(screen.getByLabelText('I accept the privacy policy.')).toBeInTheDocument(); }); it('submits the form', async () => { render(); - fireEvent.change(screen.getByPlaceholderText('Name'), { target: { value: 'John Doe' } }); - fireEvent.change(screen.getByPlaceholderText('Email'), { target: { value: 'john@example.com' } }); - fireEvent.change(screen.getByPlaceholderText('Message'), { target: { value: 'Hello!' } }); - fireEvent.click(screen.getByText('Send')); - expect(await screen.findByText('Email sent')).toBeInTheDocument(); + // Fast forward time to ensure the timestamp check passes + jest.advanceTimersByTime(3000); + + fireEvent.change(screen.getByPlaceholderText('Your Name'), { target: { value: 'John Doe' } }); + fireEvent.change(screen.getByPlaceholderText('you@example.com'), { target: { value: 'john@example.com' } }); + fireEvent.change(screen.getByPlaceholderText('Your Message...'), { target: { value: 'Hello!' } }); + fireEvent.click(screen.getByLabelText('I accept the privacy policy.')); + fireEvent.click(screen.getByText('Send Message')); + + await waitFor(() => expect(screen.getByText('Email sent')).toBeInTheDocument()); }); }); \ No newline at end of file diff --git a/app/components/Contact.tsx b/app/components/Contact.tsx index 3898dc8..b7a7109 100644 --- a/app/components/Contact.tsx +++ b/app/components/Contact.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from "react"; import { sendEmail } from "@/app/utils/send-email"; +import Link from "next/link"; export type ContactFormData = { name: string; @@ -18,11 +19,14 @@ export default function Contact() { message: "", type: "success", }); + // Record the time when the form is rendered + const [formLoadedTimestamp, setFormLoadedTimestamp] = useState(Date.now()); useEffect(() => { + setFormLoadedTimestamp(Date.now()); setTimeout(() => { setIsVisible(true); - }, 350); // Delay to start the animation after Projects + }, 350); }, []); async function onSubmit(e: React.FormEvent) { @@ -31,72 +35,170 @@ export default function Contact() { const form = e.currentTarget as HTMLFormElement; const formData = new FormData(form); + // Honeypot check + const honeypot = formData.get("hp-field"); + if (honeypot) { + setBanner({ + show: true, + message: "Bot detected", + type: "error", + }); + setTimeout(() => setBanner((prev) => ({ ...prev, show: false })), 3000); + return; + } + + // Time-based anti-bot check + const timestampStr = formData.get("timestamp") as string; + const timestamp = parseInt(timestampStr, 10); + if (Date.now() - timestamp < 3000) { + setBanner({ + show: true, + message: "Please take your time filling out the form.", + type: "error", + }); + setTimeout(() => setBanner((prev) => ({ ...prev, show: false })), 3000); + return; + } + const data: ContactFormData = { name: formData.get("name") as string, email: formData.get("email") as string, message: formData.get("message") as string, }; - // Convert FormData to a plain object const jsonData = JSON.stringify(data); - const response = await sendEmail(jsonData); - if (response.success) { - form.reset(); - } + const submitButton = form.querySelector("button[type='submit']"); + if (submitButton) { + submitButton.setAttribute("disabled", "true"); + submitButton.textContent = "Sending..."; - setBanner({ - show: true, - message: response.message, - type: response.success ? "success" : "error", - }); - setTimeout(() => { - setBanner((prev) => ({ ...prev, show: false })); - }, 3000); + const response = await sendEmail(jsonData); + if (response.success) { + form.reset(); + submitButton.textContent = "Sent!"; + setTimeout(() => { + submitButton.removeAttribute("disabled"); + submitButton.textContent = "Send Message"; + }, 2000); + } + setBanner({ + show: true, + message: response.message, + type: response.success ? "success" : "error", + }); + setTimeout(() => setBanner((prev) => ({ ...prev, show: false })), 3000); + } } + return (
-

- Contact Me +

+ Get in Touch

-
+
{banner.show && (
{banner.message}
)} -
+ + {/* Honeypot field */} + {/* Hidden timestamp field */} - + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
From 7cd50b4dca1cf573d7142d9cae1e0c3936cbd7b7 Mon Sep 17 00:00:00 2001 From: denshooter <44590296+denshooter@users.noreply.github.com> Date: Sat, 22 Feb 2025 23:33:47 +0100 Subject: [PATCH 6/6] Dev (#45) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚀 refactor: simplify deployment process in workflow file * 🚀 chore: add IMAGE_NAME to GITHUB_ENV for deployment workflow * ✨ chore: simplify deployment logging in workflow file * 🚀 fix: correct container name in deployment script logic * 🚀 refactor: rename job and streamline deployment steps * Update README.md * ✨ fix: prevent multiple form submissions in Contact component * ✨ feat: honeypot and timestamp checks to form submission * ✨ refactor: simplify contact form and improve UI elements * ✨ feat: add responsive masonry layout for projects display * ✨ style: Update project title size and improve layout visibility --- app/components/Contact.tsx | 4 +-- app/components/Projects.tsx | 61 +++++++++++++++++++------------------ app/globals.css | 19 +++++++++++- package-lock.json | 9 +++++- package.json | 3 +- 5 files changed, 62 insertions(+), 34 deletions(-) diff --git a/app/components/Contact.tsx b/app/components/Contact.tsx index b7a7109..aeb5e9c 100644 --- a/app/components/Contact.tsx +++ b/app/components/Contact.tsx @@ -96,10 +96,10 @@ export default function Contact() { id="contact" className={`p-10 ${isVisible ? "animate-fade-in" : "opacity-0"}`} > -

+

Get in Touch

-
+
{banner.show && (
-

Projects

-
- {projects.map((project, index) => ( - Projects +
+ {isVisible && ( + -
-

- {project.title} -

-

{project.meta_description}

-
- - ))} -
-

More to come

-

...

-
+ + {projects.map((project, index) => ( + +
+

+ {project.title} +

+

+ {project.meta_description} +

+
+ + ))} +
+
+ )}
); diff --git a/app/globals.css b/app/globals.css index c2b3259..c59bd8e 100644 --- a/app/globals.css +++ b/app/globals.css @@ -196,4 +196,21 @@ body { .animate-fade-out { animation: fadeOut 3s forwards; -} \ No newline at end of file +} + +.project-card { + display: flex; + flex-direction: column; + justify-content: space-between; + background: rgba(255, 255, 255, 0.45); + border-radius: 16px; + padding: 16px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease; + width: 100%; + height: auto; +} + +.project-card:hover { + transform: translateY(-5px); +} diff --git a/package-lock.json b/package-lock.json index 59346b2..79c241a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "node-fetch": "^3.3.2", "nodemailer": "^6.10.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-responsive-masonry": "^2.7.1" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -9249,6 +9250,12 @@ "dev": true, "license": "MIT" }, + "node_modules/react-responsive-masonry": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-responsive-masonry/-/react-responsive-masonry-2.7.1.tgz", + "integrity": "sha512-Q+u+nOH87PzjqGFd2PgTcmLpHPZnCmUPREHYoNBc8dwJv6fi51p9U6hqwG8g/T8MN86HrFjrU+uQU6yvETU7cA==", + "license": "MIT" + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/package.json b/package.json index e6b8cab..7ec56f7 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "node-fetch": "^3.3.2", "nodemailer": "^6.10.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-responsive-masonry": "^2.7.1" }, "devDependencies": { "@eslint/eslintrc": "^3",