Compare commits

...

1 Commits

Author SHA1 Message Date
d8c20826b0 Merge branch 'Setup-Basic-Website'
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-05 13:23:34 -05:00
36 changed files with 1946 additions and 167 deletions

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
.git
.env

111
.drone.yml Executable file
View File

@ -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

10
.env.dev Normal file
View File

@ -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

12
.env.drone Normal file
View File

@ -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

View File

@ -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 }}

1
.gitignore vendored Normal file → Executable file
View File

@ -26,6 +26,7 @@ yarn-error.log*
# local env files # local env files
.env*.local .env*.local
.env
# vercel # vercel
.vercel .vercel

18
Dockerfile Normal file
View File

@ -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.

2
ansible/inventory.yml Executable file
View File

@ -0,0 +1,2 @@
deployment_servers:
hosts: localhost

60
ansible/playbook.staging.yml Executable file
View File

@ -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"

5
ansible/requirements.yml Executable file
View File

@ -0,0 +1,5 @@
collections:
# Install a collection from Ansible Galaxy.
- name: community.docker
version: ">=3.4.8"
source: https://galaxy.ansible.com

View File

@ -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 (
<div className="flex flex-row justify-between">
<div style={{width: '-webkit-fill-available'}} className={`p-4 ${portfolioData.prototypeIframeURL ? '' : 'px-10'} flex flex-col`}>
<div className="flex flex-row self-auto justify-between">
{(currentPageIdx > 0) && <span className="cursor-pointer" onClick={() => setCurrentPageIdx(currentPageIdx - 1)}><FontAwesomeIcon width={50} height={50} icon={faChevronCircleLeft} className="fas fa-chevron-circle-left" style={{ color: "white" }} /></span>}
{(currentPageIdx < pageLength - 1) && <span className="cursor-pointer" onClick={() => setCurrentPageIdx(currentPageIdx + 1)}><FontAwesomeIcon width={25} height={50} icon={faChevronCircleRight} className="fas fa-chevron-circle-right" style={{ color: "white" }} /></span>}
</div>
<div className=" w-fit self-center pb-1 border-b-2 border-white">{portfolioData.title}</div>
<div>{portfolioData.description}</div>
{portfolioData.prototypeIframeURL && <div style={{minHeight: '100%'}} className="my-4 h-fit flex flex-row justify-center">
{portfolioData.heroImgSrc && <Link className="rounded-md w-fit h-full align-middle flex flex-row justify-center" href={portfolioData.prototypeIframeURL} target="#">
<div className="flex flex-row justify-center">
<span>
<div className="mb-8 w-fit flex-row flex justify-center bg-white"><Image height={200} width={200} src={portfolioData.heroImgSrc} alt=""/></div>
</span>
</div>
</Link>}
</div>}
</div>
</div>
);
};
export default ContentCarousel;

24
components/content/index.tsx Normal file → Executable file
View File

@ -1,16 +1,26 @@
import React from "react"; import React, { ReactNode } from "react";
import Image from "next/image"; 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 = ({ const Content = ({
title = '', title = '',
heroImgSrc, heroImgSrc = '',
description = '' description = '',
}) => { innerChildren = <></>
}: ContentProps) => {
return ( return (
<div className="p-4 flex flex-col"> <div className="h-full p-4 px-10 flex flex-col">
<div className="pb-4 border-b-2 border-white">{title}</div> <div className=" w-fit pb-1 mb-8 border-b-2 border-white">{title}</div>
{heroImgSrc && <div><Image src={heroImgSrc} alt=""/></div>} {heroImgSrc && <div className="mb-8 h-full flex flex-row justify-center"><Image height={200} src={heroImgSrc} alt=""/></div>}
<div>{description}</div> <div>{description}</div>
<div>{innerChildren}</div>
</div> </div>
); );
}; };

8
components/footer.tsx Normal file
View File

@ -0,0 +1,8 @@
import React from "react";
export default function Footer({}) {
return (
<div className="w-full bg-black h-12">
</div>
);
};

View File

