├── .eslintrc.json ├── public ├── favicon.ico ├── favicon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── projects │ ├── blog.png │ ├── book.png │ ├── vet2.png │ ├── current.png │ └── movies.webp ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── site.webmanifest └── vercel.svg ├── postcss.config.js ├── next.config.js ├── src ├── pages │ ├── about.tsx │ ├── contact.tsx │ ├── projects.tsx │ ├── _document.tsx │ ├── api │ │ └── contact.ts │ ├── _app.tsx │ └── index.tsx ├── components │ ├── SectionContainer.tsx │ ├── layout │ │ ├── index.tsx │ │ └── Navbar.tsx │ ├── sections │ │ ├── Hobbies.tsx │ │ ├── Education.tsx │ │ ├── About.tsx │ │ ├── Contact.tsx │ │ └── Projects.tsx │ └── MobileNav.tsx ├── styles │ └── globals.css └── assets │ ├── mail.tsx │ ├── edit.tsx │ ├── github.tsx │ └── whatsapp.tsx ├── tailwind.config.js ├── .gitignore ├── tsconfig.json ├── README.md └── package.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/portfolio_v2/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/portfolio_v2/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/portfolio_v2/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/portfolio_v2/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/projects/blog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/portfolio_v2/HEAD/public/projects/blog.png -------------------------------------------------------------------------------- /public/projects/book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/portfolio_v2/HEAD/public/projects/book.png -------------------------------------------------------------------------------- /public/projects/vet2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/portfolio_v2/HEAD/public/projects/vet2.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/portfolio_v2/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/projects/current.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/portfolio_v2/HEAD/public/projects/current.png -------------------------------------------------------------------------------- /public/projects/movies.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/portfolio_v2/HEAD/public/projects/movies.webp -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/portfolio_v2/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohnMwendwa/portfolio_v2/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /src/pages/about.tsx: -------------------------------------------------------------------------------- 1 | import About from "components/sections/About"; 2 | 3 | const AboutPage = () => { 4 | return ; 5 | }; 6 | 7 | export default AboutPage; 8 | -------------------------------------------------------------------------------- /src/pages/contact.tsx: -------------------------------------------------------------------------------- 1 | import Contact from "components/sections/Contact"; 2 | 3 | const ContactPage = () => { 4 | return ; 5 | }; 6 | 7 | export default ContactPage; 8 | -------------------------------------------------------------------------------- /src/pages/projects.tsx: -------------------------------------------------------------------------------- 1 | import Projects from "components/sections/Projects"; 2 | 3 | const ProjectsPage = () => { 4 | return ; 5 | }; 6 | 7 | export default ProjectsPage; 8 | -------------------------------------------------------------------------------- /src/components/SectionContainer.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | const SectionContainer = ({ children }: { children: ReactNode }) => { 4 | return ( 5 |
6 | {children} 7 |
8 | ); 9 | }; 10 | 11 | export default SectionContainer; 12 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | 8 | // Or if using `src` directory: 9 | "./src/**/*.{js,ts,jsx,tsx,mdx}", 10 | ], 11 | theme: { 12 | extend: { 13 | fontFamily: { 14 | sans: ["var(--font-inter)"], 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .dynamic { 6 | min-height: 100dvh; 7 | } 8 | 9 | input:-webkit-autofill, 10 | input:-webkit-autofill:hover, 11 | input:-webkit-autofill:focus, 12 | input:-webkit-autofill:active, 13 | textarea:-webkit-autofill, 14 | textarea:-webkit-autofill:hover, 15 | textarea:-webkit-autofill:focus, 16 | textarea:-webkit-autofill:active { 17 | -webkit-background-clip: text; 18 | /* background-color: #111827; */ 19 | -webkit-text-fill-color: white; 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "baseUrl": "src", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": false, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "incremental": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve" 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /src/assets/mail.tsx: -------------------------------------------------------------------------------- 1 | const MailIcon = ({ className = "" }) => { 2 | return ( 3 | 12 | ); 13 | }; 14 | 15 | export default MailIcon; 16 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "start_url": "/", 3 | "name": "John Mwendwa", 4 | "short_name": "Mwendwa", 5 | "description": "I'm an advocate for building responsive, accessible and inclusive digital products and experiences for the web based in Nairobi, Kenya.", 6 | "icons": [ 7 | { 8 | "src": "/android-chrome-192x192.png", 9 | "sizes": "192x192", 10 | "type": "image/png", 11 | "purpose": "any maskable" 12 | }, 13 | { 14 | "src": "/android-chrome-512x512.png", 15 | "sizes": "512x512", 16 | "type": "image/png" 17 | } 18 | ], 19 | "theme_color": "rgb(17, 24, 39)", 20 | "background_color": "rgb(17, 24, 39)", 21 | "display": "standalone" 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # My Portfolio Website 2 | ![my portfolio landing page](https://github.com/JohnMwendwa/portfolio/assets/72663882/fdc2a9fa-38a8-4595-bcb3-8b59e08c7aee) 3 | 4 | ![my porfolio gif](https://github.com/JohnMwendwa/portfolio/assets/72663882/0ad7c6f8-81c9-412e-8aea-53cef1412aaa) 5 | 6 | ## 🛠 Development 7 | 8 | 9 | Clone the repository 10 | 11 | ```zsh 12 | https://github.com/JohnMwendwa/portfolio.git 13 | ``` 14 | 15 | Install dependencies 16 | 17 | ```zsh 18 | npm install 19 | 20 | # Or using Yarn 21 | 22 | yarn 23 | ``` 24 | 25 | Start the development server 26 | 27 | ```zsh 28 | npm run dev 29 | 30 | # Or using Yarn 31 | 32 | yarn dev 33 | ``` 34 | 35 | Build for production 36 | 37 | ```zsh 38 | npm run build 39 | 40 | # Or using Yarn 41 | 42 | yarn build 43 | ``` 44 | -------------------------------------------------------------------------------- /src/components/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { motion } from "framer-motion"; 3 | 4 | import Navbar from "./Navbar"; 5 | import SectionContainer from "components/SectionContainer"; 6 | 7 | const Layout = ({ children }: { children: ReactNode }) => { 8 | return ( 9 | 16 | 17 |
{children}
18 |
19 | 20 | © John Mwendwa {new Date().getFullYear()} 21 | 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default Layout; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portfolio", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "export": "next build && next export", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@headlessui/react": "^1.7.15", 14 | "eslint": "8.29.0", 15 | "eslint-config-next": "13.0.6", 16 | "express": "^4.18.2", 17 | "framer-motion": "^10.12.16", 18 | "next": "^13.4.4", 19 | "nodemailer": "^6.9.3", 20 | "react": "18.2.0", 21 | "react-dom": "18.2.0", 22 | "react-toastify": "^9.1.3", 23 | "sharp": "^0.32.5" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "20.2.5", 27 | "@types/nodemailer": "^6.4.8", 28 | "@types/react": "18.2.8", 29 | "autoprefixer": "^10.4.14", 30 | "postcss": "^8.4.23", 31 | "tailwindcss": "^3.3.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/edit.tsx: -------------------------------------------------------------------------------- 1 | const EditIcon = ({ className = "" }) => { 2 | return ( 3 | 12 | ); 13 | }; 14 | export default EditIcon; 15 | -------------------------------------------------------------------------------- /src/assets/github.tsx: -------------------------------------------------------------------------------- 1 | const GithubIcon = ({ className = "" }) => { 2 | return ( 3 | 12 | ); 13 | }; 14 | export default GithubIcon; 15 | -------------------------------------------------------------------------------- /src/assets/whatsapp.tsx: -------------------------------------------------------------------------------- 1 | const WhatsappIcon = ({ className = "" }) => { 2 | return ( 3 | 12 | ); 13 | }; 14 | export default WhatsappIcon; 15 | -------------------------------------------------------------------------------- /src/pages/api/contact.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import nodemailer from "nodemailer"; 3 | 4 | export interface EmailProps { 5 | from: string; 6 | to: string; 7 | subject: string; 8 | text?: string; 9 | } 10 | 11 | const sendEmail = async (mailOptions: EmailProps) => { 12 | const transporter = nodemailer.createTransport({ 13 | host: process.env.EMAIL_HOST, 14 | port: parseInt(process.env.EMAIL_PORT || "465"), 15 | secure: true, 16 | auth: { 17 | user: process.env.EMAIL_NAME, 18 | pass: process.env.EMAIL_PASSWORD, 19 | }, 20 | }); 21 | 22 | const res = await transporter.sendMail(mailOptions); 23 | 24 | return res; 25 | }; 26 | 27 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 28 | if (req.method !== "POST") { 29 | return res.status(400).json({ 30 | error: "Bad Request", 31 | }); 32 | } 33 | 34 | try { 35 | const { name, email, subject, message } = req.body; 36 | 37 | if (!name.trim()) { 38 | return res.status(401).json({ 39 | error: "Please add your name", 40 | }); 41 | } else if (!email.trim() || !email.includes("@")) { 42 | return res.status(401).json({ 43 | error: "Please add a valid email", 44 | }); 45 | } else if (!subject.trim()) { 46 | return res.status(401).json({ 47 | error: "Please add the subject of this email", 48 | }); 49 | } else if (!message.trim()) { 50 | return res.status(401).json({ 51 | error: "Please add your name", 52 | }); 53 | } 54 | 55 | const response = await sendEmail({ 56 | from: `Portfolio <${process.env.EMAIL_NAME}>`, 57 | to: process.env.EMAIL_TO, 58 | subject: subject, 59 | text: `${name} - ${email} \n\n${message}`, 60 | }); 61 | 62 | if (response.rejected.length || !response.messageId) { 63 | res.status(400).json({ 64 | error: "Email not sent", 65 | }); 66 | return; 67 | } 68 | 69 | if (response.messageId) { 70 | res.status(200).json({ 71 | message: "Email sent successfully!", 72 | }); 73 | return; 74 | } 75 | } catch (e) { 76 | res.status(500).json({ 77 | error: `Email not sent - ${e.message}`, 78 | }); 79 | } 80 | }; 81 | 82 | export default handler; 83 | -------------------------------------------------------------------------------- /src/components/sections/Hobbies.tsx: -------------------------------------------------------------------------------- 1 | import { motion, Variants } from "framer-motion"; 2 | 3 | interface HobbieProps { 4 | title: string; 5 | description: string; 6 | } 7 | 8 | const HobbieDetails: HobbieProps[] = [ 9 | { 10 | title: "Skating", 11 | description: 12 | "To tell you the truth, I love skating. It's one of those things I can hardly let a day pass by without even taking atleast a 5 minutes ride around my neighborhood.", 13 | }, 14 | { 15 | title: "Travelling", 16 | description: 17 | "If you're not a kenyan, you wouldn't understand my obsession with travelling, especially outside the city. As a nation, we've been endowed with great sites that are just breath-taking.", 18 | }, 19 | { 20 | title: "Cooking", 21 | description: 22 | "I'm not a great fan of fast foods and I believe they should be taken once in a while and so I have learned to cook my dishes and boy ooh boy do I love their taste. Well, I know not everyone finds my dishes as tasty as I do, but need I remind them the opinion of my stomach and only my stomach concerning this matter supersedes all 😎.", 23 | }, 24 | { 25 | title: "Movies", 26 | description: 27 | "I'm also a big fan of movies and series. As I was growing up, my obsession with them always got me into trouble with my school teachers, but that's a story for another day.", 28 | }, 29 | ]; 30 | 31 | const Container: Variants = { 32 | initial: {}, 33 | animate: { 34 | transition: { 35 | staggerChildren: 0.75, 36 | }, 37 | }, 38 | }; 39 | 40 | const Word: Variants = { 41 | initial: { 42 | opacity: 0, 43 | y: 50, 44 | }, 45 | animate: { 46 | opacity: 1, 47 | y: 0, 48 | transition: { 49 | duration: 1, 50 | }, 51 | }, 52 | }; 53 | 54 | const Hobbies = () => { 55 | return ( 56 |
57 |

