#1 dev

Merged
khangtran merged 6 commits from dev into main 5 months ago
  1. +8
    -0
      .dockerignore
  2. +2
    -1
      .gitignore
  3. +29
    -0
      Dockerfile
  4. +61
    -3
      README.md
  5. +29
    -0
      package-lock.json
  6. +2
    -0
      package.json
  7. +162
    -116
      src/App.css
  8. +7
    -3
      src/App.js
  9. BIN
      src/assets/img/136646.jpg
  10. BIN
      src/assets/img/39668.jpg
  11. +4
    -0
      src/assets/img/github.svg
  12. +4
    -0
      src/assets/img/linkedln.svg
  13. +104
    -0
      src/components/ContactForm.js
  14. +54
    -32
      src/components/Navbar.js
  15. +67
    -0
      src/components/Projects.js

+ 8
- 0
.dockerignore View File

@ -0,0 +1,8 @@
node_modules
build
.dockerignore
Dockerfile
.git
.gitignore
README.md
npm-debug.log

+ 2
- 1
.gitignore View File

@ -10,7 +10,8 @@
# production
/build
# sensitive files
/src/components/apikey.js
# misc
.DS_Store
.env.local


+ 29
- 0
Dockerfile View File

@ -0,0 +1,29 @@
# Step 1: Use a base image with Node.js
FROM node:16 AS build
# Step 2: Set the working directory inside the container
WORKDIR /app
# Step 3: Copy package.json and package-lock.json (or yarn.lock)
COPY package*.json ./
# Step 4: Install dependencies
RUN npm install
# Step 5: Copy the rest of your application code
COPY . .
# Step 6: Build the React application
RUN npm run build
# Step 7: Use a base image to serve the application
FROM nginx:alpine
# Step 8: Copy the build output from the previous stage to the Nginx public directory
COPY --from=build /app/build /usr/share/nginx/html
# Step 9: Expose port 80 to the outside
EXPOSE 80
# Step 10: Start the Nginx server
CMD ["nginx", "-g", "daemon off;"]

+ 61
- 3
README.md View File

