upgrade
This commit is contained in:
@@ -1,149 +0,0 @@
|
||||
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;
|
||||
@@ -1,99 +0,0 @@
|
||||
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;
|
||||
@@ -1,127 +0,0 @@
|
||||
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;
|
||||
40
frontend/src/components/Footer/footer.module.css
Normal file
40
frontend/src/components/Footer/footer.module.css
Normal file
@@ -0,0 +1,40 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
39
frontend/src/components/Footer/footer.tsx
Normal file
39
frontend/src/components/Footer/footer.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { FaGithub, FaInstagram, FaYoutube, FaLinkedin, FaSteam, FaXTwitter } from "react-icons/fa6";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer id="contacts" className="mt-12 bg-[var(--c-background-light)]">
|
||||
<div className="container py-8 grid gap-6 md:grid-cols-3">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-rainbow">vontor.cz</h3>
|
||||
<p className="text-sm text-[var(--c-lines)] mt-2">© 2025 Vontor.cz</p>
|
||||
</div>
|
||||
|
||||
<address className="not-italic text-sm">
|
||||
<div><b>David Bruno Vontor</b></div>
|
||||
<div className="mt-2">Tel.: <a className="hover:text-[var(--c-other)]" href="tel:+420605512624">+420 605 512 624</a></div>
|
||||
<div>E-mail: <a className="hover:text-[var(--c-other)]" href="mailto:brunovontor@gmail.com">brunovontor@gmail.com</a></div>
|
||||
<div className="mt-1">IČO: <a className="hover:text-[var(--c-other)]" href="https://www.rzp.cz/verejne-udaje/cs/udaje/vyber-subjektu;ico=21613109;" target="_blank" rel="noopener noreferrer">21613109</a></div>
|
||||
</address>
|
||||
|
||||
<div className="flex items-center gap-4 text-2xl justify-start md:justify-end">
|
||||
<Social href="https://github.com/Brunobrno" label="GitHub"><FaGithub /></Social>
|
||||
<Social href="https://www.instagram.com/brunovontor/" label="Instagram"><FaInstagram /></Social>
|
||||
<Social href="https://twitter.com/BVontor" label="X / Twitter"><FaXTwitter /></Social>
|
||||
<Social href="https://www.youtube.com/@brunovontor" label="YouTube"><FaYoutube /></Social>
|
||||
<Social href="https://www.linkedin.com/in/brunovontor/" label="LinkedIn"><FaLinkedin /></Social>
|
||||
<Social href="https://steamcommunity.com/id/Brunobrno/" label="Steam"><FaSteam /></Social>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
function Social({ href, label, children }: { href: string; label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<a href={href} target="_blank" rel="noreferrer" aria-label={label}
|
||||
className="rounded p-2 transition-transform duration-200 hover:scale-110 hover:text-[var(--c-other)]">
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
85
frontend/src/components/Forms/ContactMe/ContactMeForm.tsx
Normal file
85
frontend/src/components/Forms/ContactMe/ContactMeForm.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
145
frontend/src/components/Forms/ContactMe/contact-me.module.css
Normal file
145
frontend/src/components/Forms/ContactMe/contact-me.module.css
Normal file
@@ -0,0 +1,145 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
BIN
frontend/src/components/Forms/ContactMe/readme.png
Normal file
BIN
frontend/src/components/Forms/ContactMe/readme.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
@@ -1,186 +0,0 @@
|
||||
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;
|
||||
@@ -1,59 +0,0 @@
|
||||
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;
|
||||
@@ -1,110 +0,0 @@
|
||||
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;
|
||||
@@ -1,115 +0,0 @@
|
||||
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;
|
||||
@@ -1,100 +0,0 @@
|
||||
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;
|
||||
@@ -1,83 +0,0 @@
|
||||
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;
|
||||
90
frontend/src/components/ads/Drone/Drone.tsx
Normal file
90
frontend/src/components/ads/Drone/Drone.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { 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>
|
||||
)
|
||||
}
|
||||
103
frontend/src/components/ads/Drone/drone.module.css
Normal file
103
frontend/src/components/ads/Drone/drone.module.css
Normal file
@@ -0,0 +1,103 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
BIN
frontend/src/components/ads/Drone/readme.png
Normal file
BIN
frontend/src/components/ads/Drone/readme.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
168
frontend/src/components/ads/Portfolio/Portfolio.module.css
Normal file
168
frontend/src/components/ads/Portfolio/Portfolio.module.css
Normal file
@@ -0,0 +1,168 @@
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
86
frontend/src/components/ads/Portfolio/Portfolio.tsx
Normal file
86
frontend/src/components/ads/Portfolio/Portfolio.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
BIN
frontend/src/components/ads/Portfolio/readme.png
Normal file
BIN
frontend/src/components/ads/Portfolio/readme.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
0
frontend/src/components/auth/LogOut.tsx
Normal file
0
frontend/src/components/auth/LogOut.tsx
Normal file
0
frontend/src/components/auth/LoginForm.tsx
Normal file
0
frontend/src/components/auth/LoginForm.tsx
Normal file
33
frontend/src/components/common/Modal.tsx
Normal file
33
frontend/src/components/common/Modal.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useEffect } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function Modal({ open, onClose, title, children }: Props) {
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
|
||||
if (open) document.addEventListener('keydown', onKey);
|
||||
return () => document.removeEventListener('keydown', onKey);
|
||||
}, [open, onClose]);
|
||||
|
||||
if (!open) return null;
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100]">
|
||||
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
|
||||
<div role="dialog" aria-modal="true" className="absolute inset-0 flex items-center justify-center p-4">
|
||||
<div className="glass max-w-3xl w-full p-4 md:p-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg md:text-xl font-semibold">{title}</h3>
|
||||
<button onClick={onClose} aria-label="Close" className="px-2 py-1 glow">✕</button>
|
||||
</div>
|
||||
<div className="prose prose-invert max-w-none">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
frontend/src/components/common/ScrollToTop.tsx
Normal file
10
frontend/src/components/common/ScrollToTop.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useEffect } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
export default function ScrollToTop() {
|
||||
const { pathname } = useLocation();
|
||||
useEffect(() => {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
||||
}, [pathname]);
|
||||
return null;
|
||||
}
|
||||
34
frontend/src/components/donate/DonationShop.tsx
Normal file
34
frontend/src/components/donate/DonationShop.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { FaCoffee, FaBatteryFull, FaMicrochip } from "react-icons/fa";
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
type Tier = { id: string; title: string; price: number; desc: string; color: string; icon: ReactNode };
|
||||
|
||||
const tiers: Tier[] = [
|
||||
{ id: 'coffee', title: 'Coffee', price: 3, desc: 'Fuel late-night coding sessions.', color: '#d97706', icon: <FaCoffee /> },
|
||||
{ id: 'battery', title: 'Drone Battery', price: 30, desc: 'Extend aerial filming time.', color: '#16a34a', icon: <FaBatteryFull /> },
|
||||
{ id: 'gpu', title: 'GPU Upgrade', price: 200, desc: 'Speed up rendering and ML tasks.', color: '#6366f1', icon: <FaMicrochip /> },
|
||||
];
|
||||
|
||||
export default function DonationShop() {
|
||||
return (
|
||||
<section id="shop" className="section">
|
||||
<div className="container">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-2 text-rainbow">Support My Creative Journey</h2>
|
||||
<p className="text-gray-300">Instead of buying products, consider donating to support my creative journey.</p>
|
||||
<div className="grid md:grid-cols-3 gap-6 mt-6">
|
||||
{tiers.map(t => (
|
||||
<div key={t.id} className="card p-5 flex flex-col">
|
||||
<div className="text-5xl mb-3" style={{ color: t.color }}>{t.icon}</div>
|
||||
<h3 className="text-xl font-semibold" style={{ color: t.color }}>{t.title}</h3>
|
||||
<p className="text-[var(--c-lines)] mt-1">{t.desc}</p>
|
||||
<div className="mt-auto flex items-center justify-between pt-4">
|
||||
<span className="text-2xl font-bold" style={{ color: t.color }}>${t.price}</span>
|
||||
<button className="px-4 py-2 rounded-lg font-semibold text-white bg-gradient-to-r from-fuchsia-600 to-orange-500 hover:shadow-lg hover:shadow-fuchsia-600/25 transition-all" onClick={() => alert(`Thank you for supporting with ${t.title}!`)}>Donate</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
62
frontend/src/components/hero/HeroCarousel.tsx
Normal file
62
frontend/src/components/hero/HeroCarousel.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const videos = ["dQw4w9WgXcQ", "M7lc1UVf-VE", "aqz-KE-bpKQ"]; // placeholder IDs
|
||||
|
||||
export default function HeroCarousel() {
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setIndex(i => (i + 1) % videos.length), 10000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section id="home" className="relative min-h-[80vh] md:min-h-[85vh] flex items-center justify-center overflow-hidden">
|
||||
{/* Background Gradient and animated glows */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-slate-800 via-slate-900 to-black -z-10" />
|
||||
<div className="absolute inset-0 -z-10 pointer-events-none">
|
||||
<div className="absolute top-1/4 left-1/4 w-64 h-64 bg-fuchsia-600/10 rounded-full blur-3xl animate-pulse" />
|
||||
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-orange-500/10 rounded-full blur-3xl animate-pulse delay-1000" />
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-80 h-80 bg-violet-400/10 rounded-full blur-3xl animate-pulse delay-2000" />
|
||||
</div>
|
||||
|
||||
<div className="relative container mx-auto px-4 py-10 grid lg:grid-cols-2 gap-10 items-center">
|
||||
{/* Text */}
|
||||
<div className="text-center lg:text-left">
|
||||
<h1 className="text-4xl md:text-6xl font-bold mb-4 leading-tight">
|
||||
<span className="text-rainbow">Welcome to</span><br />
|
||||
<span className="text-white">Vontor.cz</span>
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl text-gray-300 mb-6">Creative Tech & Design by <span className="text-fuchsia-600 font-semibold">Bruno Vontor</span></p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start">
|
||||
<a href="#portfolio" className="px-8 py-3 bg-gradient-to-r from-fuchsia-600 to-orange-500 text-white font-semibold rounded-lg hover:shadow-lg hover:shadow-fuchsia-600/25 transition-all duration-300 transform hover:scale-105">View Portfolio</a>
|
||||
<a href="#contact" className="px-8 py-3 border-2 border-violet-400 text-violet-400 font-semibold rounded-lg hover:bg-violet-400 hover:text-white transition-all duration-300">Get In Touch</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Video carousel */}
|
||||
<div className="relative">
|
||||
<div className="relative aspect-video bg-slate-800 rounded-xl overflow-hidden shadow-2xl">
|
||||
{videos.map((v,i) => (
|
||||
<iframe
|
||||
key={v}
|
||||
src={`https://www.youtube.com/embed/${v}?autoplay=${i===index?1:0}&mute=1&loop=1&playlist=${v}`}
|
||||
title={`Slide ${i+1}`}
|
||||
className={`absolute inset-0 w-full h-full transition-opacity duration-700 ${i===index? 'opacity-100':'opacity-0'}`}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
))}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none" />
|
||||
</div>
|
||||
{/* Indicators */}
|
||||
<div className="flex justify-center mt-4 space-x-2">
|
||||
{videos.map((_,i) => (
|
||||
<button key={i} onClick={()=>setIndex(i)} aria-label={`Go to slide ${i+1}`} className={`w-3 h-3 rounded-full transition-all duration-300 ${i===index? 'bg-fuchsia-600':'bg-gray-600 hover:bg-gray-400'}`} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
18
frontend/src/components/hosting/HostingSecuritySection.tsx
Normal file
18
frontend/src/components/hosting/HostingSecuritySection.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export default function HostingSecuritySection() {
|
||||
return (
|
||||
<section id="hosting" className="py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 text-rainbow">Hosting & Protection</h2>
|
||||
<div className="card p-6 md:p-8">
|
||||
<p className="text-gray-300">We host our applications ourselves, which reduces hosting costs as projects scale.</p>
|
||||
<p className="text-gray-300 mt-2">All websites are protected by Cloudflare and optimized for performance.</p>
|
||||
<div className="grid grid-cols-3 md:grid-cols-6 gap-4 text-center text-sm mt-6">
|
||||
{['Server', 'Cloudflare', 'Docker', 'SSL', 'Monitoring', 'Scaling'].map(item => (
|
||||
<div key={item} className="p-3 rounded-lg bg-slate-700/50 text-violet-300 transition-transform duration-200 hover:scale-105">{item}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
304
frontend/src/components/navbar/HomeNav.module.css
Normal file
304
frontend/src/components/navbar/HomeNav.module.css
Normal file
@@ -0,0 +1,304 @@
|
||||
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 */
|
||||
}
|
||||
}
|
||||
48
frontend/src/components/navbar/HomeNav.tsx
Normal file
48
frontend/src/components/navbar/HomeNav.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useState } from "react"
|
||||
import styles from "./HomeNav.module.css"
|
||||
import { FaBars, FaChevronDown } from "react-icons/fa";
|
||||
|
||||
export default function HomeNav() {
|
||||
const [navOpen, setNavOpen] = useState(false)
|
||||
|
||||
const toggleNav = () => setNavOpen((prev) => !prev)
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
73
frontend/src/components/navbar/SiteNav.tsx
Normal file
73
frontend/src/components/navbar/SiteNav.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { FaBars, FaTimes, FaChevronDown } from "react-icons/fa";
|
||||
|
||||
/* Responsive sticky navigation bar using theme variables */
|
||||
export default function SiteNav() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [servicesOpen, setServicesOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 50);
|
||||
onScroll();
|
||||
window.addEventListener('scroll', onScroll);
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header className={`sticky top-0 z-50 transition-all ${scrolled ? 'bg-slate-800/95 backdrop-blur-md shadow-lg' : 'bg-transparent'}`}>
|
||||
<nav className="relative container mx-auto px-4 flex items-center justify-between h-16 text-[var(--c-text)] font-medium">
|
||||
<div className="text-xl tracking-wide font-semibold">
|
||||
<NavLink to="/" className="inline-block px-2 py-1 rounded nav-item text-rainbow font-bold">vontor.cz</NavLink>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Toggle navigation"
|
||||
onClick={() => setOpen(o => !o)}
|
||||
className="md:hidden p-2 rounded glow focus:outline-none"
|
||||
>
|
||||
{open ? <FaTimes /> : <FaBars />}
|
||||
</button>
|
||||
<ul className={`md:flex md:items-center md:gap-8 md:static absolute left-0 w-full md:w-auto transition-all duration-300 ${open ? 'top-16 bg-slate-800/95 pb-6 rounded-lg backdrop-blur-md' : 'top-[-500px]'}`}>
|
||||
<li className="flex"><NavLink to="/" onClick={()=>setOpen(false)} className={linkCls}>Home</NavLink></li>
|
||||
<li className="flex"><NavLink to="/portfolio" onClick={()=>setOpen(false)} className={linkCls}>Portfolio</NavLink></li>
|
||||
<li className="flex"><NavLink to="/skills" onClick={()=>setOpen(false)} className={linkCls}>Skills</NavLink></li>
|
||||
<li className="flex"><NavLink to="/hosting-security" onClick={()=>setOpen(false)} className={linkCls}>Hosting & Security</NavLink></li>
|
||||
<li className="flex"><NavLink to="/donate" onClick={()=>setOpen(false)} className={linkCls}>Donate / Shop</NavLink></li>
|
||||
<li className="flex"><NavLink to="/contact" onClick={()=>setOpen(false)} className={linkCls}>Contact</NavLink></li>
|
||||
<li className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setServicesOpen(s => !s)}
|
||||
className={`nav-item px-3 py-2 flex items-center gap-1`}
|
||||
aria-haspopup="true" aria-expanded={servicesOpen}
|
||||
>
|
||||
More <FaChevronDown className={`transition-transform ${servicesOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
{/* Mobile inline dropdown */}
|
||||
<div className={`md:hidden w-full mt-2 ${servicesOpen ? 'block' : 'hidden'}`}>
|
||||
<ul className="space-y-2 text-sm glass p-4">
|
||||
<li><a href="#live" className={`${dropdownCls} nav-item`}>Live Performance</a></li>
|
||||
<li><a href="#shop" className={`${dropdownCls} nav-item`}>Support Journey</a></li>
|
||||
<li><a href="#portfolio" className={`${dropdownCls} nav-item`}>Featured Work</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{/* Desktop offset dropdown anchored to right under nav */}
|
||||
{servicesOpen && (
|
||||
<div className="hidden md:block absolute top-full right-4 translate-y-2 min-w-56 glass p-4 shadow-xl">
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li><a href="#live" className={`${dropdownCls} nav-item`}>Live Performance</a></li>
|
||||
<li><a href="#shop" className={`${dropdownCls} nav-item`}>Support Journey</a></li>
|
||||
<li><a href="#portfolio" className={`${dropdownCls} nav-item`}>Featured Work</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
const linkCls = ({ isActive }: { isActive: boolean }) => `nav-item px-3 py-2 rounded transition-colors ${isActive ? 'active text-[var(--c-other)] font-semibold' : 'hover:text-[var(--c-other)]'}`;
|
||||
const dropdownCls = "block px-2 py-1 rounded hover:bg-[color-mix(in_hsl,var(--c-other),transparent_85%)]";
|
||||
79
frontend/src/components/portfolio/PortfolioGrid.tsx
Normal file
79
frontend/src/components/portfolio/PortfolioGrid.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { useState } from "react";
|
||||
import Modal from "../common/Modal";
|
||||
|
||||
type Project = {
|
||||
id: string;
|
||||
title: string;
|
||||
image: string; // public/ path
|
||||
description: string;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
const projects: Project[] = [
|
||||
{
|
||||
id: "perlica",
|
||||
title: "Perlica",
|
||||
image: "/portfolio/perlica.png",
|
||||
description: "E-commerce redesign with modern UI and Django backend integration.",
|
||||
link: "#",
|
||||
},
|
||||
{
|
||||
id: "epinger",
|
||||
title: "Epinger",
|
||||
image: "/portfolio/epinger.png",
|
||||
description: "Landing page with responsive layout and animation system.",
|
||||
link: "#",
|
||||
},
|
||||
{
|
||||
id: "davo1",
|
||||
title: "Davo",
|
||||
image: "/portfolio/davo1.png",
|
||||
description: "Portfolio template and component library built with Vite + Tailwind.",
|
||||
link: "#",
|
||||
},
|
||||
];
|
||||
|
||||
export default function PortfolioGrid() {
|
||||
const [active, setActive] = useState<Project | null>(null);
|
||||
return (
|
||||
<section id="portfolio" className="section">
|
||||
<div className="container">
|
||||
<h2 className="text-2xl md:text-3xl font-bold mb-2 text-rainbow">My Work</h2>
|
||||
<p className="text-[var(--c-lines)] mb-6 max-w-2xl">Selected projects combining engineering, design systems, performance optimization and infrastructure. Click a tile for details.</p>
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
{projects.map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
className="card overflow-hidden text-left"
|
||||
onClick={() => setActive(p)}
|
||||
>
|
||||
<div className="aspect-[16/10] w-full overflow-hidden">
|
||||
<img src={p.image} alt={p.title} className="w-full h-full object-cover hover:scale-105 transition-transform" />
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<h3 className="text-lg font-semibold tracking-wide text-rainbow">{p.title}</h3>
|
||||
<p className="text-xs text-[var(--c-lines)] mt-1 uppercase">View details →</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal open={!!active} onClose={() => setActive(null)} title={active?.title}>
|
||||
<div className="space-y-3">
|
||||
{active && (
|
||||
<>
|
||||
<img src={active.image} alt={active.title} className="w-full rounded" />
|
||||
<p>{active.description}</p>
|
||||
{active.link && (
|
||||
<a href={active.link} target="_blank" rel="noreferrer" className="inline-block px-4 py-2 glow border rounded">
|
||||
Visit project
|
||||
</a>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
35
frontend/src/components/skills/SkillsSection.tsx
Normal file
35
frontend/src/components/skills/SkillsSection.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
const categories: { name: string; items: string[] }[] = [
|
||||
{ name: 'Experience', items: ['Freelance projects', 'Collaborations', 'Open-source contributions'] },
|
||||
{ name: 'Backend', items: ['Django', 'Python', 'REST API', 'PostgreSQL', 'Celery', 'Docker'] },
|
||||
{ name: 'Frontend', items: ['React', 'Tailwind', 'Vite', 'TypeScript', 'ShadCN', 'Bootstrap'] },
|
||||
{ name: 'DevOps / Hosting', items: ['Nginx', 'Docker Compose', 'SSL', 'Cloudflare', 'Self-hosting'] },
|
||||
{ name: 'Other Tools', items: ['Git', 'VSCode', 'WebRTC', 'ESP32', 'Automation'] },
|
||||
];
|
||||
|
||||
export default function SkillsSection() {
|
||||
return (
|
||||
<section id="skills" className="py-20 bg-gradient-to-b from-slate-900 to-slate-800">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl font-bold mb-4 text-rainbow">Skills</h2>
|
||||
<p className="text-gray-300 max-w-2xl mx-auto">Core technologies and tools I use across backend, frontend, infrastructure and hardware.</p>
|
||||
</div>
|
||||
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{categories.map(cat => (
|
||||
<div key={cat.name} className="card p-6">
|
||||
<h3 className="font-semibold mb-4 text-rainbow tracking-wide">{cat.name}</h3>
|
||||
<ul className="space-y-2 text-sm">
|
||||
{cat.items.map(i => (
|
||||
<li key={i} className="flex items-center gap-2 text-gray-300">
|
||||
<span className="inline-block w-2 h-2 rounded-full bg-fuchsia-600" />
|
||||
<span className="group-hover:text-white transition-colors">{i}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
15
frontend/src/components/trading/TradingGraph.tsx
Normal file
15
frontend/src/components/trading/TradingGraph.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
export default function TradingGraph() {
|
||||
return (
|
||||
<section id="live" className="py-20 bg-gradient-to-b from-slate-900 to-slate-800">
|
||||
<div className="container mx-auto px-4">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-6 text-rainbow">Live Performance</h2>
|
||||
<div className="card p-4 md:p-6">
|
||||
<div className="mb-3 text-sm text-gray-400">Trading212 graph placeholder</div>
|
||||
<div className="aspect-[16/9] w-full rounded border border-slate-700 bg-black/40 grid place-items-center">
|
||||
<span className="text-gray-400">Graph will appear here</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user