@ -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<FieldValues> = 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<FieldValues> = (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 (
<div className='w-full flex flex-row justify-center'>
<form className=" w-1/2" onSubmit={handleSubmit(handleFormSubmission, handleFormError)}>
<div className="flex flex-col justify-center h-full">
<div className=' flex flex-col justify-between space-y-4 w-full'>
<div className='flex flex-row justify-between space-x-2'>
<div className='w-1/2'>
<input placeholder='First Name' className='w-full' {...register('firstName', { required: true })} />
{errors.firstName && <p>First name is required.</p>}
</div>
<div className='w-1/2'>
<input placeholder='Last Name' className='w-full' {...register('lastName', { required: true })} />
{errors.lastName && <p>Last name is required.</p>}
</div>
</div>
<div className='flex flex-col space-y-4 w-full'>
<div>
<input placeholder='Email' type="email" className='w-full' {...register('email', { required: true })} />
{errors.email && <p>Email is required.</p>}
</div>
<div>
<select placeholder='Subject...' className='w-full' {...register('subject', { required: true })}>
<option>Subject...</option>
<option>Inquiry</option>
<option>Consultations</option>
<option>Want To Hire For Potential Job/Contract</option>
</select>
{errors.subject && <p>Subject is required.</p>}
</div>
<div className="flex flex-col w-full">
<input type='hidden' value="" {...register('honeypot_xyz', { required: true })}/>
</div>
<div className="flex flex-col w-full">
<textarea placeholder='Summary...' className="flex grow h-52" {...register('summary')} />
{errors.summary && <p>Please enter a message for your Project Inquiry.</p>}
</div>
<div className="pb-20px">
<ReCAPTCHA
size="normal"
sitekey={process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY ? process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY : 'ENTER_API_KEY'}
{...register('captchaToken', { required: true })}
onChange={onCaptchaChange}
/>
</div>
</div>
<input className="bg-blue-600 text-white rounded-sm cursor-pointer" type="submit" />
</div>
</div>
</form>
</div>
);
};
export default ContactForm;

View File

@ -1,13 +1,19 @@
// import Navbar from './navbar' import Navbar from './navbar'
// import Footer from './footer' import Footer from './footer'
import React from 'react' import React from 'react'
export default function Layout({ children }) { export interface LayoutProps {
children: React.JSX.Element
};
export default function Layout({children}: LayoutProps) {
return ( return (
<> <div style={{height: '100vh', width: '100vw'}}>
{/* <Navbar /> */} <Navbar />
<main>{children}</main> <main style={{height: '86%'}}>
{/* <Footer /> */} {children}
</> </main>
<Footer />
</div>
) )
} }

45
components/navbar.tsx Normal file
View File

@ -0,0 +1,45 @@
import Link from "next/link";
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faLinkedin } from "@fortawesome/free-brands-svg-icons";
import { faGithub } from "@fortawesome/free-brands-svg-icons";
import giteaLogo from "@/public/gitea-logo.svg";
import Image from "next/image";
export default function Navbar({}) {
return (
<div className="text-white w-full middle align-middle bg-black h-11">
<div className="h-full flex flex-col justify-center">
<div className="flex flex-row justify-between ml-2">
<div>
Okechi Onyeje
</div>
<div className="flex flex-row gap-4 mr-2">
<Link href="https://www.linkedin.com/in/okechi-onyeje-57129b9a/" target="#">
<FontAwesomeIcon width={20} height={70} icon={faLinkedin} className="fas fa-linkedin" style={{ color: "white" }} />
</Link>
<span className="flex flex-row">
<Link href="https://github.com/oonyeje" target="#">
<i className="fa-solid">
<FontAwesomeIcon width={30} height={70} icon={faGithub} className="fas fa-github" style={{ color: "white" }} />
</i>
</Link>/
<Link className="ml-2 mt-1 align-middle" href="https://gitea.bsidesolutions.net/oonyeje" target="#">
<i className="fa-solid">
<Image
height={16}
width={16}
priority
src={giteaLogo}
alt="Gitea"
/>
</i>
</Link>
</span>
{/* <Link href="https://bsidesol.com" target="#">BSide</Link> */}
</div>
</div>
</div>
</div>
);
};

27
docker-compose.dev.yaml Executable file
View File

