id
This commit is contained in:
149
frontend/src/components/DonateShop.tsx
Normal file
149
frontend/src/components/DonateShop.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Heart, ShoppingCart, Star } from 'lucide-react';
|
||||
|
||||
const DonateShop: React.FC = () => {
|
||||
const [donatedItems, setDonatedItems] = useState<Set<number>>(new Set());
|
||||
|
||||
const products = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Coffee Support',
|
||||
price: 5,
|
||||
originalPrice: 10,
|
||||
image: 'https://images.unsplash.com/photo-1509042239860-f550ce710b93?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&q=80',
|
||||
description: 'Fuel my coding sessions with a virtual coffee',
|
||||
rating: 4.8,
|
||||
reviews: 124
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Meal Contribution',
|
||||
price: 15,
|
||||
originalPrice: 25,
|
||||
image: 'https://images.unsplash.com/photo-1565299624946-b28f40a0ca4b?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&q=80',
|
||||
description: 'Help me focus on creating amazing projects',
|
||||
rating: 4.9,
|
||||
reviews: 89
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Project Boost',
|
||||
price: 25,
|
||||
originalPrice: 40,
|
||||
image: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&q=80',
|
||||
description: 'Support the development of new features',
|
||||
rating: 5.0,
|
||||
reviews: 67
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Monthly Patron',
|
||||
price: 50,
|
||||
originalPrice: 75,
|
||||
image: 'https://images.unsplash.com/photo-1552664730-d307ca884978?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&q=80',
|
||||
description: 'Ongoing support for continuous improvement',
|
||||
rating: 4.7,
|
||||
reviews: 45
|
||||
}
|
||||
];
|
||||
|
||||
const handleDonate = (productId: number) => {
|
||||
setDonatedItems(prev => new Set(prev).add(productId));
|
||||
// Here you would integrate with a payment processor like Stripe
|
||||
alert(`Thank you for your donation of $${products.find(p => p.id === productId)?.price}!`);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="py-16 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold text-text sm:text-4xl">
|
||||
Support My Creative Journey
|
||||
</h2>
|
||||
<p className="mt-4 text-lg text-lines max-w-2xl mx-auto">
|
||||
Instead of buying products, consider donating to support my creative journey
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{products.map((product) => (
|
||||
<div
|
||||
key={product.id}
|
||||
className="bg-background-light rounded-xl shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2 group border border-lines overflow-hidden"
|
||||
>
|
||||
<div className="relative overflow-hidden">
|
||||
<img
|
||||
className="w-full h-48 object-cover group-hover:scale-110 transition-transform duration-300"
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="absolute top-4 left-4">
|
||||
<div className="bg-other text-background px-2 py-1 rounded-full text-xs font-medium">
|
||||
Donation
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute top-4 right-4 flex items-center space-x-1">
|
||||
<Star className="h-4 w-4 text-other fill-current" />
|
||||
<span className="text-text text-sm font-medium bg-background/70 px-1 rounded">
|
||||
{product.rating}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<h3 className="text-xl font-semibold text-text mb-2">
|
||||
{product.name}
|
||||
</h3>
|
||||
<p className="text-lines mb-4 text-sm">
|
||||
{product.description}
|
||||
</p>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-2xl font-bold text-text">
|
||||
${product.price}
|
||||
</span>
|
||||
<span className="text-sm text-lines line-through">
|
||||
${product.originalPrice}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm text-lines">
|
||||
({product.reviews} reviews)
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleDonate(product.id)}
|
||||
disabled={donatedItems.has(product.id)}
|
||||
className={`w-full flex items-center justify-center px-4 py-3 rounded-lg text-sm font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-other focus:ring-offset-2 ${
|
||||
donatedItems.has(product.id)
|
||||
? 'bg-other/20 text-other cursor-not-allowed'
|
||||
: 'bg-other text-background hover:bg-lines transform hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
{donatedItems.has(product.id) ? (
|
||||
<>
|
||||
<Heart className="h-5 w-5 mr-2 fill-current" />
|
||||
Donated!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ShoppingCart className="h-5 w-5 mr-2" />
|
||||
Donate Now
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-12">
|
||||
<p className="text-sm text-lines">
|
||||
All donations go towards improving my portfolio and creating more amazing projects
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default DonateShop;
|
||||
99
frontend/src/components/DroneVideoCarousel.tsx
Normal file
99
frontend/src/components/DroneVideoCarousel.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
const DroneVideoCarousel: React.FC = () => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
// Placeholder YouTube video IDs - replace with actual drone video IDs
|
||||
const videos = [
|
||||
{ id: 'dQw4w9WgXcQ', title: 'Drone Footage 1' },
|
||||
{ id: 'dQw4w9WgXcQ', title: 'Drone Footage 2' },
|
||||
{ id: 'dQw4w9WgXcQ', title: 'Drone Footage 3' },
|
||||
{ id: 'dQw4w9WgXcQ', title: 'Drone Footage 4' }
|
||||
];
|
||||
|
||||
const nextSlide = () => {
|
||||
setCurrentIndex((prevIndex) => (prevIndex + 1) % videos.length);
|
||||
};
|
||||
|
||||
const prevSlide = () => {
|
||||
setCurrentIndex((prevIndex) => (prevIndex - 1 + videos.length) % videos.length);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(nextSlide, 5000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="py-20 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold text-text sm:text-4xl">
|
||||
Drone Videography
|
||||
</h2>
|
||||
<p className="mt-4 text-lg text-lines max-w-2xl mx-auto">
|
||||
Capturing stunning aerial perspectives through professional drone footage
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative max-w-4xl mx-auto">
|
||||
<div className="aspect-video bg-background-light rounded-xl overflow-hidden shadow-2xl border border-lines">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={currentIndex}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="w-full h-full"
|
||||
>
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${videos[currentIndex].id}?autoplay=0&mute=1&loop=1&playlist=${videos[currentIndex].id}`}
|
||||
title={videos[currentIndex].title}
|
||||
className="w-full h-full"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={prevSlide}
|
||||
className="absolute left-4 top-1/2 transform -translate-y-1/2 bg-boxes hover:bg-other text-text p-3 rounded-full transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-offset-2 focus:ring-offset-background"
|
||||
aria-label="Previous video"
|
||||
>
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={nextSlide}
|
||||
className="absolute right-4 top-1/2 transform -translate-y-1/2 bg-boxes hover:bg-other text-text p-3 rounded-full transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-offset-2 focus:ring-offset-background"
|
||||
aria-label="Next video"
|
||||
>
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</button>
|
||||
|
||||
<div className="flex justify-center mt-6 space-x-2">
|
||||
{videos.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setCurrentIndex(index)}
|
||||
className={`w-3 h-3 rounded-full transition-all duration-200 ${
|
||||
index === currentIndex
|
||||
? 'bg-other'
|
||||
: 'bg-lines hover:bg-boxes'
|
||||
}`}
|
||||
aria-label={`Go to video ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default DroneVideoCarousel;
|
||||
127
frontend/src/components/Footer.tsx
Normal file
127
frontend/src/components/Footer.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import React from 'react';
|
||||
import { Instagram, Twitter, Youtube, Github, Linkedin, Gamepad2, Mail, Phone } from 'lucide-react';
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const socialLinks = [
|
||||
{ name: 'Instagram', icon: Instagram, href: '#', color: 'hover:text-other' },
|
||||
{ name: 'Twitter', icon: Twitter, href: '#', color: 'hover:text-other' },
|
||||
{ name: 'YouTube', icon: Youtube, href: '#', color: 'hover:text-other' },
|
||||
{ name: 'GitHub', icon: Github, href: '#', color: 'hover:text-text' },
|
||||
{ name: 'LinkedIn', icon: Linkedin, href: '#', color: 'hover:text-other' },
|
||||
{ name: 'Steam', icon: Gamepad2, href: '#', color: 'hover:text-lines' }
|
||||
];
|
||||
|
||||
const footerLinks = [
|
||||
{
|
||||
title: 'Portfolio',
|
||||
links: [
|
||||
{ name: 'Web Development', href: '/portfolio/web' },
|
||||
{ name: 'Mobile Apps', href: '/portfolio/mobile' },
|
||||
{ name: 'UI/UX Design', href: '/portfolio/design' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Services',
|
||||
links: [
|
||||
{ name: 'Frontend Development', href: '/services/frontend' },
|
||||
{ name: 'Backend Development', href: '/services/backend' },
|
||||
{ name: 'Consulting', href: '/services/consulting' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Company',
|
||||
links: [
|
||||
{ name: 'About', href: '/about' },
|
||||
{ name: 'Blog', href: '/blog' },
|
||||
{ name: 'Contact', href: '/contact' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<footer className="bg-background text-text border-t border-lines">
|
||||
<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-2 lg:grid-cols-4 gap-8">
|
||||
{/* Brand Section */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="flex items-center">
|
||||
<span className="text-2xl font-bold text-other">
|
||||
Portfolio
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-4 text-lines text-sm">
|
||||
Creating exceptional digital experiences through innovative web development and beautiful design.
|
||||
</p>
|
||||
<div className="mt-6 flex space-x-4">
|
||||
{socialLinks.map((social) => {
|
||||
const IconComponent = social.icon;
|
||||
return (
|
||||
<a
|
||||
key={social.name}
|
||||
href={social.href}
|
||||
className={`text-lines ${social.color} transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-other focus:ring-offset-2 focus:ring-offset-background rounded`}
|
||||
aria-label={social.name}
|
||||
>
|
||||
<IconComponent className="h-5 w-5" />
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-6 space-y-2">
|
||||
<div className="flex items-center text-lines text-sm">
|
||||
<Mail className="h-4 w-4 mr-2" />
|
||||
hello@example.com
|
||||
</div>
|
||||
<div className="flex items-center text-lines text-sm">
|
||||
<Phone className="h-4 w-4 mr-2" />
|
||||
+1 (555) 123-4567
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Links Sections */}
|
||||
{footerLinks.map((section) => (
|
||||
<div key={section.title}>
|
||||
<h3 className="text-sm font-semibold text-text uppercase tracking-wider">
|
||||
{section.title}
|
||||
</h3>
|
||||
<ul className="mt-4 space-y-2">
|
||||
{section.links.map((link) => (
|
||||
<li key={link.name}>
|
||||
<a
|
||||
href={link.href}
|
||||
className="text-lines hover:text-other transition-colors duration-200 text-sm focus:outline-none focus:ring-2 focus:ring-other focus:ring-offset-2 focus:ring-offset-background rounded"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-8 border-t border-boxes">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center">
|
||||
<p className="text-lines text-sm">
|
||||
© 2025 Portfolio. All rights reserved.
|
||||
</p>
|
||||
<div className="mt-4 md:mt-0 flex space-x-6">
|
||||
<a href="/privacy" className="text-lines hover:text-text text-sm transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-other focus:ring-offset-2 focus:ring-offset-background rounded">
|
||||
Privacy Policy
|
||||
</a>
|
||||
<a href="/terms" className="text-lines hover:text-text text-sm transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-other focus:ring-offset-2 focus:ring-offset-background rounded">
|
||||
Terms of Service
|
||||
</a>
|
||||
</div>
|
||||
<p className="text-lines text-sm mt-4 md:mt-0">
|
||||
Built with ❤️ by <a rel="nofollow" target="_blank" href="https://meku.dev" className="text-other hover:text-lines transition-colors duration-200">Meku.dev</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -1,40 +0,0 @@
|
||||
footer a{
|
||||
color: var(--c-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
footer a i{
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
footer a:hover i{
|
||||
color: var(--c-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
footer{
|
||||
font-family: "Roboto Mono", monospace;
|
||||
|
||||
background-color: var(--c-boxes);
|
||||
|
||||
margin-top: 2em;
|
||||
display: flex;
|
||||
|
||||
color: white;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
footer address{
|
||||
padding: 1em;
|
||||
font-style: normal;
|
||||
}
|
||||
footer .contacts{
|
||||
font-size: 2em;
|
||||
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 990px){
|
||||
footer{
|
||||
flex-direction: column;
|
||||
padding-bottom: 1em;
|
||||
padding-top: 1em;
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import styles from "./footer.module.css"
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer id="contacts">
|
||||
<div>
|
||||
<h1>vontor.cz</h1>
|
||||
</div>
|
||||
|
||||
<address>
|
||||
Written by <b>David Bruno Vontor | © 2025</b>
|
||||
<br />
|
||||
<p>
|
||||
Tel.:{" "}
|
||||
<a href="tel:+420605512624">
|
||||
<u>+420 605 512 624</u>
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
E-mail:{" "}
|
||||
<a href="mailto:brunovontor@gmail.com">
|
||||
<u>brunovontor@gmail.com</u>
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
IČO:{" "}
|
||||
<a
|
||||
href="https://www.rzp.cz/verejne-udaje/cs/udaje/vyber-subjektu;ico=21613109;"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<u>21613109</u>
|
||||
</a>
|
||||
</p>
|
||||
</address>
|
||||
|
||||
<div className="contacts">
|
||||
<a
|
||||
href="https://github.com/Brunobrno"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i className="fa fa-github"></i>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.instagram.com/brunovontor/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i className="fa fa-instagram"></i>
|
||||
</a>
|
||||
<a
|
||||
href="https://twitter.com/BVontor"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i className="fa-brands fa-x-twitter"></i>
|
||||
</a>
|
||||
<a
|
||||
href="https://steamcommunity.com/id/Brunobrno/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i className="fa-brands fa-steam"></i>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.youtube.com/@brunovontor"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i className="fa-brands fa-youtube"></i>
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import React, { useState, useRef } from "react"
|
||||
import styles from "./contact-me.module.css"
|
||||
import { LuMousePointerClick } from "react-icons/lu";
|
||||
|
||||
export default function ContactMeForm() {
|
||||
const [opened, setOpened] = useState(false)
|
||||
const [contentMoveUp, setContentMoveUp] = useState(false)
|
||||
const [openingBehind, setOpeningBehind] = useState(false)
|
||||
const [success, setSuccess] = useState(false)
|
||||
const openingRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
function handleSubmit() {
|
||||
// form submission logic here
|
||||
}
|
||||
|
||||
const toggleOpen = () => {
|
||||
if (!opened) {
|
||||
setOpened(true)
|
||||
setOpeningBehind(false)
|
||||
setContentMoveUp(false)
|
||||
// Wait for the rotate-opening animation to finish before moving content up
|
||||
// The actual moveUp will be handled in onTransitionEnd
|
||||
} else {
|
||||
setContentMoveUp(false)
|
||||
setOpeningBehind(false)
|
||||
setTimeout(() => setOpened(false), 1000) // match transition duration
|
||||
}
|
||||
}
|
||||
|
||||
const handleTransitionEnd = (e: React.TransitionEvent<HTMLDivElement>) => {
|
||||
if (opened && e.propertyName === "transform") {
|
||||
setContentMoveUp(true)
|
||||
// Move the opening behind after the animation
|
||||
setTimeout(() => setOpeningBehind(true), 10)
|
||||
}
|
||||
if (!opened && e.propertyName === "transform") {
|
||||
setOpeningBehind(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles["contact-me"]}>
|
||||
<div
|
||||
ref={openingRef}
|
||||
className={
|
||||
[
|
||||
styles.opening,
|
||||
opened ? styles["rotate-opening"] : "",
|
||||
openingBehind ? styles["opening-behind"] : ""
|
||||
].filter(Boolean).join(" ")
|
||||
}
|
||||
onClick={toggleOpen}
|
||||
onTransitionEnd={handleTransitionEnd}
|
||||
>
|
||||
<LuMousePointerClick/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
contentMoveUp
|
||||
? `${styles.content} ${styles["content-moveup"]}`
|
||||
: styles.content
|
||||
}
|
||||
>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Váš email"
|
||||
required
|
||||
/>
|
||||
<textarea
|
||||
name="message"
|
||||
placeholder="Vaše zpráva"
|
||||
required
|
||||
/>
|
||||
<input type="submit"/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className={styles.cover}></div>
|
||||
<div className={styles.triangle}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
.contact-me {
|
||||
margin: 5em auto;
|
||||
position: relative;
|
||||
|
||||
aspect-ratio: 16 / 9;
|
||||
|
||||
background-color: #c8c8c8;
|
||||
max-width: 100vw;
|
||||
}
|
||||
.contact-me + .mail-box{
|
||||
|
||||
}
|
||||
|
||||
.contact-me .opening {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
z-index: 2;
|
||||
transform-origin: top;
|
||||
|
||||
padding-top: 4em;
|
||||
|
||||
clip-path: polygon(0 0, 100% 0, 50% 50%);
|
||||
background-color: #d2d2d2;
|
||||
|
||||
transition: all 1s ease;
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
.rotate-opening{
|
||||
background-color: #c8c8c8;
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
|
||||
.opening svg{
|
||||
margin: auto;
|
||||
font-size: 3em;
|
||||
margin-top: -0.5em;
|
||||
}
|
||||
|
||||
|
||||
.contact-me .content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
transition: all 1s ease-out;
|
||||
}
|
||||
.content-moveup{
|
||||
transform: translateY(-70%);
|
||||
}
|
||||
.content-moveup-index {
|
||||
z-index: 2 !important;
|
||||
}
|
||||
|
||||
.contact-me .content form{
|
||||
width: 80%;
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
background-color: #deefff;
|
||||
padding: 1em;
|
||||
border: 0.5em dashed #88d4ed;
|
||||
border-radius: 0.25em;
|
||||
}
|
||||
.contact-me .content form div{
|
||||
width: -webkit-fill-available;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.contact-me .content form input[type=submit]{
|
||||
margin: auto;
|
||||
border: none;
|
||||
background: #4ca4d5;
|
||||
color: #ffffff;
|
||||
padding: 1em 1.5em;
|
||||
cursor: pointer;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.contact-me .content form input[type=text],
|
||||
.contact-me .content form input[type=email],
|
||||
.contact-me .content form textarea{
|
||||
background-color: #bfe8ff;
|
||||
border: none;
|
||||
border-bottom: 0.15em solid #064c7d;
|
||||
padding: 0.5em;
|
||||
|
||||
}
|
||||
|
||||
.opening-behind { z-index: 0 !important; }
|
||||
|
||||
.contact-me .cover {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
clip-path: polygon(0 0, 50% 50%, 100% 0, 100% 100%, 0 100%);
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.contact-me .triangle{
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 3;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
clip-path: polygon(100% 0, 0 100%, 100% 100%);
|
||||
background-color: rgb(255 255 255);
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0% { transform: translateX(0); }
|
||||
25% { transform: translateX(-2px) rotate(-8deg); }
|
||||
50% { transform: translateX(2px) rotate(4deg); }
|
||||
75% { transform: translateX(-1px) rotate(-2deg); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
|
||||
|
||||
.contact-me .opening i {
|
||||
color: #797979;
|
||||
font-size: 5em;
|
||||
display: inline-block;
|
||||
animation: 0.4s ease-in-out 2s infinite normal none running shake;
|
||||
animation-delay: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@media only screen and (max-width: 990px){
|
||||
.contact-me{
|
||||
aspect-ratio: unset;
|
||||
margin-top: 7ch;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB |
186
frontend/src/components/Header.tsx
Normal file
186
frontend/src/components/Header.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { ChevronDown, Menu, X } from 'lucide-react';
|
||||
|
||||
const Header: React.FC = () => {
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleMouseEnter = (menu: string) => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
setActiveDropdown(menu);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setActiveDropdown(null);
|
||||
}, 150);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const navigationItems = [
|
||||
{ name: 'Home', href: '/' },
|
||||
{
|
||||
name: 'Portfolio',
|
||||
href: '/portfolio',
|
||||
submenu: [
|
||||
{ name: 'Web Development', href: '/portfolio/web' },
|
||||
{ name: 'Mobile Apps', href: '/portfolio/mobile' },
|
||||
{ name: 'UI/UX Design', href: '/portfolio/design' },
|
||||
{ name: 'E-commerce', href: '/portfolio/ecommerce' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Services',
|
||||
href: '/services',
|
||||
submenu: [
|
||||
{ name: 'Frontend Development', href: '/services/frontend' },
|
||||
{ name: 'Backend Development', href: '/services/backend' },
|
||||
{ name: 'Full Stack Solutions', href: '/services/fullstack' },
|
||||
{ name: 'Consulting', href: '/services/consulting' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'About',
|
||||
href: '/about',
|
||||
submenu: [
|
||||
{ name: 'My Story', href: '/about/story' },
|
||||
{ name: 'Skills', href: '/about/skills' },
|
||||
{ name: 'Experience', href: '/about/experience' },
|
||||
{ name: 'Testimonials', href: '/about/testimonials' }
|
||||
]
|
||||
},
|
||||
{ name: 'Blog', href: '/blog' },
|
||||
{ name: 'Contact', href: '/contact' }
|
||||
];
|
||||
|
||||
return (
|
||||
<header className="bg-background shadow-lg sticky top-0 z-50">
|
||||
<nav className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8" role="navigation" aria-label="Main navigation">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
{/* Logo */}
|
||||
<div className="flex-shrink-0">
|
||||
<a href="/" className="text-2xl font-bold text-other hover:text-lines transition-colors">
|
||||
Portfolio
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:block">
|
||||
<div className="ml-10 flex items-baseline space-x-4">
|
||||
{navigationItems.map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="relative"
|
||||
onMouseEnter={() => item.submenu && handleMouseEnter(item.name)}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<a
|
||||
href={item.href}
|
||||
className="flex items-center px-3 py-2 rounded-md text-sm font-medium text-text hover:text-other hover:bg-background-light transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-offset-2"
|
||||
aria-haspopup={item.submenu ? 'true' : 'false'}
|
||||
aria-expanded={activeDropdown === item.name ? 'true' : 'false'}
|
||||
>
|
||||
{item.name}
|
||||
{item.submenu && (
|
||||
<ChevronDown
|
||||
className={`ml-1 h-4 w-4 transition-transform duration-200 ${
|
||||
activeDropdown === item.name ? 'rotate-180' : ''
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{item.submenu && (
|
||||
<div
|
||||
className={`absolute left-0 mt-2 w-56 rounded-md shadow-lg bg-background-light ring-1 ring-lines ring-opacity-30 transition-all duration-200 ${
|
||||
activeDropdown === item.name
|
||||
? 'opacity-100 visible transform translate-y-0'
|
||||
: 'opacity-0 invisible transform -translate-y-2'
|
||||
}`}
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
>
|
||||
<div className="py-1">
|
||||
{item.submenu.map((subItem) => (
|
||||
<a
|
||||
key={subItem.name}
|
||||
href={subItem.href}
|
||||
className="block px-4 py-2 text-sm text-text hover:bg-boxes hover:text-other transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-inset"
|
||||
role="menuitem"
|
||||
>
|
||||
{subItem.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<div className="md:hidden">
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
className="inline-flex items-center justify-center p-2 rounded-md text-text hover:text-other hover:bg-background-light focus:outline-none focus:ring-2 focus:ring-inset focus:ring-lines transition-colors duration-200"
|
||||
aria-expanded={isMobileMenuOpen}
|
||||
aria-label="Toggle mobile menu"
|
||||
>
|
||||
{isMobileMenuOpen ? (
|
||||
<X className="block h-6 w-6" aria-hidden="true" />
|
||||
) : (
|
||||
<Menu className="block h-6 w-6" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
<div className={`md:hidden transition-all duration-300 ease-in-out ${
|
||||
isMobileMenuOpen ? 'max-h-screen opacity-100' : 'max-h-0 opacity-0 overflow-hidden'
|
||||
}`}>
|
||||
<div className="px-2 pt-2 pb-3 space-y-1 sm:px-3 bg-background-light rounded-lg mt-2">
|
||||
{navigationItems.map((item) => (
|
||||
<div key={item.name}>
|
||||
<a
|
||||
href={item.href}
|
||||
className="block px-3 py-2 rounded-md text-base font-medium text-text hover:text-other hover:bg-boxes transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-offset-2"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
{item.submenu && (
|
||||
<div className="ml-4 space-y-1">
|
||||
{item.submenu.map((subItem) => (
|
||||
<a
|
||||
key={subItem.name}
|
||||
href={subItem.href}
|
||||
className="block px-3 py-2 rounded-md text-sm text-lines hover:text-other hover:bg-boxes transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-offset-2"
|
||||
>
|
||||
{subItem.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
59
frontend/src/components/Hero.tsx
Normal file
59
frontend/src/components/Hero.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { ArrowRight, Download } from 'lucide-react';
|
||||
|
||||
const Hero: React.FC = () => {
|
||||
return (
|
||||
<section className="relative bg-background-light py-20 lg:py-32 overflow-hidden">
|
||||
<div className="absolute inset-0 bg-grid-pattern opacity-5"></div>
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="lg:grid lg:grid-cols-12 lg:gap-8 items-center">
|
||||
<div className="sm:text-center md:max-w-2xl md:mx-auto lg:col-span-6 lg:text-left">
|
||||
<h1 className="text-4xl font-bold text-text sm:text-5xl lg:text-6xl">
|
||||
<span className="block">Creative</span>
|
||||
<span className="block text-other">
|
||||
Developer
|
||||
</span>
|
||||
</h1>
|
||||
<p className="mt-6 text-lg text-lines sm:text-xl max-w-3xl">
|
||||
I craft exceptional digital experiences through innovative web development,
|
||||
combining cutting-edge technology with beautiful design to bring your ideas to life.
|
||||
</p>
|
||||
<div className="mt-8 sm:max-w-lg sm:mx-auto sm:text-center lg:text-left lg:mx-0">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<a
|
||||
href="/portfolio"
|
||||
className="inline-flex items-center justify-center px-6 py-3 border border-transparent text-base font-medium rounded-lg text-background bg-other hover:bg-lines focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-other transition-all duration-200 transform hover:scale-105"
|
||||
>
|
||||
View My Work
|
||||
<ArrowRight className="ml-2 h-5 w-5" aria-hidden="true" />
|
||||
</a>
|
||||
<a
|
||||
href="/resume.pdf"
|
||||
className="inline-flex items-center justify-center px-6 py-3 border-2 border-lines text-base font-medium rounded-lg text-text bg-transparent hover:bg-background hover:border-other hover:text-other focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-other transition-all duration-200"
|
||||
>
|
||||
<Download className="mr-2 h-5 w-5" aria-hidden="true" />
|
||||
Download Resume
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-12 relative sm:max-w-lg sm:mx-auto lg:mt-0 lg:max-w-none lg:mx-0 lg:col-span-6 lg:flex lg:items-center">
|
||||
<div className="relative mx-auto w-full rounded-lg shadow-lg lg:max-w-md">
|
||||
<div className="relative block w-full bg-boxes rounded-lg overflow-hidden">
|
||||
<img
|
||||
className="w-full h-96 object-cover"
|
||||
src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=687&q=80"
|
||||
alt="Professional developer portrait"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-tr from-boxes/40 to-other/20"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hero;
|
||||
110
frontend/src/components/Portfolio.tsx
Normal file
110
frontend/src/components/Portfolio.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import React from 'react';
|
||||
import { ExternalLink, Github } from 'lucide-react';
|
||||
|
||||
const Portfolio: React.FC = () => {
|
||||
const projects = [
|
||||
{
|
||||
title: 'E-Commerce Platform',
|
||||
description: 'A full-stack e-commerce solution with React, Node.js, and Stripe integration.',
|
||||
image: 'https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
|
||||
tags: ['React', 'Node.js', 'MongoDB', 'Stripe'],
|
||||
liveUrl: '#',
|
||||
githubUrl: '#'
|
||||
},
|
||||
{
|
||||
title: 'Task Management App',
|
||||
description: 'A collaborative project management tool with real-time updates and team features.',
|
||||
image: 'https://images.unsplash.com/photo-1611224923853-80b023f02d71?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
|
||||
tags: ['Vue.js', 'Firebase', 'Tailwind CSS'],
|
||||
liveUrl: '#',
|
||||
githubUrl: '#'
|
||||
},
|
||||
{
|
||||
title: 'Weather Dashboard',
|
||||
description: 'A responsive weather application with location-based forecasts and data visualization.',
|
||||
image: 'https://images.unsplash.com/photo-1504608524841-42fe6f032b4b?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
|
||||
tags: ['React', 'TypeScript', 'Chart.js', 'API'],
|
||||
liveUrl: '#',
|
||||
githubUrl: '#'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-20 bg-background">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl font-bold text-text sm:text-4xl">
|
||||
Featured Projects
|
||||
</h2>
|
||||
<p className="mt-4 text-lg text-lines max-w-2xl mx-auto">
|
||||
A showcase of my recent work and creative solutions
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-16 grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{projects.map((project, index) => (
|
||||
<div
|
||||
key={project.title}
|
||||
className="bg-background-light rounded-xl shadow-lg overflow-hidden hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2 group border border-lines"
|
||||
>
|
||||
<div className="relative overflow-hidden">
|
||||
<img
|
||||
className="w-full h-48 object-cover group-hover:scale-110 transition-transform duration-300"
|
||||
src={project.image}
|
||||
alt={project.title}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background/80 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<div className="absolute top-4 right-4 flex space-x-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<a
|
||||
href={project.liveUrl}
|
||||
className="p-2 bg-boxes rounded-full hover:bg-other transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-offset-2"
|
||||
aria-label={`View ${project.title} live`}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 text-text" />
|
||||
</a>
|
||||
<a
|
||||
href={project.githubUrl}
|
||||
className="p-2 bg-boxes rounded-full hover:bg-other transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-offset-2"
|
||||
aria-label={`View ${project.title} source code`}
|
||||
>
|
||||
<Github className="h-4 w-4 text-text" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<h3 className="text-xl font-semibold text-text mb-2">
|
||||
{project.title}
|
||||
</h3>
|
||||
<p className="text-lines mb-4">
|
||||
{project.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-3 py-1 text-xs font-medium bg-boxes text-other rounded-full"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-12">
|
||||
<a
|
||||
href="/portfolio"
|
||||
className="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-lg text-background bg-other hover:bg-lines focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-other transition-all duration-200 transform hover:scale-105"
|
||||
>
|
||||
View All Projects
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Portfolio;
|
||||
115
frontend/src/components/Skills.tsx
Normal file
115
frontend/src/components/Skills.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import { Briefcase, Code, Database, Palette, Smartphone, Zap } from 'lucide-react';
|
||||
|
||||
const Skills: React.FC = () => {
|
||||
const experience = [
|
||||
{
|
||||
title: 'Senior Full-Stack Developer',
|
||||
company: 'Tech Innovations Inc.',
|
||||
period: '2022 - Present',
|
||||
description: 'Leading development of scalable web applications using React, Node.js, and cloud technologies.'
|
||||
},
|
||||
{
|
||||
title: 'Frontend Developer',
|
||||
company: 'Digital Solutions Ltd.',
|
||||
period: '2020 - 2022',
|
||||
description: 'Built responsive user interfaces and improved performance for e-commerce platforms.'
|
||||
},
|
||||
{
|
||||
title: 'Junior Developer',
|
||||
company: 'StartupXYZ',
|
||||
period: '2019 - 2020',
|
||||
description: 'Developed mobile applications and contributed to backend API development.'
|
||||
}
|
||||
];
|
||||
|
||||
const specificSkills = [
|
||||
{ name: 'React', level: 95, icon: Code },
|
||||
{ name: 'TypeScript', level: 90, icon: Code },
|
||||
{ name: 'Node.js', level: 85, icon: Database },
|
||||
{ name: 'Python', level: 80, icon: Code },
|
||||
{ name: 'UI/UX Design', level: 75, icon: Palette },
|
||||
{ name: 'Mobile Development', level: 70, icon: Smartphone }
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-20 bg-background-light">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl font-bold text-text sm:text-4xl">
|
||||
Experience & Skills
|
||||
</h2>
|
||||
<p className="mt-4 text-lg text-lines max-w-2xl mx-auto">
|
||||
A comprehensive overview of my professional journey and technical expertise
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-16 grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
{/* Experience Section */}
|
||||
<div>
|
||||
<div className="flex items-center mb-8">
|
||||
<div className="p-3 bg-other rounded-lg mr-4">
|
||||
<Briefcase className="h-6 w-6 text-background" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-text">Experience</h3>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{experience.map((exp, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-background rounded-lg p-6 border border-lines"
|
||||
>
|
||||
<h4 className="text-lg font-semibold text-text mb-1">
|
||||
{exp.title}
|
||||
</h4>
|
||||
<p className="text-other font-medium mb-2">
|
||||
{exp.company} • {exp.period}
|
||||
</p>
|
||||
<p className="text-lines text-sm">
|
||||
{exp.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Specific Skills Section */}
|
||||
<div>
|
||||
<div className="flex items-center mb-8">
|
||||
<div className="p-3 bg-boxes rounded-lg mr-4">
|
||||
<Zap className="h-6 w-6 text-text" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-text">Specific Skills</h3>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
{specificSkills.map((skill, index) => {
|
||||
const IconComponent = skill.icon;
|
||||
return (
|
||||
<div key={skill.name} className="bg-background rounded-lg p-6 shadow-md border border-lines">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center">
|
||||
<div className="p-2 bg-boxes rounded-lg mr-3">
|
||||
<IconComponent className="h-5 w-5 text-other" />
|
||||
</div>
|
||||
<span className="font-semibold text-text">{skill.name}</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-lines">{skill.level}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-background-light rounded-full h-2">
|
||||
<div
|
||||
className="bg-other h-2 rounded-full transition-all duration-1000 ease-out"
|
||||
style={{ width: `${skill.level}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Skills;
|
||||
100
frontend/src/components/TradingGraph.tsx
Normal file
100
frontend/src/components/TradingGraph.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
import { TrendingUp, BarChart3 } from 'lucide-react';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
|
||||
// Placeholder data - replace with actual Trading212 API data
|
||||
const data = [
|
||||
{ date: '2024-01', value: 1000 },
|
||||
{ date: '2024-02', value: 1200 },
|
||||
{ date: '2024-03', value: 1100 },
|
||||
{ date: '2024-04', value: 1400 },
|
||||
{ date: '2024-05', value: 1300 },
|
||||
{ date: '2024-06', value: 1600 }
|
||||
];
|
||||
|
||||
const TradingGraph: React.FC = () => {
|
||||
return (
|
||||
<section className="py-16 bg-background-light">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold text-text sm:text-4xl">
|
||||
Trading Performance
|
||||
</h2>
|
||||
<p className="mt-4 text-lg text-lines max-w-2xl mx-auto">
|
||||
Real-time insights from Trading212 portfolio tracking
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-background rounded-xl shadow-lg p-6 border border-lines">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 bg-other rounded-lg">
|
||||
<BarChart3 className="h-6 w-6 text-background" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-text">Portfolio Value</h3>
|
||||
<p className="text-sm text-lines">Trading212 Integration</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-text">$1,600</div>
|
||||
<div className="flex items-center text-sm text-other">
|
||||
<TrendingUp className="h-4 w-4 mr-1" />
|
||||
+12.5% this month
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="var(--c-lines)" />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
stroke="var(--c-lines)"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="var(--c-lines)"
|
||||
fontSize={12}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => `$${value}`}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'var(--c-background-light)',
|
||||
border: '1px solid var(--c-lines)',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
|
||||
color: 'var(--c-text)'
|
||||
}}
|
||||
labelStyle={{ color: 'var(--c-text)' }}
|
||||
formatter={(value: number) => [`$${value}`, 'Value']}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke="var(--c-other)"
|
||||
strokeWidth={3}
|
||||
dot={{ fill: '#c026d3', strokeWidth: 2, r: 4 }}
|
||||
activeDot={{ r: 6, stroke: '#c026d3', strokeWidth: 2, fill: '#ffffff' }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-slate-500">
|
||||
API integration pending - displaying sample data
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default TradingGraph;
|
||||
83
frontend/src/components/WebsiteScreenshots.tsx
Normal file
83
frontend/src/components/WebsiteScreenshots.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
|
||||
const WebsiteScreenshots: React.FC = () => {
|
||||
const websites = [
|
||||
{
|
||||
title: 'E-Commerce Platform',
|
||||
image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
|
||||
url: '#',
|
||||
description: 'Modern online store with seamless checkout'
|
||||
},
|
||||
{
|
||||
title: 'Portfolio Website',
|
||||
image: 'https://images.unsplash.com/photo-1467232004584-a241de8bcf5d?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
|
||||
url: '#',
|
||||
description: 'Creative showcase for digital artists'
|
||||
},
|
||||
{
|
||||
title: 'Business Landing Page',
|
||||
image: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
|
||||
url: '#',
|
||||
description: 'Professional B2B service presentation'
|
||||
},
|
||||
{
|
||||
title: 'Blog Platform',
|
||||
image: 'https://images.unsplash.com/photo-1486312338219-ce68e2c6f44d?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
|
||||
url: '#',
|
||||
description: 'Content management system for writers'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-16 bg-background-light">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl font-bold text-text sm:text-4xl">
|
||||
Website Screenshots
|
||||
</h2>
|
||||
<p className="mt-4 text-lg text-lines max-w-2xl mx-auto">
|
||||
A glimpse of recent web development projects and designs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{websites.map((website, index) => (
|
||||
<div
|
||||
key={website.title}
|
||||
className="group bg-background rounded-lg shadow-md hover:shadow-lg transition-all duration-300 transform hover:-translate-y-1 border border-lines overflow-hidden"
|
||||
>
|
||||
<div className="relative overflow-hidden">
|
||||
<img
|
||||
className="w-full h-32 object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
src={website.image}
|
||||
alt={`${website.title} screenshot`}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background/80 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex items-end justify-end p-2">
|
||||
<a
|
||||
href={website.url}
|
||||
className="p-2 bg-boxes rounded-full hover:bg-other transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-lines focus:ring-offset-2"
|
||||
aria-label={`View ${website.title}`}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4 text-text" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<h3 className="text-sm font-semibold text-text mb-1">
|
||||
{website.title}
|
||||
</h3>
|
||||
<p className="text-xs text-lines">
|
||||
{website.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebsiteScreenshots;
|
||||
@@ -1,90 +0,0 @@
|
||||
import React, { useEffect, useRef } from "react"
|
||||
import styles from "./drone.module.css"
|
||||
|
||||
export default function Drone() {
|
||||
const videoRef = useRef<HTMLVideoElement | null>(null)
|
||||
const sourceRef = useRef<HTMLSourceElement | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
function setVideoDroneQuality() {
|
||||
if (!sourceRef.current || !videoRef.current) return
|
||||
|
||||
const videoSources = {
|
||||
fullHD: "static/home/video/drone-background-video-1080p.mp4", // For desktops (1920x1080)
|
||||
hd: "static/home/video/drone-background-video-720p.mp4", // For tablets/smaller screens (1280x720)
|
||||
lowRes: "static/home/video/drone-background-video-480p.mp4" // For mobile devices or low performance (854x480)
|
||||
}
|
||||
|
||||
const screenWidth = window.innerWidth
|
||||
|
||||
// Pick appropriate source
|
||||
if (screenWidth >= 1920) {
|
||||
sourceRef.current.src =
|
||||
"https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.fullHD
|
||||
} else if (screenWidth >= 1280) {
|
||||
sourceRef.current.src =
|
||||
"https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.hd
|
||||
} else {
|
||||
sourceRef.current.src =
|
||||
"https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.lowRes
|
||||
}
|
||||
|
||||
// Reload video
|
||||
videoRef.current.load()
|
||||
console.log("Drone video set!")
|
||||
}
|
||||
|
||||
// Run once on mount
|
||||
setVideoDroneQuality()
|
||||
|
||||
// Optional: rerun on resize
|
||||
window.addEventListener("resize", setVideoDroneQuality)
|
||||
return () => {
|
||||
window.removeEventListener("resize", setVideoDroneQuality)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={`${styles.drone}`}>
|
||||
<video
|
||||
ref={videoRef}
|
||||
id="drone-video"
|
||||
className={styles.videoBackground}
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
>
|
||||
<source ref={sourceRef} id="video-source" type="video/mp4" />
|
||||
Your browser does not support video.
|
||||
</video>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h1>Letecké záběry, co zaujmou</h1>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h2>Oprávnění</h2>
|
||||
<p>Oprávnění A1/A2/A3 + radiostanice. Bezpečný provoz i v omezených zónách, povolení zajistím.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Cena</h2>
|
||||
<p>Paušál 3 000 Kč. Ostrava zdarma; mimo 10 Kč/km. Cena se může lišit dle povolení.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Výstup</h2>
|
||||
<p>Krátký sestřih nebo surové záběry — podle potřeby.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div>
|
||||
<a href="#contacts">Zájem?</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap');
|
||||
|
||||
|
||||
|
||||
|
||||
.drone{
|
||||
margin-top: -4em;
|
||||
font-style: normal;
|
||||
|
||||
width: 100%;
|
||||
position: relative;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.drone .videoBackground {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
object-fit: cover;
|
||||
z-index: -1;
|
||||
|
||||
clip-path: polygon(0 3%, 15% 0, 30% 7%, 42% 3%, 61% 1%, 82% 5%, 100% 1%, 100% 94%, 82% 100%, 65% 96%, 47% 99%, 30% 90%, 14% 98%, 0 94%);
|
||||
}
|
||||
|
||||
|
||||
.drone article{
|
||||
padding: 5em;
|
||||
|
||||
display: flex;
|
||||
|
||||
border-radius: 2em;
|
||||
padding: 3em;
|
||||
gap: 2em;
|
||||
|
||||
position: relative;
|
||||
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
.drone article header h1{
|
||||
font-size: 4em;
|
||||
|
||||
font-weight: 300;
|
||||
}
|
||||
.drone article header{
|
||||
flex: 1;
|
||||
}
|
||||
.drone article main{
|
||||
width: 90%;
|
||||
display: flex;
|
||||
font-size: 1em;
|
||||
/* width: 60%; */
|
||||
flex: 2;
|
||||
flex-direction: row;
|
||||
|
||||
font-weight: 400;
|
||||
|
||||
gap: 2em;
|
||||
/* flex-wrap: wrap; */
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
.drone a{
|
||||
color: white;
|
||||
}
|
||||
.drone article div{
|
||||
display: flex;
|
||||
flex: 1;
|
||||
font-size: 1.25em;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 990px) {
|
||||
.drone article header h1{
|
||||
font-size: 2.3em;
|
||||
|
||||
font-weight: 200;
|
||||
}
|
||||
.drone article header{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.drone article main{
|
||||
flex-direction: column;
|
||||
font-size: 1em;
|
||||
}
|
||||
.drone article{
|
||||
height: auto;
|
||||
}
|
||||
.drone article div{
|
||||
margin: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
.drone video{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -1,168 +0,0 @@
|
||||
.portfolio {
|
||||
margin: auto;
|
||||
margin-top: 10em;
|
||||
width: 80%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
color: white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.portfolio div .door {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #c2a67d;
|
||||
color: #5e5747;
|
||||
|
||||
border-radius: 1em;
|
||||
|
||||
transform-origin: bottom;
|
||||
transition: transform 0.5s ease-in-out;
|
||||
|
||||
transform: skew(-5deg);
|
||||
z-index: 3;
|
||||
|
||||
box-shadow: #000000 5px 5px 15px;
|
||||
|
||||
}
|
||||
.portfolio div span svg{
|
||||
font-size: 5em;
|
||||
cursor: pointer;
|
||||
animation: shake 0.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes shake {
|
||||
0% { transform: translateX(0); }
|
||||
25% { transform: translateX(-2px) rotate(-8deg); }
|
||||
50% { transform: translateX(2px) rotate(4deg); }
|
||||
75% { transform: translateX(-1px) rotate(-2deg); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
|
||||
.door i{
|
||||
color: #5e5747;
|
||||
font-size: 5em;
|
||||
display: inline-block;
|
||||
animation: shake 0.4s ease-in-out infinite;
|
||||
animation-delay: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.portfolio .door-open{
|
||||
transform: rotateX(90deg) skew(-2deg) !important;
|
||||
}
|
||||
|
||||
.portfolio>header {
|
||||
width: fit-content;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
top: -3.7em;
|
||||
left: 0;
|
||||
padding: 1em 3em;
|
||||
padding-bottom: 0;
|
||||
background-color: #cdc19c;
|
||||
color: #5e5747;
|
||||
border-top-left-radius: 1em;
|
||||
border-top-right-radius: 1em;
|
||||
}
|
||||
|
||||
.portfolio>header h1 {
|
||||
font-size: 2.5em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.portfolio>header i {
|
||||
font-size: 6em;
|
||||
}
|
||||
|
||||
.portfolio article{
|
||||
position: relative;
|
||||
}
|
||||
.portfolio article::after{
|
||||
clip-path: polygon(0% 0%, 11% 12.5%, 0% 25%, 11% 37.5%, 0% 50%, 11% 62.5%, 0% 75%, 11% 87.5%, 0% 100%, 100% 100%, 84% 87.5%, 98% 75%, 86% 62.5%, 100% 50%, 86% 37.5%, 100% 25%, 93% 12.5%, 100% 0%);
|
||||
content: "";
|
||||
bottom: 0;
|
||||
right: -2em;
|
||||
|
||||
height: 2em;
|
||||
width: 6em;
|
||||
transform: rotate(-45deg);
|
||||
|
||||
position: absolute;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.portfolio article::before{
|
||||
clip-path: polygon(0% 0%, 11% 12.5%, 0% 25%, 11% 37.5%, 0% 50%, 11% 62.5%, 0% 75%, 11% 87.5%, 0% 100%, 100% 100%, 84% 87.5%, 98% 75%, 86% 62.5%, 100% 50%, 86% 37.5%, 100% 25%, 93% 12.5%, 100% 0%);
|
||||
content: "";
|
||||
top: 0;
|
||||
left: -2em;
|
||||
|
||||
height: 2em;
|
||||
width: 6em;
|
||||
transform: rotate(-45deg);
|
||||
|
||||
position: absolute;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.portfolio article header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.portfolio div {
|
||||
width: 100%;
|
||||
padding: 3em;
|
||||
background-color: #cdc19c;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: 5em;
|
||||
|
||||
border-radius: 1em;
|
||||
border-top-left-radius: 0;
|
||||
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.portfolio div article {
|
||||
display: flex;
|
||||
border-radius: 0em;
|
||||
background-color: #9c885c;
|
||||
width: 30%;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.portfolio div article header a img {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@media only screen and (max-width: 990px) {
|
||||
.portfolio div{
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.portfolio div article{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import React, { useState } from "react"
|
||||
import styles from "./Portfolio.module.css"
|
||||
import { LuMousePointerClick } from "react-icons/lu";
|
||||
|
||||
interface PortfolioItem {
|
||||
href: string
|
||||
src: string
|
||||
alt: string
|
||||
// Optional per-item styling (prefer Tailwind utility classes in className/imgClassName)
|
||||
className?: string
|
||||
imgClassName?: string
|
||||
style?: React.CSSProperties
|
||||
imgStyle?: React.CSSProperties
|
||||
}
|
||||
|
||||
const portfolioItems: PortfolioItem[] = [
|
||||
{
|
||||
href: "https://davo1.cz",
|
||||
src: "/portfolio/davo1.png",
|
||||
alt: "davo1.cz logo",
|
||||
imgClassName: "bg-black rounded-lg p-4",
|
||||
//className: "bg-white/5 rounded-lg p-4",
|
||||
},
|
||||
{
|
||||
href: "https://perlica.cz",
|
||||
src: "/portfolio/perlica.png",
|
||||
alt: "Perlica logo",
|
||||
imgClassName: "rounded-lg",
|
||||
// imgClassName: "max-h-12",
|
||||
},
|
||||
{
|
||||
href: "http://epinger2.cz",
|
||||
src: "/portfolio/epinger.png",
|
||||
alt: "Epinger2 logo",
|
||||
imgClassName: "bg-white rounded-lg",
|
||||
// imgClassName: "max-h-12",
|
||||
},
|
||||
]
|
||||
|
||||
export default function Portfolio() {
|
||||
const [doorOpen, setDoorOpen] = useState(false)
|
||||
|
||||
const toggleDoor = () => setDoorOpen((prev) => !prev)
|
||||
|
||||
return (
|
||||
<div className={styles.portfolio} id="portfolio">
|
||||
<header>
|
||||
<h1>Portfolio</h1>
|
||||
</header>
|
||||
|
||||
<div>
|
||||
|
||||
<span
|
||||
className={
|
||||
doorOpen
|
||||
? `${styles.door} ${styles["door-open"]}`
|
||||
: styles.door
|
||||
}
|
||||
onClick={toggleDoor}
|
||||
>
|
||||
<LuMousePointerClick/>
|
||||
</span>
|
||||
|
||||
{portfolioItems.map((item, index) => (
|
||||
<article
|
||||
key={index}
|
||||
className={`${styles.article} ${item.className ?? ""}`}
|
||||
style={item.style}
|
||||
>
|
||||
<header>
|
||||
<a href={item.href} target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
src={item.src}
|
||||
alt={item.alt}
|
||||
className={item.imgClassName}
|
||||
style={item.imgStyle}
|
||||
/>
|
||||
</a>
|
||||
</header>
|
||||
<main></main>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB |
@@ -1,304 +0,0 @@
|
||||
nav{
|
||||
padding: 1.1em;
|
||||
|
||||
font-family: "Roboto Mono", monospace;
|
||||
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0; /* required */
|
||||
|
||||
transition: top 1s ease-in-out, border-radius 1s ease-in-out;
|
||||
|
||||
|
||||
|
||||
z-index: 5;
|
||||
padding-left: 2em;
|
||||
padding-right: 2em;
|
||||
width: max-content;
|
||||
|
||||
background: var(--c-boxes);
|
||||
/*background: -moz-linear-gradient(117deg, rgba(34,34,34,1) 0%, rgba(59,54,54,1) 100%);
|
||||
background: -webkit-linear-gradient(117deg, rgba(34,34,34,1) 0%, rgba(59,54,54,1) 100%);
|
||||
background: linear-gradient(117deg, rgba(34,34,34,1) 0%, rgba(59,54,54,1) 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#222222",endColorstr="#3b3636",GradientType=1);*/
|
||||
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
|
||||
margin: auto;
|
||||
|
||||
border-radius: 2em;
|
||||
}
|
||||
nav.isSticky-nav{
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
nav ul #nav-logo{
|
||||
border-right: 0.2em solid var(--c-lines);
|
||||
}
|
||||
/* Add class alias for logo used in TSX */
|
||||
.logo {
|
||||
border-right: 0.2em solid var(--c-lines);
|
||||
}
|
||||
nav ul #nav-logo span{
|
||||
line-height: 0.75;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
nav a{
|
||||
color: #fff;
|
||||
transition: color 1s;
|
||||
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
}
|
||||
nav a:hover{
|
||||
color: #fff;
|
||||
}
|
||||
/* Unify link/summary layout to prevent distortion */
|
||||
nav a,
|
||||
nav summary {
|
||||
color: #fff;
|
||||
transition: color 1s;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
display: inline-block; /* ensure consistent inline sizing */
|
||||
vertical-align: middle; /* align with neighbors */
|
||||
padding: 0; /* keep padding controlled by li */
|
||||
}
|
||||
|
||||
nav a::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: #fff;
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
nav a:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
nav summary:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* underline effect shared for links and summary */
|
||||
nav a::before,
|
||||
nav summary::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background-color: #fff;
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
nav a:hover::before,
|
||||
nav summary:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
/* Submenu support */
|
||||
.hasSubmenu {
|
||||
position: relative;
|
||||
vertical-align: middle; /* align with other inline items */
|
||||
}
|
||||
|
||||
/* Keep details inline to avoid breaking the first row flow */
|
||||
.hasSubmenu details {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Ensure "Services" and caret stay on the same line */
|
||||
.hasSubmenu details > summary {
|
||||
display: inline-flex; /* horizontal layout */
|
||||
align-items: center; /* vertical alignment */
|
||||
gap: 0.5em; /* space between text and icon */
|
||||
white-space: nowrap; /* prevent wrapping */
|
||||
}
|
||||
|
||||
/* Hide native disclosure icon/marker on summary */
|
||||
.hasSubmenu details > summary {
|
||||
list-style: none;
|
||||
outline: none;
|
||||
}
|
||||
.hasSubmenu details > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.hasSubmenu details > summary::marker {
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* Reusable caret for submenu triggers */
|
||||
.caret {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
/* Rotate caret when submenu is open */
|
||||
.hasSubmenu details[open] .caret {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Submenu box: place directly under nav with a tiny gap (no overlap) */
|
||||
.submenu {
|
||||
list-style: none;
|
||||
margin: 1em 0;
|
||||
padding: 0.5em 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(100% + 0.25em);
|
||||
display: none;
|
||||
background: var(--c-background-light);
|
||||
border: 1px solid var(--c-lines);
|
||||
border-radius: 0.75em;
|
||||
min-width: max-content;
|
||||
text-align: left;
|
||||
z-index: 10;
|
||||
}
|
||||
.submenu li {
|
||||
display: block;
|
||||
padding: 0;
|
||||
}
|
||||
.submenu a {
|
||||
display: inline-block;
|
||||
padding: 0; /* remove padding so underline equals text width */
|
||||
margin: 0.35em 0; /* spacing without affecting underline width */
|
||||
}
|
||||
|
||||
/* Show submenu when open */
|
||||
.hasSubmenu details[open] .submenu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Hamburger toggle class (used by TSX) */
|
||||
.toggle {
|
||||
display: none;
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
.toggleRotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Bridge TSX classnames to existing rules */
|
||||
.navList {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.navList li {
|
||||
display: inline;
|
||||
padding: 0 3em;
|
||||
}
|
||||
.navList li a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
display: inline;
|
||||
padding: 0 3em;
|
||||
}
|
||||
|
||||
nav ul li a {
|
||||
text-decoration: none;
|
||||
}
|
||||
#toggle-nav{
|
||||
display: none;
|
||||
|
||||
-webkit-transition: transform 0.5s ease;
|
||||
-moz-transition: transform 0.5s ease;
|
||||
-o-transition: transform 0.5s ease;
|
||||
-ms-transition: transform 0.5s ease;
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
.toggle-nav-rotated {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
.nav-open{
|
||||
max-height: 20em;
|
||||
}
|
||||
@media only screen and (max-width: 990px){
|
||||
#toggle-nav{
|
||||
margin-top: 0.25em;
|
||||
margin-left: 0.75em;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
display: block;
|
||||
font-size: 2em;
|
||||
}
|
||||
nav{
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 1em;
|
||||
border-bottom-right-radius: 1em;
|
||||
overflow: hidden;
|
||||
}
|
||||
nav ul {
|
||||
margin-top: 1em;
|
||||
gap: 2em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
-webkit-transition: max-height 1s ease;
|
||||
-moz-transition: max-height 1s ease;
|
||||
-o-transition: max-height 1s ease;
|
||||
-ms-transition: max-height 1s ease;
|
||||
transition: max-height 1s ease;
|
||||
|
||||
max-height: 2em;
|
||||
}
|
||||
/* When TSX adds styles.open to the UL, expand it */
|
||||
.open {
|
||||
max-height: 20em;
|
||||
}
|
||||
|
||||
nav ul:last-child{
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
nav ul #nav-logo {
|
||||
margin: auto;
|
||||
padding-bottom: 0.5em;
|
||||
margin-bottom: -1em;
|
||||
border-bottom: 0.2em solid var(--c-lines);
|
||||
border-right: none;
|
||||
}
|
||||
/* Show hamburger on mobile */
|
||||
.toggle {
|
||||
margin-top: 0.25em;
|
||||
margin-left: 0.75em;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
display: block;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
/* Submenu stacks inline under the parent item on mobile */
|
||||
.submenu {
|
||||
position: static;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: 0 0 0.5em 0.5em;
|
||||
min-width: unset;
|
||||
}
|
||||
.submenu a {
|
||||
display: inline-block;
|
||||
padding: 0; /* keep no padding on mobile too */
|
||||
margin: 0.25em 0.5em; /* spacing via margin */
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import React, { useState, useContext } from "react"
|
||||
import styles from "./HomeNav.module.css"
|
||||
import { FaBars, FaChevronDown } from "react-icons/fa";
|
||||
|
||||
import { UserContext } from "../../context/UserContext";
|
||||
|
||||
export default function HomeNav() {
|
||||
const [navOpen, setNavOpen] = useState(false)
|
||||
|
||||
const toggleNav = () => setNavOpen((prev) => !prev)
|
||||
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
return (
|
||||
<nav className={styles.nav}>
|
||||
<FaBars
|
||||
className={`${styles.toggle} ${navOpen ? styles.toggleRotated : ""}`}
|
||||
onClick={toggleNav}
|
||||
aria-label="Toggle navigation"
|
||||
aria-expanded={navOpen}
|
||||
/>
|
||||
|
||||
<ul className={`${styles.navList} ${navOpen ? styles.open : ""}`}>
|
||||
<li id="nav-logo" className={styles.logo}>
|
||||
<span>vontor.cz</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/">Home</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#portfolio">Portfolio</a>
|
||||
</li>
|
||||
<li className={styles.hasSubmenu}>
|
||||
<details>
|
||||
<summary>
|
||||
Services
|
||||
<FaChevronDown className={`${styles.caret} ml-2 inline-block`} aria-hidden="true" />
|
||||
</summary>
|
||||
<ul className={styles.submenu}>
|
||||
<li><a href="#web">Web development</a></li>
|
||||
<li><a href="#integration">Integrations</a></li>
|
||||
<li><a href="#support">Support</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#contactme-form">Contact me</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user