Second version of my personal website. luccaromaniello.com/
css personal-website bun tailwindcss portflio design typescript code ux-ui astro javascript
at dev 156 lines 4.3 kB view raw
1--- 2import { Image } from "astro:assets"; 3import IconButton from "@components/ui/IconButton.astro"; 4import { ButtonLink } from "@components/ui"; 5import ArrowLeft from "@assets/icons/arrow-left.svg"; 6import ArrowRight from "@assets/icons/arrow-right.svg"; 7 8interface Props { 9 title: string; 10 description: string; 11 website?: { 12 href: string; 13 external?: boolean; 14 track?: boolean; 15 trackEvent?: string; 16 }; 17 repository?: string; 18 repositoryTrackEvent?: string; 19 images: { 20 src: ImageMetadata; 21 alt: string; 22 }[]; 23 even?: boolean; 24} 25 26const { 27 title, 28 description, 29 website, 30 repository, 31 repositoryTrackEvent, 32 images, 33} = Astro.props; 34const hasCarousel = images.length > 1; 35--- 36 37<div 38 class="grid grid-cols-1 gap-8 2xl:gap-0 min-h-[360px] items-center border-2 border-neutral-primary 39 rounded-lg overflow-hidden bg-white" 40> 41 <div class="flex flex-col text-center md:text-start"> 42 <div 43 class="flex flex-col md:flex-row w-full gap-4 md:gap-8 px-4 md:px-8 py-6 md:py-8 justify-between items-center" 44 > 45 <div class="flex flex-col gap-2"> 46 <div class="flex flex-row items-center justify-center md:justify-start"> 47 <h3 class="text-4xl font-bold"> 48 {title} 49 </h3> 50 <IconButton icon="externalLink" size="xs" /> 51 </div> 52 <p class="text-lg md:text-xl text-content-secondary xl:text-balance"> 53 {description} 54 </p> 55 </div> 56 <div 57 class="flex flex-wrap justify-center md:justify-start gap-4 md:gap-5 self-center md:shrink-0" 58 > 59 { 60 website && ( 61 <ButtonLink 62 icon="externalLink" 63 label="Go to website" 64 href={website.href} 65 external={website.external} 66 track={website.track} 67 trackEvent={website.trackEvent} 68 /> 69 ) 70 } 71 { 72 repository && ( 73 <ButtonLink 74 icon="github" 75 label="View repository" 76 href={repository} 77 external 78 trackEvent={repositoryTrackEvent} 79 /> 80 ) 81 } 82 </div> 83 </div> 84 85 <div 86 class="relative overflow-hidden max-h-[720px]" 87 oncontextmenu="return false" 88 data-carousel 89 > 90 <div 91 class="flex transition-transform duration-300 ease-in-out h-full" 92 data-carousel-track 93 > 94 { 95 images.map((img) => ( 96 <div class="w-full flex-shrink-0"> 97 <Image 98 src={img.src} 99 alt={img.alt} 100 class="h-full w-full object-cover" 101 /> 102 </div> 103 )) 104 } 105 </div> 106 { 107 hasCarousel && ( 108 <div class="absolute bottom-2 left-1/2 -translate-x-1/2 flex gap-2"> 109 <button 110 data-carousel-prev 111 class="w-10 h-10 flex items-center justify-center border-2 border-white text-white rounded-full font-semibold cursor-pointer hover:bg-white hover:text-black transition-colors" 112 aria-label="Previous image" 113 > 114 <ArrowLeft class="w-5 h-5" /> 115 </button> 116 <button 117 data-carousel-next 118 class="w-10 h-10 flex items-center justify-center border-2 border-white text-white rounded-full font-semibold cursor-pointer hover:bg-white hover:text-black transition-colors" 119 aria-label="Next image" 120 > 121 <ArrowRight class="w-5 h-5" /> 122 </button> 123 </div> 124 ) 125 } 126 </div> 127 </div> 128</div> 129 130<script> 131 document.querySelectorAll("[data-carousel]").forEach((carousel) => { 132 const track = carousel.querySelector( 133 "[data-carousel-track]", 134 ) as HTMLElement; 135 const prev = carousel.querySelector("[data-carousel-prev]"); 136 const next = carousel.querySelector("[data-carousel-next]"); 137 if (!track) return; 138 139 const slides = track.children.length; 140 let current = 0; 141 142 function update() { 143 track!.style.transform = `translateX(-${current * 100}%)`; 144 } 145 146 prev?.addEventListener("click", () => { 147 current = (current - 1 + slides) % slides; 148 update(); 149 }); 150 151 next?.addEventListener("click", () => { 152 current = (current + 1) % slides; 153 update(); 154 }); 155 }); 156</script>