feat: add hero section and navigation components
Deploy Portfolio Selfmade / deploy (push) Successful in 31s
Deploy Portfolio Selfmade / deploy (push) Successful in 31s
- Created HeroSection component for the landing page. - Implemented Navigation component with smooth scrolling to sections. - Added useNavigation hook to manage active section and scroll state. - Introduced useScrollToSection hook for smooth scrolling functionality. feat: add projects section with project cards - Developed ProjectCard component to display individual project details. - Created ProjectsSection component to showcase featured projects with filtering options. - Added sample project data for demonstration. feat: add skills section with skill cards - Implemented SkillCard component to display individual skills. - Created SkillsSection component to showcase skills with category filtering. - Added sample skills data for demonstration. style: update global styles and add custom scrollbar - Imported Urbanist font and set it as the default font. - Updated body background and text colors. - Customized scrollbar styles for better aesthetics. feat: add shared components and constants - Created AnimatedSection, Button, and Footer components for reusability. - Added COLORS and GRADIENT constants for consistent theming. - Updated index files for shared components and hooks. chore: update dependencies and configuration - Added framer-motion for animations. - Updated Tailwind CSS configuration for custom animations and colors. - Adjusted TypeScript configuration for better path resolution.
This commit is contained in:
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2021 The Urbanist Project Authors (https://github.com/coreyhu/Urbanist)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://openfontlicense.org
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
Urbanist Variable Font
|
||||||
|
======================
|
||||||
|
|
||||||
|
This download contains Urbanist as both variable fonts and static fonts.
|
||||||
|
|
||||||
|
Urbanist is a variable font with this axis:
|
||||||
|
wght
|
||||||
|
|
||||||
|
This means all the styles are contained in these files:
|
||||||
|
Urbanist-VariableFont_wght.ttf
|
||||||
|
Urbanist-Italic-VariableFont_wght.ttf
|
||||||
|
|
||||||
|
If your app fully supports variable fonts, you can now pick intermediate styles
|
||||||
|
that aren’t available as static fonts. Not all apps support variable fonts, and
|
||||||
|
in those cases you can use the static font files for Urbanist:
|
||||||
|
static/Urbanist-Thin.ttf
|
||||||
|
static/Urbanist-ExtraLight.ttf
|
||||||
|
static/Urbanist-Light.ttf
|
||||||
|
static/Urbanist-Regular.ttf
|
||||||
|
static/Urbanist-Medium.ttf
|
||||||
|
static/Urbanist-SemiBold.ttf
|
||||||
|
static/Urbanist-Bold.ttf
|
||||||
|
static/Urbanist-ExtraBold.ttf
|
||||||
|
static/Urbanist-Black.ttf
|
||||||
|
static/Urbanist-ThinItalic.ttf
|
||||||
|
static/Urbanist-ExtraLightItalic.ttf
|
||||||
|
static/Urbanist-LightItalic.ttf
|
||||||
|
static/Urbanist-Italic.ttf
|
||||||
|
static/Urbanist-MediumItalic.ttf
|
||||||
|
static/Urbanist-SemiBoldItalic.ttf
|
||||||
|
static/Urbanist-BoldItalic.ttf
|
||||||
|
static/Urbanist-ExtraBoldItalic.ttf
|
||||||
|
static/Urbanist-BlackItalic.ttf
|
||||||
|
|
||||||
|
Get started
|
||||||
|
-----------
|
||||||
|
|
||||||
|
1. Install the font files you want to use
|
||||||
|
|
||||||
|
2. Use your app's font picker to view the font family and all the
|
||||||
|
available styles
|
||||||
|
|
||||||
|
Learn more about variable fonts
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
|
||||||
|
https://variablefonts.typenetwork.com
|
||||||
|
https://medium.com/variable-fonts
|
||||||
|
|
||||||
|
In desktop apps
|
||||||
|
|
||||||
|
https://theblog.adobe.com/can-variable-fonts-illustrator-cc
|
||||||
|
https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
|
||||||
|
|
||||||
|
Online
|
||||||
|
|
||||||
|
https://developers.google.com/fonts/docs/getting_started
|
||||||
|
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
|
||||||
|
https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
|
||||||
|
|
||||||
|
Installing fonts
|
||||||
|
|
||||||
|
MacOS: https://support.apple.com/en-us/HT201749
|
||||||
|
Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
|
||||||
|
Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
|
||||||
|
|
||||||
|
Android Apps
|
||||||
|
|
||||||
|
https://developers.google.com/fonts/docs/android
|
||||||
|
https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
Please read the full license text (OFL.txt) to understand the permissions,
|
||||||
|
restrictions and requirements for usage, redistribution, and modification.
|
||||||
|
|
||||||
|
You can use them in your products & projects – print or digital,
|
||||||
|
commercial or otherwise.
|
||||||
|
|
||||||
|
This isn't legal advice, please consider consulting a lawyer and see the full
|
||||||
|
license for all details.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+21
-8
@@ -1,14 +1,27 @@
|
|||||||
|
import { Navigation } from './features/navigation';
|
||||||
|
import { HeroSection } from './features/hero';
|
||||||
|
import { SkillsSection } from './features/skills';
|
||||||
|
import { ProjectsSection } from './features/projects';
|
||||||
|
import { ExperienceSection } from './features/experience';
|
||||||
|
import { ContactSection } from './features/contact';
|
||||||
|
import Footer from './shared/components/Footer';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-black text-white flex items-center justify-center">
|
<div className="bg-black text-white min-h-screen overflow-x-hidden">
|
||||||
<div className="text-center">
|
{/* Background gradient decorations */}
|
||||||
<h1 className="text-7xl font-bold mb-4 bg-gradient-to-r from-emerald-400 to-cyan-400 text-transparent bg-clip-text">
|
<div className="fixed inset-0 -z-10">
|
||||||
Mein Portfolio
|
<div className="absolute top-0 left-0 w-96 h-96 bg-green-500/10 rounded-full blur-3xl opacity-50"></div>
|
||||||
</h1>
|
<div className="absolute bottom-0 right-0 w-96 h-96 bg-emerald-500/10 rounded-full blur-3xl opacity-50"></div>
|
||||||
<p className="text-xl text-gray-400">
|
|
||||||
Full-Stack Entwickler & DevOps Enthusiast
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Navigation />
|
||||||
|
<HeroSection />
|
||||||
|
<SkillsSection />
|
||||||
|
<ProjectsSection />
|
||||||
|
<ExperienceSection />
|
||||||
|
<ContactSection />
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { Mail, MapPin } from 'lucide-react';
|
||||||
|
import { FaGithub, FaLinkedin, FaTwitter } from 'react-icons/fa';
|
||||||
|
import Button from '../../../shared/components/Button';
|
||||||
|
import AnimatedSection from '../../../shared/components/AnimatedSection';
|
||||||
|
|
||||||
|
export const ContactSection: React.FC = () => {
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.2,
|
||||||
|
delayChildren: 0.3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: { duration: 0.6 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatedSection id="contact" className="py-20 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="inline-block px-4 py-2 rounded-full border border-green-500/50 bg-green-500/10 mb-6"
|
||||||
|
>
|
||||||
|
<span className="text-green-400 text-sm font-medium">GET IN TOUCH</span>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<h2 className="text-4xl sm:text-5xl font-bold text-white mb-4">
|
||||||
|
Let's Work Together
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
|
||||||
|
Have a project in mind? Let's discuss how we can bring your ideas to life
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Grid */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||||
|
{/* Contact Form */}
|
||||||
|
<motion.div
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
whileInView="visible"
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
<motion.div variants={itemVariants}>
|
||||||
|
<label className="block text-white font-medium mb-3">Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Your name"
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-900 border border-green-500/30 text-white placeholder-gray-500 focus:border-green-500 focus:outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div variants={itemVariants}>
|
||||||
|
<label className="block text-white font-medium mb-3">Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="your.email@example.com"
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-900 border border-green-500/30 text-white placeholder-gray-500 focus:border-green-500 focus:outline-none transition-colors"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div variants={itemVariants}>
|
||||||
|
<label className="block text-white font-medium mb-3">Message</label>
|
||||||
|
<textarea
|
||||||
|
placeholder="Tell me about your project"
|
||||||
|
rows={5}
|
||||||
|
className="w-full px-4 py-3 rounded-lg bg-gray-900 border border-green-500/30 text-white placeholder-gray-500 focus:border-green-500 focus:outline-none transition-colors resize-none"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div variants={itemVariants}>
|
||||||
|
<Button variant="primary" size="lg" className="w-full">
|
||||||
|
Send Message
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Contact Info */}
|
||||||
|
<motion.div
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
whileInView="visible"
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
<motion.div variants={itemVariants}>
|
||||||
|
<h3 className="text-2xl font-bold text-white mb-4">Let's Connect</h3>
|
||||||
|
<p className="text-gray-400">
|
||||||
|
I'm always open to discussing new projects, creative ideas, or opportunities
|
||||||
|
to be part of your vision. Feel free to reach out!
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Contact Items */}
|
||||||
|
<motion.div variants={itemVariants}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-4 p-4 rounded-lg border border-green-500/30 bg-green-500/5">
|
||||||
|
<div className="p-3 rounded-lg bg-green-500/20">
|
||||||
|
<Mail className="text-green-500" size={24} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-400 text-sm">Email</p>
|
||||||
|
<p className="text-white font-medium">alex@himetoprogram.com</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 p-4 rounded-lg border border-green-500/30 bg-green-500/5">
|
||||||
|
<div className="p-3 rounded-lg bg-green-500/20">
|
||||||
|
<MapPin className="text-green-500" size={24} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-400 text-sm">Location</p>
|
||||||
|
<p className="text-white font-medium">San Francisco, CA</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Social Links */}
|
||||||
|
<motion.div variants={itemVariants}>
|
||||||
|
<p className="text-white font-medium mb-4">Connect with me</p>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<motion.a
|
||||||
|
href="#"
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="p-3 rounded-lg border border-green-500/30 bg-green-500/5 hover:bg-green-500/20 text-green-500 transition-colors"
|
||||||
|
>
|
||||||
|
<FaGithub size={24} />
|
||||||
|
</motion.a>
|
||||||
|
<motion.a
|
||||||
|
href="#"
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="p-3 rounded-lg border border-green-500/30 bg-green-500/5 hover:bg-green-500/20 text-green-500 transition-colors"
|
||||||
|
>
|
||||||
|
<FaLinkedin size={24} />
|
||||||
|
</motion.a>
|
||||||
|
<motion.a
|
||||||
|
href="#"
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="p-3 rounded-lg border border-green-500/30 bg-green-500/5 hover:bg-green-500/20 text-green-500 transition-colors"
|
||||||
|
>
|
||||||
|
<FaTwitter size={24} />
|
||||||
|
</motion.a>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnimatedSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ContactSection } from './components/ContactSection';
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { experienceData } from '../data/experienceData';
|
||||||
|
import { TimelineItem } from './TimelineItem';
|
||||||
|
import AnimatedSection from '../../../shared/components/AnimatedSection';
|
||||||
|
|
||||||
|
export const ExperienceSection: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<AnimatedSection id="experience" className="py-20 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="inline-block px-4 py-2 rounded-full border border-green-500/50 bg-green-500/10 mb-6"
|
||||||
|
>
|
||||||
|
<span className="text-green-400 text-sm font-medium">My Journey</span>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<h2 className="text-4xl sm:text-5xl font-bold text-white mb-4">
|
||||||
|
Professional Experience
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-400 text-lg">
|
||||||
|
A timeline of my work experience and achievements
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timeline */}
|
||||||
|
<div className="relative">
|
||||||
|
{experienceData.map((experience, index) => (
|
||||||
|
<TimelineItem
|
||||||
|
key={experience.id}
|
||||||
|
experience={experience}
|
||||||
|
index={index}
|
||||||
|
isLast={index === experienceData.length - 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnimatedSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import type { Experience } from '../../../shared/types';
|
||||||
|
|
||||||
|
interface TimelineItemProps {
|
||||||
|
experience: Experience;
|
||||||
|
index: number;
|
||||||
|
isLast: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TimelineItem: React.FC<TimelineItemProps> = ({
|
||||||
|
experience,
|
||||||
|
index,
|
||||||
|
isLast,
|
||||||
|
}) => {
|
||||||
|
const isEven = index % 2 === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
{/* Timeline dot and line */}
|
||||||
|
<div className="absolute left-1/2 transform -translate-x-1/2">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
whileInView={{ scale: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.4, delay: index * 0.1 + 0.2 }}
|
||||||
|
className="w-4 h-4 bg-green-500 rounded-full border-4 border-black"
|
||||||
|
/>
|
||||||
|
{!isLast && (
|
||||||
|
<div className="w-1 h-32 bg-gradient-to-b from-green-500 to-transparent mx-auto -mt-4"></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div
|
||||||
|
className={`w-full md:w-5/12 ${isEven ? 'md:ml-0 md:text-right' : 'md:ml-auto'}`}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
className="p-6 rounded-xl border border-green-500/30 bg-gradient-to-br from-green-500/5 to-emerald-500/5 backdrop-blur-sm hover:border-green-500/60 transition-all duration-300"
|
||||||
|
>
|
||||||
|
{/* Year Badge */}
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="px-3 py-1 rounded-full bg-green-500/20 border border-green-500/50">
|
||||||
|
<span className="text-green-400 font-bold text-sm">
|
||||||
|
{experience.startYear}
|
||||||
|
{experience.endYear ? `-${experience.endYear}` : '-Present'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Position */}
|
||||||
|
<h3 className="text-xl font-bold text-white mb-2">
|
||||||
|
{experience.position}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Company */}
|
||||||
|
{experience.company && (
|
||||||
|
<p className="text-green-400 font-medium mb-3">{experience.company}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-gray-400 text-sm mb-4">{experience.description}</p>
|
||||||
|
|
||||||
|
{/* Technologies */}
|
||||||
|
{experience.technologies && (
|
||||||
|
<div className="flex flex-wrap gap-2 justify-end">
|
||||||
|
{experience.technologies.map((tech) => (
|
||||||
|
<span
|
||||||
|
key={tech}
|
||||||
|
className="text-xs px-2 py-1 rounded border border-green-500/30 text-green-400 bg-green-500/10"
|
||||||
|
>
|
||||||
|
{tech}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import type { Experience } from '../../../shared/types';
|
||||||
|
|
||||||
|
export const experienceData: Experience[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
position: 'Senior React Developer',
|
||||||
|
company: 'Tech Startup Inc.',
|
||||||
|
startYear: 2023,
|
||||||
|
description:
|
||||||
|
'Led the development of multiple high-performance React applications, mentored junior developers, and implemented modern design systems',
|
||||||
|
technologies: ['React', 'TypeScript', 'Next.js', 'GraphQL'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
position: 'Full Stack Developer',
|
||||||
|
company: 'Digital Agency Co.',
|
||||||
|
startYear: 2022,
|
||||||
|
endYear: 2023,
|
||||||
|
description:
|
||||||
|
'Developed responsive web applications for clients, collaborated with designers, and optimized performance metrics',
|
||||||
|
technologies: ['React', 'Node.js', 'MongoDB', 'Tailwind CSS'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
position: 'Junior Frontend Developer',
|
||||||
|
company: 'Web Solutions Ltd.',
|
||||||
|
startYear: 2021,
|
||||||
|
endYear: 2022,
|
||||||
|
description:
|
||||||
|
'Built responsive web interfaces, fixed bugs, and participated in code reviews to improve code quality',
|
||||||
|
technologies: ['React', 'JavaScript', 'CSS', 'HTML'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
position: 'Freelance Web Developer',
|
||||||
|
startYear: 2020,
|
||||||
|
endYear: 2021,
|
||||||
|
description:
|
||||||
|
'Created custom websites for various clients, managed projects independently, and delivered quality work on time',
|
||||||
|
technologies: ['React', 'WordPress', 'JavaScript', 'jQuery'],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ExperienceSection } from './components/ExperienceSection';
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { ArrowDown } from 'lucide-react';
|
||||||
|
import Button from '../../../shared/components/Button';
|
||||||
|
|
||||||
|
export const HeroSection: React.FC = () => {
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.2,
|
||||||
|
delayChildren: 0.3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: { duration: 0.8 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="home"
|
||||||
|
className="min-h-screen flex items-center justify-center relative overflow-hidden pt-20 px-4 sm:px-6 lg:px-8"
|
||||||
|
>
|
||||||
|
{/* Background gradient */}
|
||||||
|
<div className="absolute inset-0 -z-10">
|
||||||
|
<div className="absolute top-0 left-0 w-96 h-96 bg-green-500/20 rounded-full blur-3xl"></div>
|
||||||
|
<div className="absolute bottom-0 right-0 w-96 h-96 bg-emerald-500/20 rounded-full blur-3xl"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
className="max-w-5xl mx-auto text-center"
|
||||||
|
>
|
||||||
|
{/* Badge */}
|
||||||
|
<motion.div variants={itemVariants} className="mb-8">
|
||||||
|
<div className="inline-block px-4 py-2 border border-green-500/50 rounded-full bg-green-500/10 backdrop-blur-sm">
|
||||||
|
<span className="text-green-400 text-sm font-medium flex items-center gap-2">
|
||||||
|
<span className="w-2 h-2 bg-green-500 rounded-full"></span>
|
||||||
|
React Developer & UI/UX Enthusiast | Based in San Francisco, CA
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Main Heading */}
|
||||||
|
<motion.h1 variants={itemVariants} className="text-5xl sm:text-6xl lg:text-7xl font-bold mb-6">
|
||||||
|
<span className="text-white">React is Developer</span>
|
||||||
|
<br />
|
||||||
|
<span className="bg-gradient-to-r from-green-500 to-emerald-500 bg-clip-text text-transparent">
|
||||||
|
Portfolio
|
||||||
|
</span>
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
{/* Subtitle */}
|
||||||
|
<motion.p
|
||||||
|
variants={itemVariants}
|
||||||
|
className="text-gray-300 text-lg sm:text-xl max-w-2xl mx-auto mb-8"
|
||||||
|
>
|
||||||
|
Building modern, scalable web applications with React, JavaScript, and cutting-edge technologies.
|
||||||
|
Transforming ideas into exceptional digital experiences.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<motion.div
|
||||||
|
variants={itemVariants}
|
||||||
|
className="grid grid-cols-3 gap-4 sm:gap-8 mb-12 max-w-2xl mx-auto"
|
||||||
|
>
|
||||||
|
<div className="border border-green-500/30 rounded-lg p-4 bg-green-500/5">
|
||||||
|
<div className="text-2xl sm:text-3xl font-bold text-green-500">3+</div>
|
||||||
|
<div className="text-gray-400 text-sm">Years Experience</div>
|
||||||
|
</div>
|
||||||
|
<div className="border border-green-500/30 rounded-lg p-4 bg-green-500/5">
|
||||||
|
<div className="text-2xl sm:text-3xl font-bold text-green-500">50+</div>
|
||||||
|
<div className="text-gray-400 text-sm">Projects Completed</div>
|
||||||
|
</div>
|
||||||
|
<div className="border border-green-500/30 rounded-lg p-4 bg-green-500/5">
|
||||||
|
<div className="text-2xl sm:text-3xl font-bold text-green-500">98%</div>
|
||||||
|
<div className="text-gray-400 text-sm">Client Satisfaction</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* CTA Buttons */}
|
||||||
|
<motion.div variants={itemVariants} className="flex flex-col sm:flex-row gap-4 justify-center mb-12">
|
||||||
|
<Button variant="primary" size="lg">
|
||||||
|
Get in Touch
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" size="lg">
|
||||||
|
View My Work
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Scroll Indicator */}
|
||||||
|
<motion.div
|
||||||
|
variants={itemVariants}
|
||||||
|
animate={{ y: [0, 10, 0] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity }}
|
||||||
|
className="flex justify-center"
|
||||||
|
>
|
||||||
|
<ArrowDown className="text-green-500" size={32} />
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { HeroSection } from './components/HeroSection';
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { useNavigation } from '../hooks/useNavigation';
|
||||||
|
import { useScrollToSection } from '../../../shared/hooks/useScrollToSection';
|
||||||
|
import Button from '../../../shared/components/Button';
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ id: 'home', label: 'About' },
|
||||||
|
{ id: 'skills', label: 'Skills' },
|
||||||
|
{ id: 'projects', label: 'Projects' },
|
||||||
|
{ id: 'experience', label: 'Experience' },
|
||||||
|
{ id: 'contact', label: 'Contact' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Navigation: React.FC = () => {
|
||||||
|
const { activeSection, isScrolled } = useNavigation();
|
||||||
|
const { scrollToSection } = useScrollToSection();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.nav
|
||||||
|
initial={{ y: -100 }}
|
||||||
|
animate={{ y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className={`fixed top-0 w-full z-50 transition-all duration-300 ${
|
||||||
|
isScrolled
|
||||||
|
? 'bg-black/80 backdrop-blur-md border-b border-green-500/20'
|
||||||
|
: 'bg-transparent'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex justify-between items-center h-16">
|
||||||
|
{/* Logo */}
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
className="text-2xl font-bold text-green-500 cursor-pointer"
|
||||||
|
onClick={() => scrollToSection('home')}
|
||||||
|
>
|
||||||
|
</> Alex
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Nav Items */}
|
||||||
|
<div className="hidden md:flex gap-8">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<motion.button
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => scrollToSection(item.id)}
|
||||||
|
className={`text-sm font-medium transition-colors duration-300 pb-2 border-b-2 ${
|
||||||
|
activeSection === item.id
|
||||||
|
? 'text-green-500 border-green-500'
|
||||||
|
: 'text-gray-300 border-transparent hover:text-white'
|
||||||
|
}`}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA Button */}
|
||||||
|
<Button variant="primary" size="sm">
|
||||||
|
Hire Me
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const useNavigation = () => {
|
||||||
|
const [activeSection, setActiveSection] = useState<string>('home');
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setIsScrolled(window.scrollY > 50);
|
||||||
|
|
||||||
|
// Update active section based on viewport
|
||||||
|
const sections = ['home', 'skills', 'projects', 'experience', 'contact'];
|
||||||
|
for (const section of sections) {
|
||||||
|
const element = document.getElementById(section);
|
||||||
|
if (element) {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
if (rect.top < 100 && rect.bottom > 100) {
|
||||||
|
setActiveSection(section);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { activeSection, isScrolled };
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { Navigation } from './components/Navigation';
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { ExternalLink } from 'lucide-react';
|
||||||
|
import { FaGithub } from 'react-icons/fa';
|
||||||
|
import type { Project } from '../../../shared/types';
|
||||||
|
|
||||||
|
interface ProjectCardProps {
|
||||||
|
project: Project;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProjectCard: React.FC<ProjectCardProps> = ({ project, index }) => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||||
|
whileHover={{ y: -10 }}
|
||||||
|
className="group rounded-xl overflow-hidden border border-green-500/30 bg-gradient-to-br from-green-500/5 to-emerald-500/5 backdrop-blur-sm hover:border-green-500/60 transition-all duration-300"
|
||||||
|
>
|
||||||
|
{/* Image */}
|
||||||
|
<div className="relative h-64 overflow-hidden bg-gradient-to-br from-gray-900 to-black">
|
||||||
|
<img
|
||||||
|
src={project.image}
|
||||||
|
alt={project.title}
|
||||||
|
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6">
|
||||||
|
{/* Category Badge */}
|
||||||
|
<div className="inline-block mb-4 px-3 py-1 rounded-full bg-green-500/20 border border-green-500/50">
|
||||||
|
<span className="text-green-400 text-xs font-semibold">{project.category}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-xl font-bold text-white mb-2">{project.title}</h3>
|
||||||
|
<p className="text-gray-400 text-sm mb-4 line-clamp-2">{project.description}</p>
|
||||||
|
|
||||||
|
{/* Tags */}
|
||||||
|
<div className="flex flex-wrap gap-2 mb-6">
|
||||||
|
{project.tags.slice(0, 3).map((tag) => (
|
||||||
|
<span
|
||||||
|
key={tag}
|
||||||
|
className="text-xs px-2 py-1 rounded border border-green-500/30 text-green-400"
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{project.tags.length > 3 && (
|
||||||
|
<span className="text-xs px-2 py-1 text-gray-500">
|
||||||
|
+{project.tags.length - 3}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Links */}
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{project.link && (
|
||||||
|
<motion.a
|
||||||
|
href={project.link}
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-green-500/20 hover:bg-green-500/30 text-green-400 transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
<ExternalLink size={16} />
|
||||||
|
Live
|
||||||
|
</motion.a>
|
||||||
|
)}
|
||||||
|
{project.github && (
|
||||||
|
<motion.a
|
||||||
|
href={project.github}
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 rounded-lg border border-green-500/30 hover:border-green-500/60 text-gray-300 hover:text-white transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
<FaGithub size={16} />
|
||||||
|
Code
|
||||||
|
</motion.a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { projectsData } from '../data/projectsData';
|
||||||
|
import { ProjectCard } from './ProjectCard';
|
||||||
|
import AnimatedSection from '../../../shared/components/AnimatedSection';
|
||||||
|
import type { Project } from '../../../shared/types';
|
||||||
|
|
||||||
|
type Category = 'All' | 'Web Apps' | 'UI Components' | 'Full Stack';
|
||||||
|
|
||||||
|
export const ProjectsSection: React.FC = () => {
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<Category>('All');
|
||||||
|
|
||||||
|
const categories: Category[] = ['All', 'Web Apps', 'UI Components', 'Full Stack'];
|
||||||
|
|
||||||
|
const filteredProjects = projectsData.filter(
|
||||||
|
(project: Project) =>
|
||||||
|
selectedCategory === 'All' || project.category === selectedCategory
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatedSection id="projects" className="py-20 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="inline-block px-4 py-2 rounded-full border border-green-500/50 bg-green-500/10 mb-6"
|
||||||
|
>
|
||||||
|
<span className="text-green-400 text-sm font-medium">My Work</span>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<h2 className="text-4xl sm:text-5xl font-bold text-white mb-4">
|
||||||
|
Featured Projects
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
|
||||||
|
Showcasing my best work and achievements
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Filter */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="flex flex-wrap justify-center gap-3 mb-12"
|
||||||
|
>
|
||||||
|
{categories.map((category) => (
|
||||||
|
<motion.button
|
||||||
|
key={category}
|
||||||
|
onClick={() => setSelectedCategory(category)}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className={`px-6 py-2 rounded-full font-medium transition-all duration-300 ${
|
||||||
|
selectedCategory === category
|
||||||
|
? 'bg-green-500 text-black'
|
||||||
|
: 'border border-green-500/30 text-gray-300 hover:border-green-500/60'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{category}
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Projects Grid */}
|
||||||
|
<motion.div
|
||||||
|
layout
|
||||||
|
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
||||||
|
>
|
||||||
|
{filteredProjects.map((project: Project, index: number) => (
|
||||||
|
<ProjectCard key={project.id} project={project} index={index} />
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</AnimatedSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { Project } from '../../../shared/types';
|
||||||
|
|
||||||
|
export const projectsData: Project[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'E-Commerce Platform',
|
||||||
|
description:
|
||||||
|
'Full-stack e-commerce platform with product catalog, shopping cart, and payment integration using Stripe',
|
||||||
|
image: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=800&h=600&fit=crop',
|
||||||
|
tags: ['React', 'Next.js', 'TypeScript', 'Tailwind CSS', 'MongoDB'],
|
||||||
|
category: 'Full Stack',
|
||||||
|
link: '#',
|
||||||
|
github: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: 'Task Management Dashboard',
|
||||||
|
description:
|
||||||
|
'A collaborative task management application with real-time updates, user authentication, and advanced filtering',
|
||||||
|
image: 'https://images.unsplash.com/photo-1552664730-d307ca884978?w=800&h=600&fit=crop',
|
||||||
|
tags: ['React', 'Firebase', 'TypeScript', 'Tailwind CSS'],
|
||||||
|
category: 'Web Apps',
|
||||||
|
link: '#',
|
||||||
|
github: '#',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
title: 'Component Library',
|
||||||
|
description:
|
||||||
|
'A comprehensive reusable component library with Storybook documentation and design system tokens',
|
||||||
|
image: 'https://images.unsplash.com/photo-1517694712202-14dd9538aa97?w=800&h=600&fit=crop',
|
||||||
|
tags: ['React', 'Storybook', 'Tailwind CSS', 'TypeScript'],
|
||||||
|
category: 'UI Components',
|
||||||
|
link: '#',
|
||||||
|
github: '#',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ProjectsSection } from './components/ProjectsSection';
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { iconMap } from '../data/skillsData';
|
||||||
|
import type { Skill } from '../../../shared/types';
|
||||||
|
|
||||||
|
interface SkillCardProps {
|
||||||
|
skill: Skill;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SkillCard: React.FC<SkillCardProps> = ({ skill, index }) => {
|
||||||
|
const levelColors = {
|
||||||
|
Expert: 'text-green-500',
|
||||||
|
Advanced: 'text-emerald-400',
|
||||||
|
Intermediate: 'text-cyan-400',
|
||||||
|
Beginner: 'text-blue-400',
|
||||||
|
};
|
||||||
|
|
||||||
|
const levelBg = {
|
||||||
|
Expert: 'bg-green-500/20',
|
||||||
|
Advanced: 'bg-emerald-500/20',
|
||||||
|
Intermediate: 'bg-cyan-500/20',
|
||||||
|
Beginner: 'bg-blue-500/20',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get icon from the mapped skill name
|
||||||
|
const IconComponent = iconMap[skill.name as keyof typeof iconMap];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||||
|
whileHover={{ scale: 1.05, y: -10 }}
|
||||||
|
className="p-6 rounded-xl border border-green-500/30 bg-gradient-to-br from-green-500/5 to-emerald-500/5 backdrop-blur-sm hover:border-green-500/60 transition-all duration-300"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div className="p-3 rounded-lg bg-green-500/20">
|
||||||
|
{IconComponent && <IconComponent className="text-green-500" size={24} />}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`text-xs font-semibold px-3 py-1 rounded-full ${levelBg[skill.level]}`}
|
||||||
|
>
|
||||||
|
<span className={levelColors[skill.level]}>{skill.level}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-bold text-white mb-2">{skill.name}</h3>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Proficiency</span>
|
||||||
|
<span className="text-green-400 font-medium">{skill.years}+ yrs</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-2 bg-gray-700 rounded-full overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
whileInView={{ width: `${(skill.years / 5) * 100}%` }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 1, delay: index * 0.1 + 0.3 }}
|
||||||
|
className="h-full bg-gradient-to-r from-green-500 to-emerald-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { skillsData } from '../data/skillsData';
|
||||||
|
import { SkillCard } from './SkillCard';
|
||||||
|
import AnimatedSection from '../../../shared/components/AnimatedSection';
|
||||||
|
|
||||||
|
type Category = 'All' | 'Frontend' | 'Backend' | 'Tools' | 'Design';
|
||||||
|
|
||||||
|
export const SkillsSection: React.FC = () => {
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<Category>('All');
|
||||||
|
|
||||||
|
const categories: Category[] = ['All', 'Frontend', 'Backend', 'Tools', 'Design'];
|
||||||
|
|
||||||
|
const filteredSkills = skillsData.filter(
|
||||||
|
(skill) => selectedCategory === 'All' || skill.category === selectedCategory
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatedSection id="skills" className="py-20 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
{/* Section Header */}
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="inline-block px-4 py-2 rounded-full border border-green-500/50 bg-green-500/10 mb-6"
|
||||||
|
>
|
||||||
|
<span className="text-green-400 text-sm font-medium">My Expertise</span>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<h2 className="text-4xl sm:text-5xl font-bold text-white mb-4">
|
||||||
|
Skills & Technologies
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
|
||||||
|
A comprehensive overview of my technical skills and proficiency levels
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Filter */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="flex flex-wrap justify-center gap-3 mb-12"
|
||||||
|
>
|
||||||
|
{categories.map((category) => (
|
||||||
|
<motion.button
|
||||||
|
key={category}
|
||||||
|
onClick={() => setSelectedCategory(category)}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className={`px-6 py-2 rounded-full font-medium transition-all duration-300 ${
|
||||||
|
selectedCategory === category
|
||||||
|
? 'bg-green-500 text-black'
|
||||||
|
: 'border border-green-500/30 text-gray-300 hover:border-green-500/60'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{category}
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Skills Grid */}
|
||||||
|
<motion.div
|
||||||
|
layout
|
||||||
|
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
||||||
|
>
|
||||||
|
{filteredSkills.map((skill, index: number) => (
|
||||||
|
<SkillCard key={skill.name} skill={skill} index={index} />
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</AnimatedSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
Code,
|
||||||
|
Zap,
|
||||||
|
Layout,
|
||||||
|
Database,
|
||||||
|
GitBranch,
|
||||||
|
Wind,
|
||||||
|
Package,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import type { Skill } from '../../../shared/types';
|
||||||
|
|
||||||
|
export const iconMap = {
|
||||||
|
'React.js': Code,
|
||||||
|
'JavaScript': Zap,
|
||||||
|
'TypeScript': Code,
|
||||||
|
'Next.js': Package,
|
||||||
|
'Node.js': Database,
|
||||||
|
'REST APIs': Code,
|
||||||
|
'Git & GitHub': GitBranch,
|
||||||
|
'Responsive Design': Layout,
|
||||||
|
'Figma': Wind,
|
||||||
|
'Tailwind CSS': Wind,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const skillsData: Array<Omit<Skill, 'icon'> & { iconName: keyof typeof iconMap }> = [
|
||||||
|
{
|
||||||
|
name: 'React.js',
|
||||||
|
iconName: 'React.js',
|
||||||
|
level: 'Expert',
|
||||||
|
years: 3,
|
||||||
|
category: 'Frontend',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'JavaScript',
|
||||||
|
iconName: 'JavaScript',
|
||||||
|
level: 'Expert',
|
||||||
|
years: 4,
|
||||||
|
category: 'Frontend',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TypeScript',
|
||||||
|
iconName: 'TypeScript',
|
||||||
|
level: 'Advanced',
|
||||||
|
years: 2,
|
||||||
|
category: 'Frontend',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Next.js',
|
||||||
|
iconName: 'Next.js',
|
||||||
|
level: 'Advanced',
|
||||||
|
years: 2,
|
||||||
|
category: 'Frontend',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Node.js',
|
||||||
|
iconName: 'Node.js',
|
||||||
|
level: 'Intermediate',
|
||||||
|
years: 2,
|
||||||
|
category: 'Backend',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'REST APIs',
|
||||||
|
iconName: 'REST APIs',
|
||||||
|
level: 'Advanced',
|
||||||
|
years: 3,
|
||||||
|
category: 'Backend',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Git & GitHub',
|
||||||
|
iconName: 'Git & GitHub',
|
||||||
|
level: 'Advanced',
|
||||||
|
years: 4,
|
||||||
|
category: 'Tools',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Responsive Design',
|
||||||
|
iconName: 'Responsive Design',
|
||||||
|
level: 'Expert',
|
||||||
|
years: 3,
|
||||||
|
category: 'Design',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Figma',
|
||||||
|
iconName: 'Figma',
|
||||||
|
level: 'Intermediate',
|
||||||
|
years: 2,
|
||||||
|
category: 'Design',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Tailwind CSS',
|
||||||
|
iconName: 'Tailwind CSS',
|
||||||
|
level: 'Expert',
|
||||||
|
years: 3,
|
||||||
|
category: 'Frontend',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { SkillsSection } from './components/SkillsSection';
|
||||||
@@ -1 +1,41 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Urbanist';
|
||||||
|
src: url('/Urbanist/Urbanist-VariableFont_wght.ttf') format('truetype');
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: 'Urbanist', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: #000000;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #22c55e;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #16a34a;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
interface AnimatedSectionProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
delay?: number;
|
||||||
|
className?: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnimatedSection: React.FC<AnimatedSectionProps> = ({
|
||||||
|
children,
|
||||||
|
delay = 0,
|
||||||
|
className = '',
|
||||||
|
id,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<motion.section
|
||||||
|
id={id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true, margin: '-100px' }}
|
||||||
|
transition={{ duration: 0.6, delay }}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnimatedSection;
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
onClick?: () => void;
|
||||||
|
variant?: 'primary' | 'secondary';
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button: React.FC<ButtonProps> = ({
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
variant = 'primary',
|
||||||
|
size = 'md',
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
const baseClasses = 'font-semibold rounded-full transition-all duration-300';
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'px-4 py-2 text-sm',
|
||||||
|
md: 'px-6 py-3 text-base',
|
||||||
|
lg: 'px-8 py-4 text-lg',
|
||||||
|
};
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
primary:
|
||||||
|
'bg-green-500 hover:bg-green-600 text-black shadow-lg hover:shadow-green-500/50',
|
||||||
|
secondary:
|
||||||
|
'border-2 border-green-500 text-green-500 hover:bg-green-500/10',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
onClick={onClick}
|
||||||
|
className={`${baseClasses} ${sizeClasses[size]} ${variantClasses[variant]} ${className}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Button;
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
const Footer: React.FC = () => {
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="border-t border-green-500/20 bg-black/40 backdrop-blur-md">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 mb-8">
|
||||||
|
{/* Brand */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
>
|
||||||
|
<h3 className="text-2xl font-bold text-green-500 mb-2"></> Alex</h3>
|
||||||
|
<p className="text-gray-400 text-sm">
|
||||||
|
Crafting seamless digital experiences with modern web technologies
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Quick Links */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
>
|
||||||
|
<h4 className="text-white font-semibold mb-4">Quick Links</h4>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{['About', 'Skills', 'Projects', 'Contact'].map((link) => (
|
||||||
|
<li key={link}>
|
||||||
|
<a href={`#${link.toLowerCase()}`} className="text-gray-400 hover:text-green-400 transition-colors">
|
||||||
|
{link}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Services */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<h4 className="text-white font-semibold mb-4">Services</h4>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{['Frontend Development', 'UI/UX Design', 'Full Stack Dev'].map((service) => (
|
||||||
|
<li key={service}>
|
||||||
|
<span className="text-gray-400">{service}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Tech Stack */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
>
|
||||||
|
<h4 className="text-white font-semibold mb-4">Tech Stack</h4>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{['React & Next.js', 'TypeScript', 'Tailwind CSS'].map((tech) => (
|
||||||
|
<li key={tech}>
|
||||||
|
<span className="text-gray-400 text-sm">{tech}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="border-t border-green-500/10 my-8"></div>
|
||||||
|
|
||||||
|
{/* Copyright */}
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-center">
|
||||||
|
<p className="text-gray-500 text-sm">
|
||||||
|
© {currentYear} Alex Johnson. All rights reserved.
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500 text-sm mt-4 md:mt-0">
|
||||||
|
Built with 💚 using React & Tailwind CSS
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as AnimatedSection } from './AnimatedSection';
|
||||||
|
export { default as Button } from './Button';
|
||||||
|
export { default as Footer } from './Footer';
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export const COLORS = {
|
||||||
|
primary: '#22c55e', // Green
|
||||||
|
secondary: '#1f2937', // Dark gray
|
||||||
|
accent: '#10b981', // Teal
|
||||||
|
background: '#000000', // Black
|
||||||
|
surface: '#111827', // Darker gray
|
||||||
|
text: {
|
||||||
|
primary: '#ffffff',
|
||||||
|
secondary: '#d1d5db',
|
||||||
|
muted: '#9ca3af',
|
||||||
|
},
|
||||||
|
border: '#1f2937',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GRADIENT = {
|
||||||
|
primary: 'from-green-500 to-emerald-500',
|
||||||
|
hover: 'hover:from-emerald-500 hover:to-green-500',
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { useScrollToSection } from './useScrollToSection';
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
export const useScrollToSection = () => {
|
||||||
|
const scrollToSection = useCallback((sectionId: string) => {
|
||||||
|
const element = document.getElementById(sectionId);
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { scrollToSection };
|
||||||
|
};
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
export type { Skill, Project, Experience, NavItem } from './index';
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
export interface Skill {
|
||||||
|
name: string;
|
||||||
|
level: 'Expert' | 'Advanced' | 'Intermediate' | 'Beginner';
|
||||||
|
years: number;
|
||||||
|
category: 'Frontend' | 'Backend' | 'Tools' | 'Design';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Project {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
image: string;
|
||||||
|
tags: string[];
|
||||||
|
category: 'Web Apps' | 'UI Components' | 'Full Stack';
|
||||||
|
link?: string;
|
||||||
|
github?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Experience {
|
||||||
|
id: string;
|
||||||
|
company?: string;
|
||||||
|
position: string;
|
||||||
|
startYear: number;
|
||||||
|
endYear?: number;
|
||||||
|
description: string;
|
||||||
|
technologies?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NavItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type { Config } from 'tailwindcss';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Urbanist', 'system-ui', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
green: {
|
||||||
|
50: '#f0fdf4',
|
||||||
|
100: '#dcfce7',
|
||||||
|
200: '#bbf7d0',
|
||||||
|
300: '#86efac',
|
||||||
|
400: '#4ade80',
|
||||||
|
500: '#22c55e',
|
||||||
|
600: '#16a34a',
|
||||||
|
700: '#15803d',
|
||||||
|
800: '#166534',
|
||||||
|
900: '#145231',
|
||||||
|
950: '#052e16',
|
||||||
|
},
|
||||||
|
emerald: {
|
||||||
|
50: '#f0fdf4',
|
||||||
|
100: '#dcfce7',
|
||||||
|
200: '#bbf7d0',
|
||||||
|
300: '#86efac',
|
||||||
|
400: '#4ade80',
|
||||||
|
500: '#10b981',
|
||||||
|
600: '#059669',
|
||||||
|
700: '#047857',
|
||||||
|
800: '#065f46',
|
||||||
|
900: '#064e3b',
|
||||||
|
950: '#022c1d',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'fade-in': 'fadeIn 0.5s ease-in-out',
|
||||||
|
'fade-out': 'fadeOut 0.5s ease-in-out',
|
||||||
|
'slide-up': 'slideUp 0.5s ease-out',
|
||||||
|
'slide-down': 'slideDown 0.5s ease-out',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
fadeIn: {
|
||||||
|
'0%': { opacity: '0' },
|
||||||
|
'100%': { opacity: '1' },
|
||||||
|
},
|
||||||
|
fadeOut: {
|
||||||
|
'0%': { opacity: '1' },
|
||||||
|
'100%': { opacity: '0' },
|
||||||
|
},
|
||||||
|
slideUp: {
|
||||||
|
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
||||||
|
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||||
|
},
|
||||||
|
slideDown: {
|
||||||
|
'0%': { transform: 'translateY(-10px)', opacity: '0' },
|
||||||
|
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
} satisfies Config;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"ignoreDeprecations": "6.0",
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "es2023",
|
"target": "es2023",
|
||||||
"lib": ["ES2023", "DOM"],
|
"lib": ["ES2023", "DOM"],
|
||||||
@@ -10,15 +11,18 @@
|
|||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
"@shared/*": ["src/shared/*"],
|
||||||
|
"@features/*": ["src/features/*"]
|
||||||
|
},
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": false,
|
||||||
"erasableSyntaxOnly": true,
|
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
|
|||||||
@@ -5,5 +5,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm --filter web dev",
|
"dev": "pnpm --filter web dev",
|
||||||
"build": "pnpm --filter web build"
|
"build": "pnpm --filter web build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"framer-motion": "^12.38.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+41
-3
@@ -6,7 +6,11 @@ settings:
|
|||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.: {}
|
.:
|
||||||
|
dependencies:
|
||||||
|
framer-motion:
|
||||||
|
specifier: ^12.38.0
|
||||||
|
version: 12.38.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
|
|
||||||
apps/web:
|
apps/web:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -679,6 +683,20 @@ packages:
|
|||||||
flatted@3.4.2:
|
flatted@3.4.2:
|
||||||
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
|
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
|
||||||
|
|
||||||
|
framer-motion@12.38.0:
|
||||||
|
resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emotion/is-prop-valid': '*'
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@emotion/is-prop-valid':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
@@ -854,6 +872,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
|
motion-dom@12.38.0:
|
||||||
|
resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
|
||||||
|
|
||||||
|
motion-utils@12.36.0:
|
||||||
|
resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==}
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -1669,6 +1693,15 @@ snapshots:
|
|||||||
|
|
||||||
flatted@3.4.2: {}
|
flatted@3.4.2: {}
|
||||||
|
|
||||||
|
framer-motion@12.38.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
|
||||||
|
dependencies:
|
||||||
|
motion-dom: 12.38.0
|
||||||
|
motion-utils: 12.36.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.6
|
||||||
|
react-dom: 19.2.6(react@19.2.6)
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -1794,6 +1827,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 5.0.6
|
brace-expansion: 5.0.6
|
||||||
|
|
||||||
|
motion-dom@12.38.0:
|
||||||
|
dependencies:
|
||||||
|
motion-utils: 12.36.0
|
||||||
|
|
||||||
|
motion-utils@12.36.0: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
nanoid@3.3.12: {}
|
nanoid@3.3.12: {}
|
||||||
@@ -1896,8 +1935,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
|
|
||||||
tslib@2.8.1:
|
tslib@2.8.1: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user