From d8c20826b01d117afbadb04ab78313135300549c Mon Sep 17 00:00:00 2001 From: oonyeje Date: Wed, 5 Mar 2025 13:23:34 -0500 Subject: [PATCH] Merge branch 'Setup-Basic-Website' --- .dockerignore | 3 + .drone.yml | 111 +++ .env.dev | 10 + .env.drone | 12 + .../workflows/manual_deploy_to_coolify.yaml | 38 + .gitignore | 71 +- Dockerfile | 18 + ansible/inventory.yml | 2 + ansible/playbook.staging.yml | 60 ++ ansible/requirements.yml | 5 + components/content/carousel/index.tsx | 49 + components/content/index.tsx | 24 +- components/footer.tsx | 8 + components/form/ContactForm/index.tsx | 114 +++ components/layout.tsx | 24 +- components/navbar.tsx | 45 + docker-compose.dev.yaml | 27 + docker-compose.drone.yaml | 25 + docker-compose.yaml | 17 + lib/content.ts | 34 + lib/navigationContent.tsx | 59 +- lib/portfolio.ts | 31 + next.config.js | 13 + package.json | 20 +- pages/_document.tsx | 10 +- pages/api/contact.ts | 101 ++ pages/index.tsx | 206 ++-- postcss.config.js | 1 + public/aqua_intelligence_image.png | Bin 0 -> 3299 bytes ...Okechi_Onyeje_Resume_Professional_2023.pdf | Bin 0 -> 69630 bytes public/gethip-image.png | Bin 0 -> 9658 bytes public/gitea-logo.svg | 10 + public/vendoo-image.png | Bin 0 -> 18147 bytes styles/globals.css | 26 + tailwind.config.ts | 10 + yarn.lock | 929 +++++++++++++++++- 36 files changed, 1946 insertions(+), 167 deletions(-) create mode 100644 .dockerignore create mode 100755 .drone.yml create mode 100644 .env.dev create mode 100644 .env.drone create mode 100644 .gitea/workflows/manual_deploy_to_coolify.yaml mode change 100644 => 100755 .gitignore create mode 100644 Dockerfile create mode 100755 ansible/inventory.yml create mode 100755 ansible/playbook.staging.yml create mode 100755 ansible/requirements.yml create mode 100755 components/content/carousel/index.tsx mode change 100644 => 100755 components/content/index.tsx create mode 100644 components/footer.tsx create mode 100644 components/form/ContactForm/index.tsx create mode 100644 components/navbar.tsx create mode 100755 docker-compose.dev.yaml create mode 100755 docker-compose.drone.yaml create mode 100755 docker-compose.yaml create mode 100755 lib/content.ts mode change 100644 => 100755 lib/navigationContent.tsx create mode 100755 lib/portfolio.ts create mode 100644 pages/api/contact.ts mode change 100644 => 100755 pages/index.tsx create mode 100644 public/aqua_intelligence_image.png create mode 100755 public/assets/Okechi_Onyeje_Resume_Professional_2023.pdf create mode 100644 public/gethip-image.png create mode 100644 public/gitea-logo.svg create mode 100644 public/vendoo-image.png diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2eac0dc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +.git +.env \ No newline at end of file diff --git a/.drone.yml b/.drone.yml new file mode 100755 index 0000000..273b0ce --- /dev/null +++ b/.drone.yml @@ -0,0 +1,111 @@ +kind: pipeline +type: docker +name: build + +steps: +################################################################################################################################################ +## example of running dind manually in drone +# - name: build +# image: docker +# environment: +# REGISTRY_USER: +# from_secret: DOCKER_USERNAME +# REGISTRY_PASS: +# from_secret: DOCKER_PASSWORD +# volumes: +# - name: docker_sock +# path: /var/run/docker.sock +# commands: +# - export DRONE_SHA=${DRONE_COMMIT_SHA:0:7} +# - docker login gitea.bsidesolutions.net --username $REGISTRY_USER --password $REGISTRY_PASS +# # - docker exec --env-file .env.drone compose-container mkdir /home/node +# # - docker exec --env-file .env.drone compose-container mkdir /home/node/app +# # - docker cp . compose-container:/home/node/app +# # - docker exec -w /home/node/app --env-file .env.drone compose-container ls # check current directory +# - docker build -t gitea.bsidesolutions.net/oonyeje/oonyeje-portfolio:${DRONE_COMMIT_SHA:0:7} . +# - docker tag gitea.bsidesolutions.net/oonyeje/oonyeje-portfolio gitea.bsidesolutions.net/oonyeje/oonyeje-portfolio:${DRONE_COMMIT_SHA:0:7} +# - docker tag gitea.bsidesolutions.net/oonyeje/oonyeje-portfolio gitea.bsidesolutions.net/oonyeje/oonyeje-portfolio:latest +################################################################################################################################################ + +- name: Setup Build + image: gitea.bsidesolutions.net/bside-solutions/docker-compose-v2-alpine:latest + commands: + # - docker container stop compose-container + - docker container prune -f + - docker image prune -f + # - docker login gitea.bsidesolutions.net --username $REGISTRY_USER --password $REGISTRY_PASS + # - docker pull gitea.bsidesolutions.net/bside-solutions/docker-compose-v2-alpine:latest + # - docker run -v /var/run/docker.sock:/var/run/docker.sock --rm -d --name compose-container gitea.bsidesolutions.net/bside-solutions/docker-compose-v2-alpine sleep inf #start container in detached mode + # - sleep 5 # give container some time to fully start + volumes: + - name: docker_sock + path: /var/run/docker.sock +- name: Build and Push Container Image + image: plugins/docker + settings: + username: + from_secret: DOCKER_USERNAME + password: + from_secret: DOCKER_PASSWORD + repo: gitea.bsidesolutions.net/oonyeje/oonyeje-portfolio + registry: gitea.bsidesolutions.net + dockerfile: ./Dockerfile + force_tag: true + tags: + - latest + - dev + - dev-build-${DRONE_COMMIT_SHA:0:7} + + +# - name: deploy-staging +# image: plugins/ansible:latest +# settings: +# playbook: ansible/playbook.staging.yml +# galaxy: ansible/requirements.yml +# inventory: ansible/inventory.yml +# become_user: bside +# user: bside +# verbose: 4 +# become: true +# list_tasks: true +# list_hosts: true +# private_key: +# from_secret: STAGING_SERVER_PRIVATE_KEY +# trigger: +# branch: +# - master +# - prod +# - qa +# - feature/* +# event: +# - push +# - pull_request +# --- +# kind: pipeline +# type: exec +# name: staging-deploy + +# platform: +# os: linux +# arch: amd64 + +# steps: +# - name: ansible-deploy +# commands: +# - ansible --version +# - ansible-galaxy install --force --role-file ansible/requirements.yml -vvvv +# - ansible-playbook --inventory ansible/inventory.yml --list-hosts ansible/playbook.staging.yml +# trigger: +# branch: +# - master +# - prod +# - qa +# - feature/* +# event: +# - push +# - pull_request + +volumes: + - name: docker_sock + host: + path: /var/run/docker.sock \ No newline at end of file diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..2614730 --- /dev/null +++ b/.env.dev @@ -0,0 +1,10 @@ +SMTP_PROXY_EMAIL=$SMTP_PROXY_EMAIL +SMTP_RECIPIENT_EMAIL=$SMTP_RECIPIENT_EMAIL +SMTP_HOST=$SMTP_HOST +SMTP_PORT=$SMTP_PORT +SMTP_USERNAME=$SMTP_USERNAME +SMTP_PASSWORD=$SMTP_PASSWORD +NEXT_PUBLIC_GOOGLE_APPOINTMENTS_LINK=$NEXT_PUBLIC_GOOGLE_APPOINTMENTS_LINK +RECAPTCHA_SECRET_KEY=$RECAPTCHA_SECRET_KEY +NEXT_PUBLIC_RECAPTCHA_SITE_KEY=$NEXT_PUBLIC_RECAPTCHA_SITE_KEY +GIT_SHA=$GIT_SHA diff --git a/.env.drone b/.env.drone new file mode 100644 index 0000000..edba6f5 --- /dev/null +++ b/.env.drone @@ -0,0 +1,12 @@ +SMTP_PROXY_EMAIL=$SMTP_PROXY_EMAIL +SMTP_RECIPIENT_EMAIL=$SMTP_RECIPIENT_EMAIL +SMTP_HOST=$SMTP_HOST +SMTP_PORT=$SMTP_PORT +SMTP_USERNAME=$SMTP_USERNAME +SMTP_PASSWORD=$SMTP_PASSWORD +NEXT_PUBLIC_GOOGLE_APPOINTMENTS_LINK=$NEXT_PUBLIC_GOOGLE_APPOINTMENTS_LINK +RECAPTCHA_SECRET_KEY=$RECAPTCHA_SECRET_KEY +NEXT_PUBLIC_RECAPTCHA_SITE_KEY=$NEXT_PUBLIC_RECAPTCHA_SITE_KEY +DRONE_SHA=$DRONE_SHA +DOCKER_USERNAME=$DOCKER_USERNAME +DOCKER_PASSWORD=$DOCKER_PASSWORD diff --git a/.gitea/workflows/manual_deploy_to_coolify.yaml b/.gitea/workflows/manual_deploy_to_coolify.yaml new file mode 100644 index 0000000..0bc78d6 --- /dev/null +++ b/.gitea/workflows/manual_deploy_to_coolify.yaml @@ -0,0 +1,38 @@ +name: Manually Deploy Image to Coolify + +on: + workflow_dispatch: + inputs: + image_tag: + description: 'Built Container Image to deploy' + required: false + default: 'latest' + type: string + environment: + description: 'Coolify Environment to deploy to' + required: false + type: choice + default: 'staging' + options: + - staging + - production + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + # - name: Login to Gitea + # run: | + # echo "${{ secrets.TOKEN }}" | docker login ${{ gitea.server_url }} --username ${{ gitea.actor }} --password-stdin + + - name: Deploy to Coolify + uses: carlozanella/deploy-coolify@v1 + with: + endpoint: ${{ secrets.COOLIFY_ENDPOINT }}/api/v1 + token: ${{ secrets.COOLIFY_TOKEN }} + app_uuid: ${{ (contains(github.event.inputs.environment, 'staging') && secrets.COOLIFY_APP_STAGING_UUID) || secrets.COOLIFY_APP_PRODUCTION_UUID }} + image_name: 'gitea.bsidesolutions.net/oonyeje/oonyeje-portfolio' + image_tag: ${{ github.event.inputs.image_tag }} \ No newline at end of file diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 8f322f0..12ee344 --- a/.gitignore +++ b/.gitignore @@ -1,35 +1,36 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a823c02 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM node:20-alpine AS builder + +WORKDIR /app +COPY package*.json ./ +RUN npm install -g yarn --force && yarn install --frozen-lockfile --production=false --cache-folder /yarn-cache && yarn cache clean --force && rm -rf /yarn-cache /tmp/* +COPY . . +# RUN yarn build # Uncomment if you have a build step + +FROM node:20-alpine + +WORKDIR /app +COPY --from=builder /app/package*.json ./ +COPY --from=builder /app/yarn.lock ./ +RUN npm install -g yarn --force && yarn install --frozen-lockfile --production --cache-folder /yarn-cache && yarn cache clean --force && rm -rf /yarn-cache /tmp/* +COPY --from=builder /app/ . + +EXPOSE 3000 +CMD ["yarn", "dev", "-p", "3000"] #or node dist/index.js if you built the project. \ No newline at end of file diff --git a/ansible/inventory.yml b/ansible/inventory.yml new file mode 100755 index 0000000..d94651b --- /dev/null +++ b/ansible/inventory.yml @@ -0,0 +1,2 @@ +deployment_servers: + hosts: localhost diff --git a/ansible/playbook.staging.yml b/ansible/playbook.staging.yml new file mode 100755 index 0000000..c556172 --- /dev/null +++ b/ansible/playbook.staging.yml @@ -0,0 +1,60 @@ +- name: deploy docker stack + hosts: localhost + tasks: + - name: Tear down existing services + community.docker.docker_compose: + project_src: ../ + state: absent + + - name: Create and start services + community.docker.docker_compose: + project_src: ../ + register: output + + - name: Show results + ansible.builtin.debug: + var: output + + - name: Run `docker-compose up` again + community.docker.docker_compose: + project_src: ../ + build: false + register: output + + # - name: Show results + # ansible.builtin.debug: + # var: output + + # - ansible.builtin.assert: + # that: not output.changed + + # - name: Stop all services + # community.docker.docker_compose: + # project_src: ../ + # build: false + # stopped: true + # register: output + + - name: Show results + ansible.builtin.debug: + var: output + + - name: Verify that app and db services are running + ansible.builtin.assert: + that: + - "not output.services.app.oonyeje-portfolio_app_1.state.running" + # - name: Restart services + # community.docker.docker_compose: + # project_src: ../ + # build: false + # restarted: true + # register: output + + - name: Show results + ansible.builtin.debug: + var: output + +# - name: Verify that app and db services are running +# ansible.builtin.assert: +# that: +# - "not output.services.app.oonyeje-portfolio_app_1.state.running" diff --git a/ansible/requirements.yml b/ansible/requirements.yml new file mode 100755 index 0000000..fd50a41 --- /dev/null +++ b/ansible/requirements.yml @@ -0,0 +1,5 @@ +collections: +# Install a collection from Ansible Galaxy. +- name: community.docker + version: ">=3.4.8" + source: https://galaxy.ansible.com diff --git a/components/content/carousel/index.tsx b/components/content/carousel/index.tsx new file mode 100755 index 0000000..4850082 --- /dev/null +++ b/components/content/carousel/index.tsx @@ -0,0 +1,49 @@ +import React, { useState } from "react"; +import Image from "next/image"; +import { StaticImport } from "next/dist/shared/lib/get-img-props"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faChevronCircleLeft, faChevronCircleRight } from "@fortawesome/free-solid-svg-icons"; +import IframeResizer from "iframe-resizer-react"; +import Link from "next/link"; + +interface ContentCarouselProps { + data: Array<{ + title: String, + heroImgSrc: string | StaticImport, + description?: string, + prototypeIframeURL?: string + }> +} + +const ContentCarousel = ({ + data = [] +}: ContentCarouselProps) => { + const [pageLength, setPageLength] = useState(data.length) + const [currentPageIdx, setCurrentPageIdx] = useState(0); + + const portfolioData = data[currentPageIdx]; + return ( +
+
+
+ {(currentPageIdx > 0) && setCurrentPageIdx(currentPageIdx - 1)}>} + {(currentPageIdx < pageLength - 1) && setCurrentPageIdx(currentPageIdx + 1)}>} +
+
{portfolioData.title}
+
{portfolioData.description}
+ {portfolioData.prototypeIframeURL &&
+ {portfolioData.heroImgSrc && +
+ +
+
+
+ } +
} +
+
+ + ); +}; + +export default ContentCarousel; \ No newline at end of file diff --git a/components/content/index.tsx b/components/content/index.tsx old mode 100644 new mode 100755 index a761964..b2ff522 --- a/components/content/index.tsx +++ b/components/content/index.tsx @@ -1,16 +1,26 @@ -import React from "react"; +import React, { ReactNode } from "react"; import Image from "next/image"; +import { StaticImport } from "next/dist/shared/lib/get-img-props"; + +interface ContentProps { + title: string, + heroImgSrc: string | StaticImport | null, + description?: string, + innerChildren?: ReactNode +} const Content = ({ title = '', - heroImgSrc, - description = '' -}) => { + heroImgSrc = '', + description = '', + innerChildren = <> +}: ContentProps) => { return ( -
-
{title}
- {heroImgSrc &&
} +
+
{title}
+ {heroImgSrc &&
}
{description}
+
{innerChildren}
); }; diff --git a/components/footer.tsx b/components/footer.tsx new file mode 100644 index 0000000..8e7d1cd --- /dev/null +++ b/components/footer.tsx @@ -0,0 +1,8 @@ +import React from "react"; +export default function Footer({}) { + return ( +
+ +
+ ); +}; \ No newline at end of file diff --git a/components/form/ContactForm/index.tsx b/components/form/ContactForm/index.tsx new file mode 100644 index 0000000..d5b0350 --- /dev/null +++ b/components/form/ContactForm/index.tsx @@ -0,0 +1,114 @@ +import { RefObject, useRef, useState } from 'react'; +import { useForm, SubmitHandler, FieldValues } from 'react-hook-form'; +import ReCAPTCHA from 'react-google-recaptcha'; + +export interface ContactFormData extends FieldValues { + firstName: string, + lastName: string, + email: string, + subject: string, + summary: string, + honeypot_xyz: string +}; + +const ContactForm = () => { + const { + register, + resetField, + handleSubmit, + setValue, + formState: { errors }, + } = useForm(); + + const [submitted, setSubmitted] = useState(false); + + const handleFormSubmission: SubmitHandler = async (data: FieldValues) => { + const submissionData = data as ContactFormData; + console.log(submissionData) + + if (submissionData.honeypot_xyz !== "") { + // Form submission is spam + return; + } + const res = await fetch('/api/contact', { + method: 'POST', + headers: { + 'Accept': 'application/json, text/plain, */*', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(submissionData) + }); + + console.log('Response received') + if (res.status === 200) { + console.log('Response succeeded!') + setSubmitted(true) + resetField('captchaToken') + } + }; + + const handleFormError: SubmitHandler = (error) => { + console.log(error) + }; + + const onCaptchaChange = (token: string | null) => { + // Set the captcha token when the user completes the reCAPTCHA + if (token) { + setValue('captchaToken', token); + } + }; + return ( +
+
+
+
+
+
+ + {errors.firstName &&

First name is required.

} +
+
+ + {errors.lastName &&

Last name is required.

} +
+
+
+
+ + {errors.email &&

Email is required.

} +
+
+ + {errors.subject &&

Subject is required.

} +
+
+ +
+ +
+