@ -0,0 +1,27 @@
name: oonyeje-portfolio
services:
oonyeje-portfolio:
# this is not tagging corectly in drone, which is not pushing the correct build in the end
image: gitea.bsidesolutions.net/oonyeje/oonyeje-portfolio
build:
context: ./
dockerfile: Dockerfile
ports:
- "6000:3000"
volumes:
- .:/home/node/app
- ./node_modules:/home/node/app/node_modules
environment:
- 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}
working_dir: /home/node/app/
command: sh -c "yarn install && yarn dev"
env_file:
- .env.dev

25
docker-compose.drone.yaml Executable file
View File

@ -0,0 +1,25 @@
name: oonyeje-portfolio
services:
oonyeje-portfolio:
# this is not tagging corectly in drone, which is not pushing the correct build in the end
image: gitea.bsidesolutions.net/oonyeje/oonyeje-portfolio
build:
context: ./
dockerfile: Dockerfile
ports:
- "6000:3000"
volumes:
- .:/home/node/app
- ./node_modules:/home/node/app/node_modules
environment:
- 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}
working_dir: /home/node/app/
command: sh -c "yarn install && yarn dev"

17
docker-compose.yaml Executable file
View File

@ -0,0 +1,17 @@
version: '3.3'
services:
oonyeje-portfolio:
image: gitea.bsidesolutions.net/oonyeje/oonyeje-portfolio
build:
context: ./
dockerfile: Dockerfile
ports:
- "6000:3000"
volumes:
- .:/home/node/app
- ./node_modules:/home/node/app/node_modules
working_dir: /home/node/app/
command: sh -c "yarn install && yarn dev"
env_file:
- .env.local

34
lib/content.ts Executable file
View File

