Second version of my personal website.
luccaromaniello.com/
css
personal-website
bun
tailwindcss
portflio
design
typescript
code
ux-ui
astro
javascript
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>