├── .env.example ├── .eslintrc.json ├── .github └── FUNDING.yml ├── .gitignore ├── README.md ├── app ├── api │ ├── contact │ │ └── route.js │ └── google │ │ └── route.js ├── blog │ ├── [slug] │ │ └── page.js │ └── page.js ├── components │ ├── footer.jsx │ ├── helper │ │ ├── animation-lottie.jsx │ │ ├── glow-card.jsx │ │ └── scroll-to-top.jsx │ ├── homepage │ │ ├── about │ │ │ └── index.jsx │ │ ├── blog │ │ │ ├── blog-card.jsx │ │ │ └── index.jsx │ │ ├── contact │ │ │ ├── contact-with-captcha.jsx │ │ │ ├── contact-without-captcha.jsx │ │ │ └── index.jsx │ │ ├── education │ │ │ └── index.jsx │ │ ├── experience │ │ │ └── index.jsx │ │ ├── hero-section │ │ │ └── index.jsx │ │ ├── projects │ │ │ ├── index.jsx │ │ │ ├── project-card.jsx │ │ │ └── single-project.jsx │ │ └── skills │ │ │ └── index.jsx │ └── navbar.jsx ├── css │ ├── card.scss │ └── globals.scss ├── favicon.ico ├── layout.js └── page.js ├── jsconfig.json ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── blur-23.svg ├── card.png ├── grid.svg ├── hero.svg ├── image │ ├── ayla.jpg │ ├── crefin.jpg │ ├── portfolio.gif │ ├── real-estate.jpg │ ├── screen.png │ └── travel.jpg ├── lottie │ ├── build.json │ ├── code.json │ ├── coding.json │ ├── contact.json │ ├── development.json │ ├── education.json │ ├── js.json │ ├── lotti.json │ └── study.json ├── next.svg ├── png │ └── placeholder.png ├── profile.png ├── section.svg ├── svg │ ├── contactsImage.svg │ ├── education │ │ ├── eduBlack.svg │ │ ├── eduBlue.svg │ │ ├── eduGreen.svg │ │ ├── eduImgBlack.svg │ │ ├── eduImgWhite.svg │ │ ├── eduOrange.svg │ │ ├── eduPink.svg │ │ ├── eduPurple.svg │ │ ├── eduRed.svg │ │ ├── eduTwitter.svg │ │ └── eduYellow.svg │ ├── experience │ │ ├── expBlack.svg │ │ ├── expBlue.svg │ │ ├── expGreen.svg │ │ ├── expImgBlack.svg │ │ ├── expImgWhite.svg │ │ ├── expOrange.svg │ │ ├── expPink.svg │ │ ├── expPurple.svg │ │ ├── expRed.svg │ │ ├── expTwitter.svg │ │ └── expYellow.svg │ ├── projects │ │ ├── eight.svg │ │ ├── eleven.svg │ │ ├── five.svg │ │ ├── four.svg │ │ ├── nine.svg │ │ ├── one.svg │ │ ├── sample.svg │ │ ├── seven.svg │ │ ├── six.svg │ │ ├── ten.svg │ │ ├── thirteen.svg │ │ ├── three.svg │ │ ├── twelve.svg │ │ └── two.svg │ └── skills │ │ ├── adobe-xd.svg │ │ ├── adobeaudition.svg │ │ ├── after-effects.svg │ │ ├── angular.svg │ │ ├── aws.svg │ │ ├── azure.svg │ │ ├── blender.svg │ │ ├── bootstrap.svg │ │ ├── bulma.svg │ │ ├── c.svg │ │ ├── canva.svg │ │ ├── capacitorjs.svg │ │ ├── coffeescript.svg │ │ ├── cplusplus.svg │ │ ├── csharp.svg │ │ ├── css.svg │ │ ├── dart.svg │ │ ├── deno.svg │ │ ├── django.svg │ │ ├── docker.svg │ │ ├── fastify.svg │ │ ├── figma.svg │ │ ├── firebase.svg │ │ ├── flutter.svg │ │ ├── gcp.svg │ │ ├── gimp.svg │ │ ├── git.svg │ │ ├── go.svg │ │ ├── graphql.svg │ │ ├── haxe.svg │ │ ├── html.svg │ │ ├── illustrator.svg │ │ ├── ionic.svg │ │ ├── java.svg │ │ ├── javascript.svg │ │ ├── julia.svg │ │ ├── kotlin.svg │ │ ├── lightroom.svg │ │ ├── markdown.svg │ │ ├── materialui.svg │ │ ├── matlab.svg │ │ ├── memsql.svg │ │ ├── microsoftoffice.svg │ │ ├── mongoDB.svg │ │ ├── mysql.svg │ │ ├── nextJS.svg │ │ ├── nginx.svg │ │ ├── numpy.svg │ │ ├── nuxtJS.svg │ │ ├── opencv.svg │ │ ├── photoshop.svg │ │ ├── php.svg │ │ ├── picsart.svg │ │ ├── postgresql.svg │ │ ├── premierepro.svg │ │ ├── python.svg │ │ ├── pytorch.svg │ │ ├── react.svg │ │ ├── ruby.svg │ │ ├── selenium.svg │ │ ├── sketch.svg │ │ ├── sqlite.svg │ │ ├── strapi.svg │ │ ├── svelte.svg │ │ ├── swift.svg │ │ ├── tailwind.svg │ │ ├── tensorflow.svg │ │ ├── typescript.svg │ │ ├── unity.svg │ │ ├── vitejs.svg │ │ ├── vue.svg │ │ ├── vuetifyjs.svg │ │ ├── webix.svg │ │ ├── wolframalpha.svg │ │ └── wordpress.svg ├── top-bg.svg └── vercel.svg ├── tailwind.config.js └── utils ├── check-email.js ├── data ├── contactsData.js ├── educations.js ├── experience.js ├── personal-data.js ├── projects-data.js └── skills.js ├── skill-image.js └── time-converter.js /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_EMAILJS_SERVICE_ID = 2 | NEXT_PUBLIC_EMAILJS_TEMPLATE_ID = 3 | NEXT_PUBLIC_EMAILJS_PUBLIC_KEY = 4 | NEXT_PUBLIC_GTM = 5 | NEXT_PUBLIC_APP_URL = "http://127.0.0.1:3000" 6 | NEXT_PUBLIC_RECAPTCHA_SECRET_KEY = 7 | NEXT_PUBLIC_RECAPTCHA_SITE_KEY = -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.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 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | --- 6 | # [Built Portfolio With GitHub ](https://github.com/said7388/github-portfolio) 7 | 8 | --- 9 | 10 | # Developer Portfolio 11 | 12 | ### Are you struggling to create a professional portfolio website? Look no further! You can use the Developer Portfolio template and create your very own personalized portfolio today! My website is designed to be user-friendly and easily customizable, making it perfect for both developers and freelancers. 13 | 14 | --- 15 | 16 | # Demo :movie_camera: 17 | 18 | ![](./public/image/screen.png) 19 | 20 | ## View live preview [here](https://abusaid.netlify.app/). 21 | 22 | --- 23 | 24 | ## Table of Contents :scroll: 25 | 26 | - [Sections](#sections-bookmark) 27 | - [Demo](#demo-movie_camera) 28 | - [Installation](#installation-arrow_down) 29 | - [Getting Started](#getting-started-dart) 30 | - [Usage](#usage-joystick) 31 | - [Packages Used](#packages-used-package) 32 | 33 | --- 34 | 35 | # Sections :bookmark: 36 | 37 | - HERO SECTION 38 | - ABOUT ME 39 | - EXPERIENCE 40 | - SKILLS 41 | - PROJECTS 42 | - EDUCATION 43 | - BLOG 44 | - CONTACTS 45 | 46 | --- 47 | 48 | # Installation :arrow_down: 49 | 50 | ### You will need to download Git and Node to run this project 51 | 52 | - [Git](https://git-scm.com/downloads) 53 | - [Node](https://nodejs.org/en/download/) 54 | 55 | #### Make sure you have the latest version of both Git and Node on your computer. 56 | 57 | ``` 58 | node --version 59 | git --version 60 | ``` 61 | 62 | ##
63 | 64 | # Getting Started :dart: 65 | 66 | ### Fork and Clone the repo 67 | 68 | To Fork the repo click on the fork button at the top right of the page. Once the repo is forked open your terminal and perform the following commands 69 | 70 | ``` 71 | git clone https://github.com//developer-portfolio.git 72 | 73 | cd developer-portfolio 74 | ``` 75 | 76 | ### Install packages from the root directory 77 | 78 | ```bash 79 | npm install 80 | # or 81 | yarn install 82 | ``` 83 | 84 | Then, run the development server: 85 | 86 | ```bash 87 | npm run dev 88 | # or 89 | yarn dev 90 | ``` 91 | 92 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 93 | 94 | --- 95 | 96 | # Usage :joystick: 97 | 98 | Goto [emailjs.com](https://www.emailjs.com/) and create a new account for the mail sending. In free trial you will get 200 mail per month. After setup `emailjs` account, Please create a new `.env` file from `.env.example` file. 99 | 100 | Eg: 101 | 102 | ```env 103 | NEXT_PUBLIC_EMAILJS_SERVICE_ID = 104 | NEXT_PUBLIC_EMAILJS_TEMPLATE_ID = 105 | NEXT_PUBLIC_EMAILJS_PUBLIC_KEY = 106 | NEXT_PUBLIC_GTM = # For site analytics 107 | NEXT_PUBLIC_APP_URL = "http://127.0.0.1:3000" 108 | NEXT_PUBLIC_RECAPTCHA_SECRET_KEY = # For captcha verification on contact form 109 | NEXT_PUBLIC_RECAPTCHA_SITE_KEY = 110 | ``` 111 | 112 | ### Then, Customize data in the `utils/data` [folder](https://github.com/said7388/developer-portfolio/tree/main/utils/data). 113 | 114 | Eg: 115 | 116 | ```javascript 117 | export const personalData = { 118 | name: "ABU SAID", 119 | profile: "/profile.png", 120 | designation: "Full-Stack Software Developer", 121 | description: "My name is ABU SAID....", 122 | email: "abusaid7388@gmail.com", 123 | phone: "+8801608797655", 124 | address: "Dhaka, Bangladesh", 125 | github: "https://github.com/said7388", 126 | facebook: "https://www.facebook.com/abusaid.riyaz/", 127 | linkedIn: "https://www.linkedin.com/in/abu-said-bd/", 128 | twitter: "https://twitter.com/said7388", 129 | stackOverflow: "https://stackoverflow.com/users/16840768/abu-said", 130 | leetcode: "https://leetcode.com/said3812/", 131 | devUsername: "said7388", 132 | resume: "...", 133 | }; 134 | ``` 135 | 136 | `devUsername` Used for fetching blog from `dev.to`. 137 | 138 | --- 139 | 140 | --- 141 | 142 | # Packages Used :package: 143 | 144 | | Used Package List | 145 | | :----------------: | 146 | | next | 147 | | @emailjs/browser | 148 | | lottie-react | 149 | | react-fast-marquee | 150 | | react-icons | 151 | | react-toastify | 152 | | sass | 153 | | tailwindcss | 154 | 155 | --- 156 | -------------------------------------------------------------------------------- /app/api/contact/route.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { NextResponse } from "next/server"; 3 | 4 | export async function POST(request) { 5 | const payload = await request.json(); 6 | const token = process.env.TELEGRAM_BOT_TOKEN; 7 | const chat_id = process.env.TELEGRAM_CHAT_ID; 8 | 9 | if (!token || !chat_id) { 10 | return NextResponse.json({ 11 | success: false, 12 | }, { status: 200 }); 13 | }; 14 | 15 | try { 16 | const url = `https://api.telegram.org/bot${token}/sendMessage`; 17 | const message = `New message from ${payload.name}\n\nEmail: ${payload.email}\n\nMessage:\n ${payload.message}\n\n`; 18 | 19 | const res = await axios.post(url, { 20 | text: message, 21 | chat_id: process.env.TELEGRAM_CHAT_ID 22 | }); 23 | 24 | if (res.data.ok) { 25 | return NextResponse.json({ 26 | success: true, 27 | message: "Message sent successfully!", 28 | }, { status: 200 }); 29 | }; 30 | } catch (error) { 31 | console.log(error.response.data) 32 | return NextResponse.json({ 33 | message: "Message sending failed!", 34 | success: false, 35 | }, { status: 500 }); 36 | } 37 | }; -------------------------------------------------------------------------------- /app/api/google/route.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export async function POST(request) { 5 | const reqBody = await request.json(); 6 | const secret_key = process.env.NEXT_PUBLIC_RECAPTCHA_SECRET_KEY; 7 | 8 | try { 9 | const url = `https://www.google.com/recaptcha/api/siteverify?secret=${secret_key}&response=${reqBody.token}`; 10 | 11 | const res = await axios.post(url); 12 | if (res.data.success) { 13 | return NextResponse.json({ 14 | message: "Captcha verification success!!", 15 | success: true, 16 | }) 17 | }; 18 | 19 | return NextResponse.json({ 20 | error: "Captcha verification failed!", 21 | success: false, 22 | }, { status: 500 }); 23 | } catch (error) { 24 | return NextResponse.json({ 25 | error: "Captcha verification failed!", 26 | success: false, 27 | }, { status: 500 }); 28 | } 29 | }; -------------------------------------------------------------------------------- /app/blog/[slug]/page.js: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | import { personalData } from "@/utils/data/personal-data"; 3 | 4 | async function getBlog(slug) { 5 | const res = await fetch(`https://dev.to/api/articles/${personalData.devUsername}/${slug}`) 6 | 7 | if (!res.ok) { 8 | throw new Error('Failed to fetch data') 9 | } 10 | 11 | const data = await res.json(); 12 | return data; 13 | }; 14 | 15 | async function BlogDetails({params}) { 16 | const slug = params.slug; 17 | const blog = await getBlog(slug); 18 | 19 | return ( 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default BlogDetails; -------------------------------------------------------------------------------- /app/blog/page.js: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | 3 | import { personalData } from "@/utils/data/personal-data"; 4 | import BlogCard from "../components/homepage/blog/blog-card"; 5 | 6 | async function getBlogs() { 7 | const res = await fetch(`https://dev.to/api/articles?username=${personalData.devUsername}`) 8 | 9 | if (!res.ok) { 10 | throw new Error('Failed to fetch data') 11 | } 12 | 13 | const data = await res.json(); 14 | return data; 15 | }; 16 | 17 | async function page() { 18 | const blogs = await getBlogs(); 19 | 20 | return ( 21 |
22 |
23 |
24 | 25 | 26 | All Blog 27 | 28 | 29 |
30 |
31 | 32 |
33 | { 34 | blogs.map((blog, i) => ( 35 | blog?.cover_image && 36 | 37 | )) 38 | } 39 |
40 |
41 | ); 42 | }; 43 | 44 | export default page; -------------------------------------------------------------------------------- /app/components/footer.jsx: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | import Link from 'next/link'; 3 | import { CgGitFork } from "react-icons/cg"; 4 | import { IoStar } from "react-icons/io5"; 5 | 6 | function Footer() { 7 | return ( 8 |
9 |
10 |
11 |
12 |
13 |
14 |

15 | © Developer Portfolio by Abu Said 16 |

17 |
18 | 23 | 24 | Star 25 | 26 | 31 | 32 | Fork 33 | 34 |
35 |
36 |
37 |
38 | ); 39 | }; 40 | 41 | export default Footer; -------------------------------------------------------------------------------- /app/components/helper/animation-lottie.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Lottie from "lottie-react"; 4 | 5 | const AnimationLottie = ({ animationPath, width }) => { 6 | const defaultOptions = { 7 | loop: true, 8 | autoplay: true, 9 | animationData: animationPath, 10 | style: { 11 | width: '95%', 12 | } 13 | }; 14 | 15 | return ( 16 | 17 | ); 18 | }; 19 | 20 | export default AnimationLottie; -------------------------------------------------------------------------------- /app/components/helper/glow-card.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { useEffect } from 'react'; 3 | 4 | const GlowCard = ({ children , identifier}) => { 5 | useEffect(() => { 6 | const CONTAINER = document.querySelector(`.glow-container-${identifier}`); 7 | const CARDS = document.querySelectorAll(`.glow-card-${identifier}`); 8 | 9 | const CONFIG = { 10 | proximity: 40, 11 | spread: 80, 12 | blur: 12, 13 | gap: 32, 14 | vertical: false, 15 | opacity: 0, 16 | }; 17 | 18 | const UPDATE = (event) => { 19 | for (const CARD of CARDS) { 20 | const CARD_BOUNDS = CARD.getBoundingClientRect(); 21 | 22 | if ( 23 | event?.x > CARD_BOUNDS.left - CONFIG.proximity && 24 | event?.x < CARD_BOUNDS.left + CARD_BOUNDS.width + CONFIG.proximity && 25 | event?.y > CARD_BOUNDS.top - CONFIG.proximity && 26 | event?.y < CARD_BOUNDS.top + CARD_BOUNDS.height + CONFIG.proximity 27 | ) { 28 | CARD.style.setProperty('--active', 1); 29 | } else { 30 | CARD.style.setProperty('--active', CONFIG.opacity); 31 | } 32 | 33 | const CARD_CENTER = [ 34 | CARD_BOUNDS.left + CARD_BOUNDS.width * 0.5, 35 | CARD_BOUNDS.top + CARD_BOUNDS.height * 0.5, 36 | ]; 37 | 38 | let ANGLE = 39 | (Math.atan2(event?.y - CARD_CENTER[1], event?.x - CARD_CENTER[0]) * 40 | 180) / 41 | Math.PI; 42 | 43 | ANGLE = ANGLE < 0 ? ANGLE + 360 : ANGLE; 44 | 45 | CARD.style.setProperty('--start', ANGLE + 90); 46 | } 47 | }; 48 | 49 | document.body.addEventListener('pointermove', UPDATE); 50 | 51 | const RESTYLE = () => { 52 | CONTAINER.style.setProperty('--gap', CONFIG.gap); 53 | CONTAINER.style.setProperty('--blur', CONFIG.blur); 54 | CONTAINER.style.setProperty('--spread', CONFIG.spread); 55 | CONTAINER.style.setProperty( 56 | '--direction', 57 | CONFIG.vertical ? 'column' : 'row' 58 | ); 59 | }; 60 | 61 | RESTYLE(); 62 | UPDATE(); 63 | 64 | // Cleanup event listener 65 | return () => { 66 | document.body.removeEventListener('pointermove', UPDATE); 67 | }; 68 | }, [identifier]); 69 | 70 | return ( 71 |
72 |
73 |
74 | {children} 75 |
76 |
77 | ); 78 | }; 79 | 80 | export default GlowCard; 81 | -------------------------------------------------------------------------------- /app/components/helper/scroll-to-top.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { FaArrowUp } from "react-icons/fa6"; 5 | 6 | const DEFAULT_BTN_CLS = 7 | "fixed bottom-8 right-6 z-50 flex items-center rounded-full bg-gradient-to-r from-pink-500 to-violet-600 p-4 hover:text-xl transition-all duration-300 ease-out"; 8 | const SCROLL_THRESHOLD = 50; 9 | 10 | const ScrollToTop = () => { 11 | const [btnCls, setBtnCls] = useState(DEFAULT_BTN_CLS); 12 | 13 | useEffect(() => { 14 | const handleScroll = () => { 15 | if (window.scrollY > SCROLL_THRESHOLD) { 16 | setBtnCls(DEFAULT_BTN_CLS.replace(" hidden", "")); 17 | } else { 18 | setBtnCls(DEFAULT_BTN_CLS + " hidden"); 19 | } 20 | }; 21 | window.addEventListener("scroll", handleScroll, { passive: true }); 22 | return () => { 23 | window.removeEventListener("scroll", handleScroll, { passive: true }); 24 | }; 25 | }, []); 26 | 27 | const onClickBtn = () => window.scrollTo({ top: 0, behavior: "smooth" }); 28 | 29 | return ( 30 | 33 | ); 34 | }; 35 | 36 | export default ScrollToTop; 37 | -------------------------------------------------------------------------------- /app/components/homepage/about/index.jsx: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | 3 | import { personalData } from "@/utils/data/personal-data"; 4 | import Image from "next/image"; 5 | 6 | 7 | function AboutSection() { 8 | return ( 9 |
10 |
11 | 12 | ABOUT ME 13 | 14 | 15 |
16 |
17 |
18 |

19 | Who I am? 20 |

21 |

22 | {personalData.description} 23 |

24 |
25 |
26 | Abu Said 33 |
34 |
35 |
36 | ); 37 | }; 38 | 39 | export default AboutSection; -------------------------------------------------------------------------------- /app/components/homepage/blog/blog-card.jsx: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | import { timeConverter } from '@/utils/time-converter'; 3 | import Image from 'next/image'; 4 | import Link from 'next/link'; 5 | import { BsHeartFill } from 'react-icons/bs'; 6 | import { FaCommentAlt } from 'react-icons/fa'; 7 | 8 | function BlogCard({ blog }) { 9 | 10 | return ( 11 |
13 |
14 | 21 |
22 |
23 |
24 |

{timeConverter(blog.published_at)}

25 |
26 |

27 | 28 | {blog.public_reactions_count} 29 |

30 | {blog.comments_count > 0 && 31 |

32 | 33 | {blog.comments_count} 34 |

35 | } 36 |
37 |
38 | 39 |

40 | {blog.title} 41 |

42 | 43 |

44 | {`${blog.reading_time_minutes} Min Read`} 45 |

46 |

47 | {blog.description} 48 |

49 | {/*
50 | 51 | 54 | 55 |
*/} 56 |
57 |
58 | ); 59 | }; 60 | 61 | export default BlogCard; -------------------------------------------------------------------------------- /app/components/homepage/blog/index.jsx: -------------------------------------------------------------------------------- 1 | // @flow strict 2 | import Link from 'next/link'; 3 | import { FaArrowRight } from 'react-icons/fa'; 4 | import BlogCard from './blog-card'; 5 | 6 | function Blog({ blogs }) { 7 | 8 | return ( 9 |
10 |
11 | 12 |
13 |
14 |
15 |
16 |
17 | 18 |
19 |
20 | 21 | 22 | Blogs 23 | 24 | 25 |
26 |
27 | 28 |
29 | { 30 | blogs.slice(0, 6).map((blog, i) => ( 31 | blog?.cover_image && 32 | 33 | )) 34 | } 35 |
36 | 37 |
38 | 43 | View More 44 | 45 | 46 |
47 |
48 | ); 49 | }; 50 | 51 | export default Blog; -------------------------------------------------------------------------------- /app/components/homepage/contact/contact-without-captcha.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | // @flow strict 3 | import { isValidEmail } from '@/utils/check-email'; 4 | import axios from 'axios'; 5 | import { useState } from 'react'; 6 | import { TbMailForward } from "react-icons/tb"; 7 | import { toast } from 'react-toastify'; 8 | 9 | function ContactWithoutCaptcha() { 10 | const [error, setError] = useState({ email: false, required: false }); 11 | const [userInput, setUserInput] = useState({ 12 | name: '', 13 | email: '', 14 | message: '', 15 | }); 16 | 17 | const checkRequired = () => { 18 | if (userInput.email && userInput.message && userInput.name) { 19 | setError({ ...error, required: false }); 20 | } 21 | }; 22 | 23 | const handleSendMail = async (e) => { 24 | e.preventDefault(); 25 | if (!userInput.email || !userInput.message || !userInput.name) { 26 | setError({ ...error, required: true }); 27 | return; 28 | } else if (error.email) { 29 | return; 30 | } else { 31 | setError({ ...error, required: false }); 32 | }; 33 | 34 | const serviceID = process.env.NEXT_PUBLIC_EMAILJS_SERVICE_ID; 35 | const templateID = process.env.NEXT_PUBLIC_EMAILJS_TEMPLATE_ID; 36 | const options = { publicKey: process.env.NEXT_PUBLIC_EMAILJS_PUBLIC_KEY }; 37 | 38 | try { 39 | const res = await emailjs.send(serviceID, templateID, userInput, options); 40 | const teleRes = await axios.post(`${process.env.NEXT_PUBLIC_APP_URL}/api/contact`, userInput); 41 | 42 | if (res.status === 200 || teleRes.status === 200) { 43 | toast.success('Message sent successfully!'); 44 | setUserInput({ 45 | name: '', 46 | email: '', 47 | message: '', 48 | }); 49 | }; 50 | } catch (error) { 51 | toast.error(error?.text || error); 52 | }; 53 | }; 54 | 55 | return ( 56 |
57 |

58 | Contact with me 59 |

60 |
61 |

62 | {"If you have any questions or concerns, please don't hesitate to contact me. I am open to any work opportunities that align with my skills and interests."} 63 |

64 |
65 |
66 | 67 | setUserInput({ ...userInput, name: e.target.value })} 73 | onBlur={checkRequired} 74 | value={userInput.name} 75 | /> 76 |
77 | 78 |
79 | 80 | setUserInput({ ...userInput, email: e.target.value })} 87 | onBlur={() => { 88 | checkRequired(); 89 | setError({ ...error, email: !isValidEmail(userInput.email) }); 90 | }} 91 | /> 92 | {error.email && 93 |

Please provide a valid email!

94 | } 95 |
96 | 97 |
98 | 99 |