@ -1,5 +1,63 @@
# My Portfolio
### `npm start`
This is a personal portfolio web application built using React. It showcases various projects and skills and includes a contact form for users to get in touch.
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
## Table of Contents
- [Getting Started](#getting-started)
- [Features](#features)
- [Installation](#installation)
- [Usage](#usage)
- [EmailJS Setup](#emailjs-setup)
- [Contributing](#contributing)
- [License](#license)
## Getting Started
To get a local copy of the project up and running, follow these simple steps.
### Prerequisites
Make sure you have the following installed:
- [Node.js](https://nodejs.org/) (v14 or later)
- [npm](https://www.npmjs.com/) (comes with Node.js)
- [EmailJS](https://www.emailjs.com)
Create an account on EmailJS, then create a new email service and template. Copy the Service ID, Template ID, and User ID.
### EmailJS Configuration
1. **Create a configuration file:**
Create a file named `emailConfig.js` in the `src/components` directory with the following content:
```javascript
// src/components/emailConfig.js
const emailConfig = {
serviceId: 'yourserviceid', // Replace with your EmailJS Service ID
templateId: 'yourtemplateid', // Replace with your EmailJS Template ID
userId: 'youruserid' // Replace with your EmailJS User ID
};
export default emailConfig;
you then import it to the ContactForm.js
### Installation
1. **Install the dependencies:**
bash
npm install
## Usage
### Running the App
To start the app in development mode:
bash
npm start

+ 29
- 0
package-lock.json View File

@ -12,10 +12,12 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"bootstrap": "^5.3.3",
"emailjs-com": "^3.2.0",
"react": "^18.3.1",
"react-bootstrap": "^2.10.4",
"react-bootstrap-icons": "^1.11.4",
"react-dom": "^18.3.1",
"react-multi-carousel": "^2.8.5",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
}
@ -7298,6 +7300,15 @@
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.822.tgz",
"integrity": "sha512-qJzHIt4dRRFKjHHvaExCrG95F65kUP3xysaEZ4I2+/R/uIyr5Ar5g/rkAnrRz0parRUYwzpqN8Pz1HgoiYQPpg=="
},
"node_modules/emailjs-com": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/emailjs-com/-/emailjs-com-3.2.0.tgz",
"integrity": "sha512-Prbz3E1usiAwGjMNYRv6EsJ5c373cX7/AGnZQwOfrpNJrygQJ15+E9OOq4pU8yC977Z5xMetRfc3WmDX6RcjAA==",
"deprecated": "The SDK name changed to @emailjs/browser",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/emittery": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz",
@ -15176,6 +15187,14 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"node_modules/react-multi-carousel": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/react-multi-carousel/-/react-multi-carousel-2.8.5.tgz",
"integrity": "sha512-C5DAvJkfzR2JK9YixZ3oyF9x6R4LW6nzTpIXrl9Oujxi4uqP9SzVVCjl+JLM3tSdqdjAx/oWZK3dTVBSR73Q+w==",
"engines": {
"node": ">=8"
}
},
"node_modules/react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@ -23607,6 +23626,11 @@
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.822.tgz",
"integrity": "sha512-qJzHIt4dRRFKjHHvaExCrG95F65kUP3xysaEZ4I2+/R/uIyr5Ar5g/rkAnrRz0parRUYwzpqN8Pz1HgoiYQPpg=="
},
"emailjs-com": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/emailjs-com/-/emailjs-com-3.2.0.tgz",
"integrity": "sha512-Prbz3E1usiAwGjMNYRv6EsJ5c373cX7/AGnZQwOfrpNJrygQJ15+E9OOq4pU8yC977Z5xMetRfc3WmDX6RcjAA=="
},
"emittery": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz",
@ -29066,6 +29090,11 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-multi-carousel": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/react-multi-carousel/-/react-multi-carousel-2.8.5.tgz",
"integrity": "sha512-C5DAvJkfzR2JK9YixZ3oyF9x6R4LW6nzTpIXrl9Oujxi4uqP9SzVVCjl+JLM3tSdqdjAx/oWZK3dTVBSR73Q+w=="
},
"react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",


+ 2
- 0
package.json View File

@ -7,10 +7,12 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"bootstrap": "^5.3.3",
"emailjs-com": "^3.2.0",
"react": "^18.3.1",
"react-bootstrap": "^2.10.4",
"react-bootstrap-icons": "^1.11.4",
"react-dom": "^18.3.1",
"react-multi-carousel": "^2.8.5",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},


+ 162
- 116
src/App.css View File

@ -1,4 +1,4 @@
/* custom fonts */
/* Custom fonts */
@font-face {
font-family: centra;
src: url('./assets/font/CentraNo2-Bold.ttf');
@ -15,32 +15,34 @@
font-weight: 400;
}
/* default CSS */
/* Default CSS */
#root {
min-height: 100vh;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
}
html {
html {
scroll-behavior: smooth;
scroll-padding-top: 75px;
}
}
body {
body {
font-weight: 400;
overflow: hidden;
position: relative;
color: #685454 !important;
font-family: 'Centra', sans-serif !important;
}
h1, h2, h3, h4, h5, h6 {
margin: 0;
padding: 0;
line-height: normal;
}
}
h1, h2, h3, h4, h5, h6 {
margin: 0;
padding: 0;
line-height: normal;
}
p, a, li, button, ul {
margin: 0;
@ -67,35 +69,36 @@ input:focus, textarea:focus, select:focus {
outline: none;
}
@media (min-width:1700px) {
main .container {
max-width: 100%;
padding: 0 150px;
}
@media (min-width: 1700px) {
main .container {
max-width: 100%;
padding: 0 150px;
}
}
p.success {
color: green;
color: green;
}
p.danger {
color: red;
color: red;
}
/************ Navbar Css ************/
/* Navbar CSS */
nav.navbar {
padding: 18px 0;
position: fixed;
width: 100%;
top: 0;
z-index: 9999;
transition: 0.32s ease-in-out;
transition: 0.32s ease-in-out;
}
nav.navbar.scrolled {
padding: 0px 0;
background-color: #121212;
}
nav.navbar a.navbar-brand {
width: 9%;
width: 9%;
}
nav.navbar .navbar-nav .nav-link.navbar-link {
font-weight: 400;
@ -107,48 +110,48 @@ nav.navbar .navbar-nav .nav-link.navbar-link {
}
nav.navbar .navbar-nav a.nav-link.navbar-link:hover,
nav.navbar .navbar-nav a.nav-link.navbar-link.active {
opacity: 1;
opacity: 1;
}
span.navbar-text {
display: flex;
align-items: center;
display: flex;
align-items: center;
}
.social-icon {
display: inline-block;
margin-left: 14px;
display: inline-block;
margin-left: 14px;
}
.social-icon a {
width: 42px;
height: 42px;
background: rgba(217, 217, 217, 0.1);
display: inline-flex;
border-radius: 50%;
margin-right: 6px;
align-items: center;
justify-content: center;
line-height: 1;
border: 1px solid rgba(255, 255, 255, 0.5);
width: 42px;
height: 42px;
background: rgba(217, 217, 217, 0.1);
display: inline-flex;
border-radius: 50%;
margin-right: 6px;
align-items: center;
justify-content: center;
line-height: 1;
border: 1px solid rgba(255, 255, 255, 0.5);
}
.social-icon a::before {
content: "";
width: 42px;
height: 42px;
position: absolute;
background-color: #ffffff;
border-radius: 50%;
transform: scale(0);
transition: 0.3s ease-in-out;
content: "";
width: 42px;
height: 42px;
position: absolute;
background-color: #ffffff;
border-radius: 50%;
transform: scale(0);
transition: 0.3s ease-in-out;
}
.social-icon a:hover::before {
transform: scale(1);
transform: scale(1);
}
.social-icon a img {
width: 40%;
z-index: 1;
transition: 0.3s ease-in-out;
width: 40%;
z-index: 1;
transition: 0.3s ease-in-out;
}
.social-icon a:hover img {
filter: brightness(0) saturate(100%) invert(0%) sepia(7%) saturate(98%) hue-rotate(346deg) brightness(95%) contrast(86%);
filter: brightness(0) saturate(100%) invert(0%) sepia(7%) saturate(98%) hue-rotate(346deg) brightness(95%) contrast(86%);
}
.navbar-text button {
font-weight: 700;
@ -157,9 +160,9 @@ span.navbar-text {
padding: 18px 34px;
font-size: 18px;
margin-left: 18px;
position: relative;
background-color: transparent;
transition: 0.3s ease-in-out;
position: relative;
background-color: transparent;
transition: 0.3s ease-in-out;
}
.navbar-text button span {
z-index: 1;
@ -179,15 +182,15 @@ span.navbar-text {
color: #121212;
}
.navbar-text button:hover::before {
content: "";
width: 100%;
height: 100%;
position: absolute;
content: "";
width: 100%;
height: 100%;
position: absolute;
}
nav.navbar .navbar-toggler:active,
nav.navbar .navbar-toggler:focus {
outline: none;
box-shadow: none;
outline: none;
box-shadow: none;
}
nav.navbar .navbar-toggler-icon {
width: 24px;
@ -199,43 +202,42 @@ nav.navbar .navbar-toggler-icon {
top: -2px;
}
nav.navbar .navbar-toggler-icon:focus {
border-bottom: 2px solid #fff;
border-bottom: 2px solid #fff;
}
nav.navbar .navbar-toggler-icon:after,
nav.navbar .navbar-toggler-icon:before {
width: 24px;
position: absolute;
height: 2px;
background-color: #fff;
top: 0;
left: 0;
content: '';
z-index: 2;
transition: all 300ms linear;
width: 24px;
position: absolute;
height: 2px;
background-color: #fff;
top: 0;
left: 0;
content: '';
z-index: 2;
transition: all 300ms linear;
}
nav.navbar .navbar-toggler-icon:after {
top: 8px;
top: 8px;
}
nav.navbar .navbar-toggler[aria-expanded="true"] .navbar-toggler-icon:after {
transform: rotate(45deg);
background-color: #fff;
height: 2px;
transform: rotate(45deg);
background-color: #fff;
height: 2px;
}
nav.navbar .navbar-toggler[aria-expanded="true"] .navbar-toggler-icon:before {
transform: translateY(8px) rotate(-45deg);
background-color: #fff;
height: 2px;
transform: translateY(8px) rotate(-45deg);
background-color: #fff;
height: 2px;
}
nav.navbar .navbar-toggler[aria-expanded="true"] .navbar-toggler-icon {
border-color: transparent;
border-color: transparent;
}
/* banner */
/* Banner */
.banner {
margin-top: 0;
margin-bottom: 0;
padding: 150px 0 100px 0;
position: relative;
background-image: url('./assets/img/01.jpg');
background-image: url('./assets/img/136646.jpg');
background-position: top center;
background-size: cover;
background-repeat: no-repeat;
@ -288,57 +290,101 @@ nav.navbar .navbar-toggler[aria-expanded="true"] .navbar-toggler-icon {
animation: updown 3s linear infinite;
}
@keyframes updown {
0% {
transform: translateY(-20px);
}
50% {
transform: translateY(20px);
}
100% {
transform: translateY(-20px);
}
0% {
transform: translateY(-20px);
}
50% {
transform: translateY(20px);
}
100% {
transform: translateY(-20px);
}
}
.txt-rotate > .wrap {
border-right: 0.08em solid #666;
}
/* General styling for projects section */
.projects-section {
padding: 0 0 50px 0;
position: relative;
}
/* Styling for the carousel container */
.skill-slider {
width: 80%;
margin: 0 auto;
position: relative;
}
.App {
text-align: center;
/* Styling for the cards within the carousel */
.card-custom {
background-color: #463c3c; /* Light grey background for cards */
border-radius: 20px; /* Rounded corners */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Shadow effect */
overflow: hidden; /* Ensure content doesn't overflow */
position: relative; /* Allow for overlapping */
transition: transform 250ms, box-shadow 250ms; /* Smooth transitions */
}
.App-logo {
height: 40vmin;
pointer-events: none;
/* Image styling */
.card-custom img {
width: 100%;
border-bottom: 1px solid #ddd; /* Divider line between image and text */
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
/* Styling for card content */
.card-custom-body {
padding: 1rem;
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
/* Hover effects for the card */
.card-custom:hover {
transform: translateY(-10px) rotate(3deg); /* Move up and rotate */
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); /* Enhance shadow on hover */
}
/* Styling to ensure cards overlap */
.card-custom:nth-child(n+2) {
margin-left: -40px; /* Overlap effect */
}
/* Adjust carousel arrows */
.react-multi-carousel-arrow {
background-color: #333; /* Dark background for arrows */
border-radius: 50%; /* Round arrows */
height: 30px;
width: 30px;
z-index: 10;
}
.react-multi-carousel-arrow::before {
font-size: 20px;
color: #fff; /* White arrow color */
}
/* contactform */
.modal-header {
background-color: #121212;
color: white;
}
.App-link {
color: #61dafb;
.modal-body {
background-color: #f8f9fa;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
.modal-footer {
background-color: #121212;
}
.btn-primary {
background-color: #007bff;
border-color: #007bff;
}
.btn-primary:hover {
background-color: #0056b3;
border-color: #0056b3;
}

+ 7
- 3
src/App.js View File

@ -1,13 +1,17 @@
import './App.css';
import NavBar from './components/Navbar';
import Banner from './components/banner';
import Projects from './components/Projects';
import 'bootstrap/dist/css/bootstrap.min.css';
function App() {
return (
<div className ="App">
<NavBar />
<Banner />
<div className="App">
<NavBar />
<div className="content-wrapper">
<Banner />
<Projects />
</div>
</div>
);
}


BIN
src/assets/img/136646.jpg View File

Before After
Width: 2560  |  Height: 1600  |  Size: 366 KiB

BIN
src/assets/img/39668.jpg View File

Before After
Width: 3840  |  Height: 2160  |  Size: 631 KiB

+ 4
- 0
src/assets/img/github.svg View File

@ -0,0 +1,4 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<title>GitHub</title>
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.302 3.438 9.8 8.207 11.387.6.11.82-.26.82-.577v-2.022c-3.338.725-4.033-1.41-4.033-1.41-.546-1.385-1.333-1.756-1.333-1.756-1.09-.745.083-.73.083-.73 1.204.084 1.837 1.236 1.837 1.236 1.07 1.835 2.807 1.305 3.492.997.108-.774.42-1.305.763-1.605-2.665-.304-5.466-1.333-5.466-5.93 0-1.31.47-2.38 1.236-3.22-.124-.303-.536-1.52.117-3.168 0 0 1.008-.322 3.3 1.23.957-.266 1.98-.398 3-.403 1.02.005 2.043.137 3 .403 2.29-1.552 3.297-1.23 3.297-1.23.653 1.648.242 2.865.118 3.168.77.84 1.235 1.91 1.235 3.22 0 4.61-2.803 5.624-5.475 5.92.43.372.814 1.102.814 2.222v3.293c0 .32.218.694.825.576C20.565 21.795 24 17.298 24 12c0-6.63-5.373-12-12-12z"/>
</svg>

+ 4
- 0
src/assets/img/linkedln.svg View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img">
<title>LinkedIn</title>
<path d="M22.23 0H1.77C.79 0 0 .8 0 1.78v20.45C0 23.2.79 24 1.77 24h20.45c.98 0 1.78-.8 1.78-1.77V1.78C24 .8 23.21 0 22.23 0zM7.12 20.45H3.56V9h3.56v11.45zM5.34 7.48c-1.14 0-2.06-.93-2.06-2.08 0-1.15.92-2.08 2.06-2.08 1.14 0 2.07.93 2.07 2.08 0 1.15-.93 2.08-2.07 2.08zM20.45 20.45h-3.56v-5.58c0-1.33-.03-3.04-1.85-3.04-1.85 0-2.13 1.45-2.13 2.94v5.68h-3.56V9h3.42v1.56h.05c.48-.9 1.66-1.85 3.42-1.85 3.65 0 4.32 2.4 4.32 5.51v6.23z"/>
</svg>

+ 104
- 0
src/components/ContactForm.js View File

@ -0,0 +1,104 @@
// src/components/ContactForm.js
import React, { useState } from 'react';
import { Modal, Button, Form } from 'react-bootstrap';
import emailjs from 'emailjs-com';
import emailConfig from './apikey';
const ContactForm = ({ show, handleClose }) => {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
message: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value
});
};
const handleSubmit = (e) => {
e.preventDefault();
emailjs.send(
emailConfig.serviceId, // EmailJS service ID
emailConfig.templateId, // EmailJS template ID
formData,
emailConfig.userId // EmailJS user ID
)
.then((result) => {
alert('Message sent successfully!');
handleClose();
}, (error) => {
alert('Failed to send message, please try again.');
});
};
return (
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>Contact Me</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form onSubmit={handleSubmit}>
<Form.Group controlId="formFirstName">
<Form.Label>First Name</Form.Label>
<Form.Control
type="text"
placeholder="Enter your first name"
name="firstName"
value={formData.firstName}
onChange={handleChange}
required
/>
</Form.Group>
<Form.Group controlId="formLastName">
<Form.Label>Last Name</Form.Label>
<Form.Control
type="text"
placeholder="Enter your last name"
name="lastName"
value={formData.lastName}
onChange={handleChange}
required
/>
</Form.Group>
<Form.Group controlId="formEmail">
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
placeholder="Enter your email"
name="email"
value={formData.email}
onChange={handleChange}
required
/>
</Form.Group>
<Form.Group controlId="formMessage">
<Form.Label>Message</Form.Label>
<Form.Control
as="textarea"
rows={3}
placeholder="Enter your message"
name="message"
value={formData.message}
onChange={handleChange}
required
/>
</Form.Group>
<Button variant="primary" type="submit">
Send Message
</Button>
</Form>
</Modal.Body>
</Modal>
);
};
export default ContactForm;

+ 54
- 32
src/components/Navbar.js View File

@ -1,17 +1,19 @@
import Container from 'react-bootstrap/Container';
import Nav from 'react-bootstrap/Nav';
import Navbar from 'react-bootstrap/Navbar';
import { useState, useEffect} from "react";
// src/components/NavBar.js
import React, { useState, useEffect } from 'react';
import { Container, Nav, Navbar } from 'react-bootstrap';
import logo from '../assets/img/logo.svg';
import navIcon1 from '../assets/img/nav-icon1.svg';
import navIcon3 from '../assets/img/nav-icon2.svg';
import navIcon2 from '../assets/img/nav-icon3.svg';
import navIcon from '../assets/img/github.svg';
import linkedin from '../assets/img/linkedln.svg';
import ContactForm from './ContactForm';
const NavBar = () => {
const [activeLink, setActiveLink] = useState('home');
const [scrolled, setScrolled] = useState(false);
//check for scrolling
const [showContactForm, setShowContactForm] = useState(false);
const handleShowContactForm = () => setShowContactForm(true);
const handleCloseContactForm = () => setShowContactForm(false);
useEffect(() => {
const onScroll = () => {
if (window.scrollY > 50) {
@ -19,43 +21,63 @@ const NavBar = () => {
} else {
setScrolled(false);
}
}
};
window.addEventListener("scroll", onScroll);
// remove event listener when components unmounts
return () => window.removeEventListener("scroll", onScroll);
}, [])
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
const onUpdateLink = (value) => {
setActiveLink(value)
}
setActiveLink(value);
};
return (
<Navbar expand="lg" className= {scrolled ? "scrolled": ""}>
<Navbar expand="lg" className={scrolled ? 'scrolled' : ''}>
<Container>
<Navbar.Brand href="#home">
<img src={logo} alt="logo" />
<img src={logo} alt="Logo" />
</Navbar.Brand>
<Navbar.Toggle/>
<Navbar.Toggle />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="me-auto">
<Nav.Link href="#home" className={activeLink === 'home' ? 'active navbar-link': 'navbar-link'} onClick={() => onUpdateLink('home')}>Home</Nav.Link>
<Nav.Link href="#skills" className={activeLink === 'home' ? 'active navbar-link': 'navbar-link'} onClick={() => onUpdateLink('skills')}>skills</Nav.Link>
<Nav.Link href="#projects" className={activeLink === 'home' ? 'active navbar-link': 'navbar-link'} onClick={() => onUpdateLink('projects')}>Projects</Nav.Link>
<Nav.Link
href="#home"
className={
activeLink === 'home' ? 'active navbar-link' : 'navbar-link'
}
onClick={() => onUpdateLink('home')}
>
Home
</Nav.Link>
<Nav.Link
href="#projects"
className={
activeLink === 'projects' ? 'active navbar-link' : 'navbar-link'
}
onClick={() => onUpdateLink('projects')}
>
Projects
</Nav.Link>
</Nav>
<span className="navbar-text">
<div className="social-icon">
<a href="#"><img src={navIcon1} alt="nav-icon1" /></a>
<a href="#"><img src={navIcon2} alt="nav-icon2" /></a>
<a href="#"><img src={navIcon3} alt="nav-icon3" /></a>
</div>
<button className="vvd" onClick={() => console.log('connect')}>
<span>Let's connect!</span>
</button>
<div className="social-icon">
<a href="https://github.com/khang2005">
<img src={navIcon} alt="github Icon" />
</a>
<a href="https://www.linkedin.com/in/khang-tran-4ba6b3285/">
<img src={linkedin} alt="linkedin Icon" />
</a>
</div>
<button className="vvd" onClick={handleShowContactForm}>
<span>Let's connect!</span>
</button>
</span>
</Navbar.Collapse>
</Container>
<ContactForm show={showContactForm} handleClose={handleCloseContactForm} />
</Navbar>
);
}
};
export default NavBar;
export default NavBar;

+ 67
- 0
src/components/Projects.js View File

@ -0,0 +1,67 @@
import React from 'react';
import Carousel from 'react-multi-carousel';
import 'react-multi-carousel/lib/styles.css';
const projects = [
{
title: 'Project One',
description: 'This is a brief description of project one.',
imageUrl: 'https://via.placeholder.com/150',
link: 'https://github.com/yourusername/project-one'
},
{
title: 'Project Two',
description: 'This is a brief description of project two.',
imageUrl: 'https://via.placeholder.com/150',
link: 'https://github.com/yourusername/project-two'
},
{
title: 'Project Three',
description: 'This is a brief description of project three.',
imageUrl: 'https://via.placeholder.com/150',
link: 'https://github.com/yourusername/project-three'
}
];
const Projects = () => {
const handleCardClick = (link) => {
window.open(link, '_blank');
};
const responsive = {
superLargeDesktop: {
breakpoint: { max: 4000, min: 3000 },
items: 5
},
desktop: {
breakpoint: { max: 3000, min: 1024 },
items: 3
},
tablet: {
breakpoint: { max: 1024, min: 464 },
items: 2
},
mobile: {
breakpoint: { max: 464, min: 0 },
items: 1
}
};
return (
<section className="projects-section" id="projects">
<Carousel responsive={responsive} infinite={true} className="skill-slider">
{projects.map((project, index) => (
<div key={index} className="card-custom" onClick={() => handleCardClick(project.link)}>
<img src={project.imageUrl} alt={project.title} />
<div className="card-custom-body">
<h5>{project.title}</h5>
<p>{project.description}</p>
</div>
</div>
))}
</Carousel>
</section>
);
};
export default Projects;

Loading…
Cancel
Save