@ -0,0 +1,34 @@
const contentValues = {
intro: {
name: 'INTRO',
description: `A seasoned software developer and architect, building bespoke software solutions\n
for the modern small business. With almost a decade of experience in mobile and web development\n
in both the website hosting and streaming media industries, you can trust me to deliver your next\n
project venture or business need.`
},
work: {
name: 'WORK',
description: ``
},
about: {
name: 'ABOUT',
description: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Rhoncus urna neque viverra justo nec ultrices dui. Quis risus sed vulputate odio ut. Praesent semper feugiat nibh sed pulvinar proin gravida hendrerit. Mi ipsum faucibus vitae aliquet nec. Adipiscing elit ut aliquam purus sit amet luctus. Morbi tempus iaculis urna id. Vitae congue mauris rhoncus aenean vel elit scelerisque. Cursus turpis massa tincidunt dui. Vulputate sapien nec sagittis aliquam malesuada. Lectus proin nibh nisl condimentum id venenatis a condimentum. Est lorem ipsum dolor sit amet consectetur. Blandit aliquam etiam erat velit.
Vel turpis nunc eget lorem dolor. Volutpat commodo sed egestas egestas fringilla phasellus faucibus. Auctor eu augue ut lectus arcu bibendum. Suscipit tellus mauris a diam maecenas sed enim ut sem. Vivamus at augue eget arcu dictum varius. Vitae et leo duis ut diam quam nulla porttitor. Enim eu turpis egestas pretium. A diam sollicitudin tempor id eu nisl nunc mi. Venenatis cras sed felis eget. Sagittis eu volutpat odio facilisis mauris sit amet massa vitae. Tempor orci eu lobortis elementum nibh tellus molestie. Semper auctor neque vitae tempus quam pellentesque nec nam. Tempor commodo ullamcorper a lacus vestibulum sed arcu non odio. Tristique nulla aliquet enim tortor. Volutpat commodo sed egestas egestas fringilla phasellus. Enim ut sem viverra aliquet eget sit amet tellus cras. Cras semper auctor neque vitae tempus quam.
In nibh mauris cursus mattis molestie. Vivamus arcu felis bibendum ut tristique et egestas quis ipsum. Dolor purus non enim praesent elementum facilisis leo. Enim ut tellus elementum sagittis. Sed libero enim sed faucibus turpis in. Ac turpis egestas maecenas pharetra convallis posuere morbi leo. Nibh sit amet commodo nulla facilisi nullam vehicula ipsum. Arcu dictum varius duis at consectetur lorem donec massa. Tellus cras adipiscing enim eu turpis egestas pretium. Phasellus faucibus scelerisque eleifend donec pretium vulputate. Sollicitudin nibh sit amet commodo nulla facilisi nullam. Bibendum enim facilisis gravida neque convallis. Quis viverra nibh cras pulvinar mattis nunc. Quam nulla porttitor massa id neque aliquam vestibulum. Pharetra diam sit amet nisl suscipit adipiscing bibendum est ultricies. Faucibus turpis in eu mi bibendum neque. Curabitur gravida arcu ac tortor. Purus sit amet luctus venenatis lectus magna fringilla. Tincidunt vitae semper quis lectus nulla at volutpat. Vestibulum rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt lobortis.
Mi quis hendrerit dolor magna eget est lorem. Ut lectus arcu bibendum at. Elit at imperdiet dui accumsan sit amet nulla. Tristique et egestas quis ipsum suspendisse ultrices. A arcu cursus vitae congue. Dolor sed viverra ipsum nunc aliquet bibendum enim facilisis. Nec dui nunc mattis enim ut tellus elementum sagittis. Posuere ac ut consequat semper. Eu non diam phasellus vestibulum lorem sed risus. Arcu ac tortor dignissim convallis aenean et tortor at. Velit scelerisque in dictum non consectetur a erat. Condimentum mattis pellentesque id nibh. Quam quisque id diam vel quam. At lectus urna duis convallis convallis tellus id interdum velit. Aliquam vestibulum morbi blandit cursus risus at ultrices. Ullamcorper velit sed ullamcorper morbi tincidunt ornare massa. Mattis molestie a iaculis at erat pellentesque adipiscing. In nulla posuere sollicitudin aliquam ultrices sagittis. Vel elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique.
Amet consectetur adipiscing elit ut aliquam purus sit amet luctus. Et ultrices neque ornare aenean euismod elementum. Orci sagittis eu volutpat odio. Vitae aliquet nec ullamcorper sit amet risus nullam. A scelerisque purus semper eget duis at. Risus viverra adipiscing at in tellus integer feugiat scelerisque varius. Quis auctor elit sed vulputate. Enim ut sem viverra aliquet eget sit. Nulla aliquet porttitor lacus luctus. Mi sit amet mauris commodo quis imperdiet. Ac felis donec et odio pellentesque diam volutpat commodo sed. Lacus viverra vitae congue eu consequat ac felis donec et. Facilisis magna etiam tempor orci eu lobortis elementum nibh tellus. Velit egestas dui id ornare arcu.`
},
contact: {
name: 'CONTACT',
description: ``
}
}
export default contentValues;

55
lib/navigationContent.tsx Normal file → Executable file
View File

@ -1,42 +1,49 @@
import React from 'react'; import React from 'react';
import Content from '../components/content' import Content from '../components/content'
import backgroundPic from '../public/unspash_image.jpg' import backgroundPic from '../public/unspash_image.jpg'
import ContentCarousel from '../components/content/carousel';
import ContactForm from '../components/form/ContactForm';
import contentValues from './content';
import portfolioValues from './portfolio';
export default [ const resumePdf: string = '/assets/Okechi_Onyeje_Resume_Professional_2023.pdf'
const navigationContent = [
{ {
name: 'INTRO', name: contentValues.intro.name,
url: '#intro', url: '#intro',
content: <Content content: <Content
title='Intro' title='Intro'
heroImgSrc={backgroundPic} heroImgSrc={backgroundPic}
description={"Intro Section about okechi"} description={contentValues.intro.description}
/> />
}, },
{ {
name: 'WORK', name: contentValues.work.name,
url: '#work', url: '#work',
content: <Content content: <Content
title='Work' title='Work'
heroImgSrc={backgroundPic} heroImgSrc={null}
description={"Work Section about okechi"} description={contentValues.work.description}
innerChildren={
<div>
<ContentCarousel
data={portfolioValues}
/>
<embed style={{height: 2850}} className='w-full mt-10' src={`${resumePdf}#toolbar=0&navpanes=0&scrollbar=0`} type="application/pdf"/>
</div>
}
/> />
}, },
{ // {
name: 'ABOUT', // name: contentValues.about.name,
url: '#about', // url: '#about',
content: <Content // content: <Content
title='About' // title='About'
heroImgSrc={backgroundPic} // heroImgSrc={backgroundPic}
description={"About Section about okechi"} // description={contentValues.about.description}
/> // />
}, // },
{
name: 'CONTACT',
url: '#contact',
content: <Content
title='Contact'
heroImgSrc={backgroundPic}
description={"Contact Section about okechi"}
/>
}
]; ];
export default navigationContent;

