Compare commits
1 Commits
Appointmen
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d8c20826b0 |
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
.git
|
||||
.env
|
||||
111
.drone.yml
Executable file
111
.drone.yml
Executable 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
10
.env.dev
Normal 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
12
.env.drone
Normal 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
|
||||
38
.gitea/workflows/manual_deploy_to_coolify.yaml
Normal file
38
.gitea/workflows/manual_deploy_to_coolify.yaml
Normal 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
1
.gitignore
vendored
Normal file → Executable file
@ -26,6 +26,7 @@ yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
18
Dockerfile
Normal file
18
Dockerfile
Normal 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
2
ansible/inventory.yml
Executable file
@ -0,0 +1,2 @@
|
||||
deployment_servers:
|
||||
hosts: localhost
|
||||
60
ansible/playbook.staging.yml
Executable file
60
ansible/playbook.staging.yml
Executable 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
5
ansible/requirements.yml
Executable file
@ -0,0 +1,5 @@
|
||||
collections:
|
||||
# Install a collection from Ansible Galaxy.
|
||||
- name: community.docker
|
||||
version: ">=3.4.8"
|
||||
source: https://galaxy.ansible.com
|
||||
49
components/content/carousel/index.tsx
Executable file
49
components/content/carousel/index.tsx
Executable 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
24
components/content/index.tsx
Normal file → Executable file
@ -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 (
|
||||
<div className="p-4 flex flex-col">
|
||||
<div className="pb-4 border-b-2 border-white">{title}</div>
|
||||
{heroImgSrc && <div><Image src={heroImgSrc} alt=""/></div>}
|
||||
<div className="h-full p-4 px-10 flex flex-col">
|
||||
<div className=" w-fit pb-1 mb-8 border-b-2 border-white">{title}</div>
|
||||
{heroImgSrc && <div className="mb-8 h-full flex flex-row justify-center"><Image height={200} src={heroImgSrc} alt=""/></div>}
|
||||
<div>{description}</div>
|
||||
<div>{innerChildren}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
8
components/footer.tsx
Normal file
8
components/footer.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import React from "react";
|
||||
export default function Footer({}) {
|
||||
return (
|
||||
<div className="w-full bg-black h-12">
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
114
components/form/ContactForm/index.tsx
Normal file
114
components/form/ContactForm/index.tsx
Normal 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;
|
||||
@ -1,13 +1,19 @@
|
||||
// import Navbar from './navbar'
|
||||
// import Footer from './footer'
|
||||
import Navbar from './navbar'
|
||||
import Footer from './footer'
|
||||
import React from 'react'
|
||||
|
||||
export default function Layout({ children }) {
|
||||
export interface LayoutProps {
|
||||
children: React.JSX.Element
|
||||
};
|
||||
|
||||
export default function Layout({children}: LayoutProps) {
|
||||
return (
|
||||
<>
|
||||
{/* <Navbar /> */}
|
||||
<main>{children}</main>
|
||||
{/* <Footer /> */}
|
||||
</>
|
||||
<div style={{height: '100vh', width: '100vw'}}>
|
||||
<Navbar />
|
||||
<main style={{height: '86%'}}>
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
45
components/navbar.tsx
Normal file
45
components/navbar.tsx
Normal 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
27
docker-compose.dev.yaml
Executable 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
25
docker-compose.drone.yaml
Executable 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
17
docker-compose.yaml
Executable 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
34
lib/content.ts
Executable 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
55
lib/navigationContent.tsx
Normal file → Executable file
@ -1,42 +1,49 @@
|
||||
import React from 'react';
|
||||
import Content from '../components/content'
|
||||
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',
|
||||
content: <Content
|
||||
title='Intro'
|
||||
heroImgSrc={backgroundPic}
|
||||
description={"Intro Section about okechi"}
|
||||
description={contentValues.intro.description}
|
||||
/>
|
||||
},
|
||||
{
|
||||
name: 'WORK',
|
||||
name: contentValues.work.name,
|
||||
url: '#work',
|
||||
content: <Content
|
||||
title='Work'
|
||||
heroImgSrc={backgroundPic}
|
||||
description={"Work Section about okechi"}
|
||||
heroImgSrc={null}
|
||||
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',
|
||||
url: '#about',
|
||||
content: <Content
|
||||
title='About'
|
||||
heroImgSrc={backgroundPic}
|
||||
description={"About Section about okechi"}
|
||||
/>
|
||||
},
|
||||
{
|
||||
name: 'CONTACT',
|
||||
url: '#contact',
|
||||
content: <Content
|
||||
title='Contact'
|
||||
heroImgSrc={backgroundPic}
|
||||
description={"Contact Section about okechi"}
|
||||
/>
|
||||
}
|
||||
// {
|
||||
// name: contentValues.about.name,
|
||||
// url: '#about',
|
||||
// content: <Content
|
||||
// title='About'
|
||||
// heroImgSrc={backgroundPic}
|
||||
// description={contentValues.about.description}
|
||||
// />
|
||||
// },
|
||||
];
|
||||
|
||||
export default navigationContent;
|
||||
31
lib/portfolio.ts
Executable file
31
lib/portfolio.ts
Executable 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;
|
||||
@ -1,6 +1,19 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
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
|
||||
|
||||
20
package.json
20
package.json
@ -6,18 +6,34 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"export": "next export"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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/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",
|
||||
"nodemailer": "^6.9.7",
|
||||
"pdfjs-dist": "^4.0.269",
|
||||
"react": "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": {
|
||||
"@tailwindcss/postcss": "^4.0.9",
|
||||
"@types/node": "latest",
|
||||
"@types/react": "latest",
|
||||
"@types/react-dom": "latest",
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
import { Html, Head, Main, NextScript } from 'next/document'
|
||||
import path from 'node:path';
|
||||
import { pdfjs } from 'react-pdf';
|
||||
|
||||
|
||||
export default function Document() {
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
|
||||
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
@ -9,11 +14,6 @@ export default function Document() {
|
||||
type="text/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>
|
||||
<body>
|
||||
<Main />
|
||||
|
||||
101
pages/api/contact.ts
Normal file
101
pages/api/contact.ts
Normal 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
112
pages/index.tsx
Normal file → Executable file
@ -1,14 +1,20 @@
|
||||
import React, {useState} from 'react'
|
||||
import React, {useRef, useState} from 'react'
|
||||
import Image from 'next/image'
|
||||
import backgroundPic from '../public/unspash_image.jpg'
|
||||
import { Inter } from 'next/font/google'
|
||||
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 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'] })
|
||||
|
||||
export default function Home() {
|
||||
const contactFormSectionRef = useRef<HTMLDivElement>(null);
|
||||
const landingSectionRef = useRef<HTMLDivElement>(null);
|
||||
const [contactClicked, setContactClicked] = useState(false);
|
||||
|
||||
const customStyles = {
|
||||
overlay: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)'
|
||||
@ -20,22 +26,24 @@ export default function Home() {
|
||||
bottom: 'auto',
|
||||
marginRight: '-50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
backgroundColor: 'gray'
|
||||
backgroundColor: 'gray',
|
||||
width: '75vw'
|
||||
}
|
||||
}
|
||||
|
||||
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: '',
|
||||
url: '',
|
||||
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) => (
|
||||
<span
|
||||
onClick={() => {
|
||||
setShowModal(true)
|
||||
setShowModal(true);
|
||||
setModalData(data);
|
||||
}}
|
||||
key={idx}
|
||||
@ -43,27 +51,87 @@ export default function Home() {
|
||||
>
|
||||
<a href={data.url}>{data.name}</a>
|
||||
</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 (
|
||||
<div className='relative'>
|
||||
<div className='absolute tablet:w-full h-full bg-cover'>
|
||||
<Image src={backgroundPic} alt="record background"/>
|
||||
<div className='flex flex-col justify-center text-center'>
|
||||
{/* <FontAwesomeIcon width={25} height={50} icon={faGem} className="fas fa-gem" style={{ color: "red" }} /> */}
|
||||
<div className='w-full grid grid-row-4 justify-center h-1/2'>
|
||||
<div className='col-span-2 w-1/2 border-t-2 border-white border-r-2'></div>
|
||||
<div className='col-span-2 h-1/2 w-1/2 border-t-2 border-white border-l-2'></div>
|
||||
<div className='relative h-full w-full flex-row justify-center'>
|
||||
<div id="landing-section" ref={landingSectionRef} className='h-full w-full'>
|
||||
{!showModal && <div className='tablet:w-full flex flex-row justify-center h-full bg-cover bg-black bg-opacity-30'>
|
||||
{/* <Image src={backgroundPic} alt="record background"/> */}
|
||||
<div className='w-1/2 flex flex-col justify-center text-center'>
|
||||
<div className="py-20 flex flex-col text-white border-white border-y-2 border-solid">
|
||||
<div className='text-2xl'><h2>Okechi Onyeje</h2></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 className='flex flex-row justify-center'>
|
||||
{renderNav(navigationContent)}
|
||||
</div>}
|
||||
{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>
|
||||
<Modal isOpen={showModal} contentLabel='' style={customStyles} onRequestClose={() => setShowModal(false)}>
|
||||
{modalData.content}
|
||||
</Modal>
|
||||
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,5 +2,6 @@ module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
"@tailwindcss/postcss": {}
|
||||
},
|
||||
}
|
||||
|
||||
BIN
public/aqua_intelligence_image.png
Normal file
BIN
public/aqua_intelligence_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
BIN
public/assets/Okechi_Onyeje_Resume_Professional_2023.pdf
Executable file
BIN
public/assets/Okechi_Onyeje_Resume_Professional_2023.pdf
Executable file
Binary file not shown.
BIN
public/gethip-image.png
Normal file
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
10
public/gitea-logo.svg
Normal 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
BIN
public/vendoo-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@ -18,12 +18,16 @@
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
height: 100vh;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
rgb(var(--background-end-rgb))
|
||||
)
|
||||
rgb(var(--background-start-rgb));
|
||||
background-image: url("../public/unspash_image.jpg");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.modal-wrapper {
|
||||
@ -60,3 +64,25 @@ body {
|
||||
justify-content: flex-end;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -14,6 +14,16 @@ const config: Config = {
|
||||
'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: [],
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user