feat: add hero section and navigation components
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:
2026-05-10 14:34:54 +02:00
parent e85ce6a67b
commit 4ff07915b8
56 changed files with 1602 additions and 16 deletions
+93
View File
@@ -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.
+81
View File
@@ -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 arent 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.
+21 -8
View File
@@ -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() {
return (
<div className="min-h-screen bg-black text-white flex items-center justify-center">
<div className="text-center">
<h1 className="text-7xl font-bold mb-4 bg-gradient-to-r from-emerald-400 to-cyan-400 text-transparent bg-clip-text">
Mein Portfolio
</h1>
<p className="text-xl text-gray-400">
Full-Stack Entwickler & DevOps Enthusiast
</p>
<div className="bg-black text-white min-h-screen overflow-x-hidden">
{/* Background gradient decorations */}
<div className="fixed inset-0 -z-10">
<div className="absolute top-0 left-0 w-96 h-96 bg-green-500/10 rounded-full blur-3xl opacity-50"></div>
<div className="absolute bottom-0 right-0 w-96 h-96 bg-emerald-500/10 rounded-full blur-3xl opacity-50"></div>
</div>
<Navigation />
<HeroSection />
<SkillsSection />
<ProjectsSection />
<ExperienceSection />
<ContactSection />
<Footer />
</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>
);
};
+1
View File
@@ -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>
);
};
+1
View File
@@ -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')}
>
&lt;/&gt; 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: '#',
},
];
+1
View File
@@ -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',
},
];
+1
View File
@@ -0,0 +1 @@
export { SkillsSection } from './components/SkillsSection';
+40
View File
@@ -1 +1,41 @@
@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;
+46
View File
@@ -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;
+94
View File
@@ -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">&lt;/&gt; 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;
+3
View File
@@ -0,0 +1,3 @@
export { default as AnimatedSection } from './AnimatedSection';
export { default as Button } from './Button';
export { default as Footer } from './Footer';
+18
View File
@@ -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',
};
+1
View File
@@ -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 };
};
+1
View File
@@ -0,0 +1 @@
export type { Skill, Project, Experience, NavItem } from './index';
+33
View File
@@ -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;
}
+65
View File
@@ -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;
+9 -5
View File
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"ignoreDeprecations": "6.0",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
@@ -10,15 +11,18 @@
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@shared/*": ["src/shared/*"],
"@features/*": ["src/features/*"]
},
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]