31
lib/portfolio.ts Executable file
View File

@ -0,0 +1,31 @@
import gethip from '@/public/gethip-image.png';
import aqua from '@/public/aqua_intelligence_image.png';
import vendoo from '@/public/vendoo-image.png';
const portfolioValues = [
{
title: 'Vendoo',
heroImgSrc: vendoo,
description: '',
prototypeIframeURL: 'https://www.kroleo.com/invision/Vendoo/#/screens'
},
// {
// title: 'Garage2Garage',
// heroImgSrc: '',
// description: '',
// prototypeIframeURL: ''
// },
{
title: 'Aqua',
heroImgSrc: aqua,
description: '',
prototypeIframeURL: 'https://projects.invisionapp.com/share/QV9OA25MA#/screens'
},
{
title: 'GetHip',
heroImgSrc: gethip,
description: '',
prototypeIframeURL: 'https://www.kroleo.com/invision/GetHip/#/screens'
},
]
export default portfolioValues;

View File

@ -1,6 +1,19 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
webpack: (config) => {
config.resolve.alias.canvas = false;
return config;
},
output: 'export',
images: {
unoptimized: true
},
exportPathMap: async (defaultMap, ctx) => {
return defaultMap
}
// basePath: '/github-pages',
} }
module.exports = nextConfig module.exports = nextConfig

View File