58 | Hobbies 59 |

60 |

61 | Whenever I'm not coding, I tend to engage myself in activties that 62 | build me up. Some of these activities include but not limited to the 63 | following : 64 |

65 | 71 | {HobbieDetails.map((hobbie, idx) => ( 72 | 73 | {hobbie.title} : 74 | {hobbie.description} 75 | 76 | ))} 77 | 78 |
79 | ); 80 | }; 81 | 82 | export default Hobbies; 83 | -------------------------------------------------------------------------------- /src/components/MobileNav.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from "react"; 2 | import { Menu, Transition } from "@headlessui/react"; 3 | import Link from "next/link"; 4 | import { MenuProps } from "./layout/Navbar"; 5 | 6 | interface MobileNavProps { 7 | MenuItems: MenuProps[]; 8 | } 9 | 10 | const MobileNav = ({ MenuItems }: MobileNavProps) => { 11 | return ( 12 | 13 | 17 | 25 | 26 | 27 | 36 | 37 | {MenuItems.map((item) => ( 38 | 39 | {({ active }) => ( 40 | 46 | {item.name} 47 | 48 | )} 49 | 50 | ))} 51 | 52 | {({ active }) => ( 53 | 62 | 73 | Github 74 | 75 | )} 76 | 77 | 78 | 79 | 80 | ); 81 | }; 82 | 83 | export default MobileNav; 84 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import { Inter } from "next/font/google"; 3 | import { useRouter } from "next/router"; 4 | import { motion, AnimatePresence, Variants } from "framer-motion"; 5 | 6 | import Layout from "components/layout"; 7 | import "../styles/globals.css"; 8 | 9 | const inter = Inter({ 10 | subsets: ["latin"], 11 | variable: "--font-inter", 12 | }); 13 | 14 | const JsonLdData = () => { 15 | return { 16 | __html: `{ 17 | "@context" : "https://schema.org", 18 | "@type" : "WebSite", 19 | "name" : "John Mwendwa", 20 | "url" : "https://johnmwendwa.me/", 21 | "description" : "I'm an advocate for building responsive, accessible and inclusive digital products and experiences for the web based in Nairobi, Kenya.", 22 | "keywords" : "Full Stack web developer, John Mwendwa, Mwendwa, Front-End, React, JavaScript, Node.js, Next.js, MongoDB , SEO", 23 | "author" : { 24 | "@type": "Person", 25 | "name": "John Mwendwa", 26 | "url": "https://johnmwendwa.me/", 27 | "image": "", 28 | "sameAs": [ 29 | "https://github.com/JohnMwendwa", 30 | "https://johnmwendwa.me/" 31 | ], 32 | "jobTitle": "Full Stack Web Developer" 33 | } 34 | }`, 35 | }; 36 | }; 37 | 38 | function MyApp({ Component, pageProps }) { 39 | const router = useRouter(); 40 | return ( 41 | <> 42 | 43 | John Mwendwa | Full Stack Web Developer 44 | 45 | 50 | 51 | 55 | 56 | 57 | 61 | 62 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 83 | 89 | 95 | 96 |