@ -6,18 +6,34 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint",
"export": "next export"
},
"engines": {
"node": "20"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.4.2", "@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/free-regular-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@types/nodemailer": "^6.4.14",
"@types/react-google-recaptcha": "^2.1.8",
"iframe-resizer-react": "^1.1.0",
"next": "latest", "next": "latest",
"nodemailer": "^6.9.7",
"pdfjs-dist": "^4.0.269",
"react": "latest", "react": "latest",
"react-dom": "latest", "react-dom": "latest",
"react-modal": "^3.16.1" "react-google-recaptcha": "^3.1.0",
"react-hook-form": "^7.47.0",
"react-modal": "^3.16.1",
"react-pdf": "^7.4.0",
"sharp": "^0.32.6"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.0.9",
"@types/node": "latest", "@types/node": "latest",
"@types/react": "latest", "@types/react": "latest",
"@types/react-dom": "latest", "@types/react-dom": "latest",

View File

@ -1,6 +1,11 @@
import { Html, Head, Main, NextScript } from 'next/document' import { Html, Head, Main, NextScript } from 'next/document'
import path from 'node:path';
import { pdfjs } from 'react-pdf';
export default function Document() { export default function Document() {
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
return ( return (
<Html lang="en"> <Html lang="en">
<Head> <Head>
@ -9,11 +14,6 @@ export default function Document() {
type="text/css" type="text/css"
href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
/> />
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-modal/3.14.3/react-modal.min.js"
integrity="sha512-MY2jfK3DBnVzdS2V8MXo5lRtr0mNRroUI9hoLVv2/yL3vrJTam3VzASuKQ96fLEpyYIT4a8o7YgtUs5lPjiLVQ=="
crossOrigin="anonymous"
referrerPolicy="no-referrer"
/>
</Head> </Head>
<body> <body>
<Main /> <Main />

101
pages/api/contact.ts Normal file
View File

@ -0,0 +1,101 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import nodemailer from 'nodemailer';
type Data = {
firstName: string,
lastName: string,
email: string,
subject: string,
summary: string,
}
type MailData = {
from?: string,
to?: string,
subject?: string,
text?: string,
html?: string
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
console.log(req.body)
const {
firstName,
lastName,
subject,
email,
summary,
captchaToken
} = req.body;
const {
SMTP_PROXY_EMAIL,
SMTP_RECIPIENT_EMAIL,
SMTP_HOST,
SMTP_PORT,
SMTP_USERNAME,
SMTP_PASSWORD,
RECAPTCHA_SECRET_KEY
} = process.env;
const transporter = nodemailer.createTransport({
port: parseInt(SMTP_PORT!),
host: SMTP_HOST!,
auth: {
user: SMTP_USERNAME!,
pass: SMTP_PASSWORD!,
},
secure: true,
});
console.log(JSON.stringify({SMTP_PROXY_EMAIL, SMTP_RECIPIENT_EMAIL, SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD}));
try {
const response = await fetch(
`https://www.google.com/recaptcha/api/siteverify?secret=${RECAPTCHA_SECRET_KEY}&response=${captchaToken}`
);
if ((await response.json()).success) {
//reCaptcha verification successfull
const mailData: MailData = {
from: SMTP_PROXY_EMAIL!,
to: SMTP_RECIPIENT_EMAIL!,
subject: `Message From ${firstName} ${lastName}: ${subject}`,
text: summary + " | Sent from: " + email,
html: `<div>${summary}</div><p>Sent from:
${email}</p>`
}
transporter.sendMail(mailData, function (err, info) {
if(err) {
console.log(err);
// res.status(500).send('Internal Server Error');
res.status(500).end();
}
else {
console.log('successful');
console.log(info);
res.status(200).end();
}
})
res.status(200).json(req.body);
} else {
// reCAPTCHA verification failed
// res.status(400).send('reCAPTCHA verification failed.');
res.status(400).end();
}
} catch (error) {
console.error(error);
// res.status(500).send('Internal server error');
res.status(500).end();
}
}

112
pages/index.tsx Normal file → Executable file
View File

@ -1,14 +1,20 @@
import React, {useState} from 'react' import React, {useRef, useState} from 'react'
import Image from 'next/image' import Image from 'next/image'
import backgroundPic from '../public/unspash_image.jpg' import backgroundPic from '../public/unspash_image.jpg'
import { Inter } from 'next/font/google' import { Inter } from 'next/font/google'
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGem } from "@fortawesome/free-solid-svg-icons"; import { faChevronCircleUp } from "@fortawesome/free-solid-svg-icons";
import navigationContent from '../lib/navigationContent' import navigationContent from '../lib/navigationContent'
import Modal from 'react-modal'; import ContactForm from '@/components/form/ContactForm';
import Footer from '@/components/footer';
import Link from 'next/link';
const inter = Inter({ subsets: ['latin'] }) const inter = Inter({ subsets: ['latin'] })
export default function Home() { export default function Home() {
const contactFormSectionRef = useRef<HTMLDivElement>(null);
const landingSectionRef = useRef<HTMLDivElement>(null);
const [contactClicked, setContactClicked] = useState(false);
const customStyles = { const customStyles = {
overlay: { overlay: {
backgroundColor: 'rgba(0, 0, 0, 0.6)' backgroundColor: 'rgba(0, 0, 0, 0.6)'
@ -20,22 +26,24 @@ export default function Home() {
bottom: 'auto', bottom: 'auto',
marginRight: '-50%', marginRight: '-50%',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
backgroundColor: 'gray' backgroundColor: 'gray',
width: '75vw'
} }
} }
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [modalData, setModalData] = useState<{name: string, url: string, content?: any}>({ const [modalData, setModalData] = useState<{name: string, url: string, content?: string | React.JSX.Element | null}>({
name: '', name: '',
url: '', url: '',
content: '' content: ''
}); });
const renderNav = (navData: Array<{name: string, url: string, content: (string | React.ElementType | null)}>) => { const renderNav = (navData: Array<{name: string, url: string, content: (string | React.JSX.Element | null)}>) => {
return navData.map((data, idx) => ( return navData.map((data, idx) => (
<span <span
onClick={() => { onClick={() => {
setShowModal(true) setShowModal(true);
setModalData(data); setModalData(data);
}} }}
key={idx} key={idx}
@ -43,27 +51,87 @@ export default function Home() {
> >
<a href={data.url}>{data.name}</a> <a href={data.url}>{data.name}</a>
</span> </span>
)); )).concat([
<span
onClick={async () => {
setContactClicked(true);
setTimeout(() => {
executeScrollToContact();
}, 10);
}}
key={navData.length}
className='p-4 border-white text-white border-2 hover:cursor-pointer hover:bg-white hover:bg-opacity-10'
>
<span>CONTACT</span>
</span>
]);
}; };
const executeScrollToContact = () => contactFormSectionRef.current?.scrollIntoView({behavior: 'smooth'});
const executeScrollToLandiing = () => landingSectionRef.current?.scrollIntoView({behavior: 'smooth'});
return ( return (
<div className='relative'> <div className='relative h-full w-full flex-row justify-center'>
<div className='absolute tablet:w-full h-full bg-cover'> <div id="landing-section" ref={landingSectionRef} className='h-full w-full'>
<Image src={backgroundPic} alt="record background"/> {!showModal && <div className='tablet:w-full flex flex-row justify-center h-full bg-cover bg-black bg-opacity-30'>
<div className='flex flex-col justify-center text-center'> {/* <Image src={backgroundPic} alt="record background"/> */}
{/* <FontAwesomeIcon width={25} height={50} icon={faGem} className="fas fa-gem" style={{ color: "red" }} /> */} <div className='w-1/2 flex flex-col justify-center text-center'>
<div className='w-full grid grid-row-4 justify-center h-1/2'> <div className="py-20 flex flex-col text-white border-white border-y-2 border-solid">
<div className='col-span-2 w-1/2 border-t-2 border-white border-r-2'></div> <div className='text-2xl'><h2>Okechi Onyeje</h2></div>
<div className='col-span-2 h-1/2 w-1/2 border-t-2 border-white border-l-2'></div> <div className='flex flex-row justify-center space-x-2'>
<span>Software Professional</span>
<span>|</span>
<span>
Application Artisan
</span>
<span>|</span>
<span>
Creative & Technology Evangilist
</span>
</div>
</div>
<div className='flex flex-row justify-center'>
<div className='flex flex-col'>
<div className='flex flex-row'>
{renderNav(navigationContent)}
</div>
</div>
</div>
</div> </div>
<div className='flex flex-row justify-center'> </div>}
{renderNav(navigationContent)} {showModal && (
<div className='justify-center flex flex-row bg-black bg-opacity-40'>
<div className="h-full w-full laptop:px-24 absolute z-10 p-10 overflow-y-auto">
<div className='flex flex-row justify-center'>
<div className=" h-max w-3/4 z-20 bg-black bg-opacity-90 text-white rounded overflow-clip">
<div className="w-full flex flex-row justify-end p-4">
<span className="cursor-pointer" onClick={() => setShowModal(false)}>X</span>
</div>
{modalData.content}
</div>
</div>
</div>
</div>
)}
</div>
{contactClicked && <div id='contact-form-section' ref={contactFormSectionRef} className='h-screen bg-black'>
<div className='h-full'>
<div className='flex flex-row h-full w-full justify-center'>
<div className='flex flex-col h-full w-full justify-center'>
<ContactForm/>
</div>
<span className="cursor-pointer self-end" onClick={() => {
executeScrollToLandiing();
setTimeout(() => {
setContactClicked(false);
}, 10);
}}>
<FontAwesomeIcon width={25} height={200} icon={faChevronCircleUp} className="fas fa-chevron-circle-up" style={{ color: "white" }} />
</span>
</div> </div>
</div> </div>
</div>
<Modal isOpen={showModal} contentLabel='' style={customStyles} onRequestClose={() => setShowModal(false)}> </div>}
{modalData.content}
</Modal>
</div> </div>
) )
} }

View File

@ -2,5 +2,6 @@ module.exports = {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
"@tailwindcss/postcss": {}
}, },
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

BIN
public/gethip-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

10
public/gitea-logo.svg Normal file
View File

@ -0,0 +1,10 @@
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<style>
.st1{fill:#000}
</style>
<g id="Icon">
<circle cx="512" cy="512" r="512" style="fill:#FFFFFF"/>
<path class="st1" d="M762.2 350.3c-100.9 5.3-160.7 8-212 8.5v114.1l-16-7.9-.1-106.1c-58.9 0-110.7-3.1-209.1-8.6-12.3-.1-29.5-2.4-47.9-2.5-47.1-.1-110.2 33.5-106.7 118C175.8 597.6 296 609.9 344 610.9c5.3 24.7 61.8 110.1 103.6 114.6H631c109.9-8.2 192.3-373.8 131.2-375.2zm-546 117.3c-4.7-36.6 11.8-74.8 73.2-73.2C296.1 462 307 501.5 329 561.9c-56.2-7.4-104-25.7-112.8-94.3zm415.6 83.5-51.3 105.6c-6.5 13.4-22.7 19-36.2 12.5l-105.6-51.3c-13.4-6.5-19-22.7-12.5-36.2l51.3-105.6c6.5-13.4 22.7-19 36.2-12.5l105.6 51.3c13.4 6.6 19 22.8 12.5 36.2z"/>
<path class="st1" d="M555 609.9c.1-.2.2-.3.2-.5 17.2-35.2 24.3-49.8 19.8-62.4-3.9-11.1-15.5-16.6-36.7-26.6-.8-.4-1.7-.8-2.5-1.2.2-2.3-.1-4.7-1-7-.8-2.3-2.1-4.3-3.7-6l13.6-27.8-11.9-5.8-13.7 28.4c-2 0-4.1.3-6.2 1-8.9 3.2-13.5 13-10.3 21.9.7 1.9 1.7 3.5 2.8 5l-23.6 48.4c-1.9 0-3.8.3-5.7 1-8.9 3.2-13.5 13-10.3 21.9 3.2 8.9 13 13.5 21.9 10.3 8.9-3.2 13.5-13 10.3-21.9-.9-2.5-2.3-4.6-4-6.3l23-47.2c2.5.2 5 0 7.5-.9 2.1-.8 3.9-1.9 5.5-3.3.9.4 1.9.9 2.7 1.3 17.4 8.2 27.9 13.2 30 19.1 2.6 7.5-5.1 23.4-19.3 52.3-.1.2-.2.5-.4.7-2.2-.1-4.4.2-6.5 1-8.9 3.2-13.5 13-10.3 21.9 3.2 8.9 13 13.5 21.9 10.3 8.9-3.2 13.5-13 10.3-21.9-.6-2-1.9-4-3.4-5.7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
public/vendoo-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -18,12 +18,16 @@
body { body {
color: rgb(var(--foreground-rgb)); color: rgb(var(--foreground-rgb));
height: 100vh;
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,
transparent, transparent,
rgb(var(--background-end-rgb)) rgb(var(--background-end-rgb))
) )
rgb(var(--background-start-rgb)); rgb(var(--background-start-rgb));
background-image: url("../public/unspash_image.jpg");
background-size: cover;
background-position: center;
} }
.modal-wrapper { .modal-wrapper {
@ -60,3 +64,25 @@ body {
justify-content: flex-end; justify-content: flex-end;
font-size: 25px; font-size: 25px;
} }
.content {
content: '';
display: block;
position: absolute;
top: calc(-3.5rem - 1px);
left: calc(50% - 1px);
width: 1px;
height: calc(3.5rem + 1px);
background: #ffffff;
}
.use-middle::before {
content: '';
display: block;
position: absolute;
top: calc(-3.5rem - 1px);
left: calc(50% - 1px);
width: 1px;
height: calc(3.5rem + 1px);
background: #ffffff;
}

View File

@ -14,6 +14,16 @@ const config: Config = {
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
}, },
}, },
screens: {
'tablet': '640px',
// => @media (min-width: 640px) { ... }
'laptop': '1024px',
// => @media (min-width: 1024px) { ... }
'desktop': '1280px',
// => @media (min-width: 1280px) { ... }
},
}, },
plugins: [], plugins: [],
} }

929
yarn.lock

File diff suppressed because it is too large Load Diff