added ImageCarousel component, added icons to action buttons, modified project route

This commit is contained in:
2026-01-13 00:15:08 -05:00
parent 20321ece21
commit 2d945f6015
5 changed files with 103 additions and 9 deletions

View File

@@ -15,6 +15,7 @@ import * as $projects_index from "./routes/projects/index.tsx";
import * as $rss_index from "./routes/rss/index.tsx";
import * as $sitemap_index from "./routes/sitemap/index.tsx";
import * as $Counter from "./islands/Counter.tsx";
import * as $ImageCarousel from "./islands/ImageCarousel.tsx";
import * as $Portal from "./islands/Portal.tsx";
import * as $ProjectCard from "./islands/ProjectCard.tsx";
import * as $ProjectModal from "./islands/ProjectModal.tsx";
@@ -37,6 +38,7 @@ const manifest = {
},
islands: {
"./islands/Counter.tsx": $Counter,
"./islands/ImageCarousel.tsx": $ImageCarousel,
"./islands/Portal.tsx": $Portal,
"./islands/ProjectCard.tsx": $ProjectCard,
"./islands/ProjectModal.tsx": $ProjectModal,

View File

@@ -0,0 +1,82 @@
import { useState } from "preact/hooks";
export const ImageCarousel = function ImageCarousel(props: ImageCarouselProps) {
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const nextImage = (e: Event) => {
e.stopPropagation();
if (props.images && props.images?.length > 0) {
const localImage = props.images;
setCurrentImageIndex((prev) => (prev + 1) % localImage.length || 0);
}
};
const prevImage = (e: Event) => {
e.stopPropagation();
if (props.images && props.images.length > 0) {
const localImage = props.images;
setCurrentImageIndex(
(prev) => (prev - 1 + localImage.length) % localImage.length,
);
}
};
return (
<div class="relative rounded overflow-hidden bg-gray-100 dark:bg-gray-800">
<div class="relative w-full h-96">
{props.images.map((image, index) => (
<img
key={index}
src={image}
alt={`screenshot ${index + 1}`}
class={`absolute inset-0 w-full h-full object-contain transition-opacity duration-500 ${
index === currentImageIndex ? "opacity-100" : "opacity-0"
}`}
/>
))}
</div>
{props.images.length > 1 && (
<>
<button
type="button"
onClick={prevImage}
class="absolute left-2 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white rounded-full w-10 h-10 flex items-center justify-center transition-all"
aria-label="Previous image"
>
</button>
<button
type="button"
onClick={nextImage}
class="absolute right-2 top-1/2 -translate-y-1/2 bg-black/50 hover:bg-black/70 text-white rounded-full w-10 h-10 flex items-center justify-center transition-all"
aria-label="Next image"
>
</button>
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2 bg-black/30 px-3 py-2 rounded-full">
{props.images.map((_, index) => (
<button
key={index}
type="button"
onClick={(e) => {
e.stopPropagation();
setCurrentImageIndex(index);
}}
class={`w-2.5 h-2.5 rounded-full transition-all ${
index === currentImageIndex
? "bg-white scale-125"
: "bg-white/50 hover:bg-white/75"
}`}
aria-label={`Go to image ${index + 1}`}
/>
))}
</div>
</>
)}
</div>
);
};
type ImageCarouselProps = {
images: Array<string>;
};

View File

@@ -1,6 +1,8 @@
import { useState } from "preact/hooks";
import { Portal } from "./Portal.tsx";
import { type ModalAction, ProjectModal } from "./ProjectModal.tsx";
import { ImageCarousel } from "./ImageCarousel.tsx";
import * as hi from "@preact-icons/hi2";
export const ProjectCard = function ProjectCard(props: ProjectProps) {
const [open, setOpen] = useState(false);
@@ -8,6 +10,7 @@ export const ProjectCard = function ProjectCard(props: ProjectProps) {
const modalButtons: Array<ModalAction> = [
{
label: "Open repository",
icon: <hi.HiCodeBracket />,
onClick: () => {
if (props.repo) globalThis.open(props.repo, "_blank");
},
@@ -31,7 +34,6 @@ export const ProjectCard = function ProjectCard(props: ProjectProps) {
}
onClick={() => {
// clicking the card (not the link) opens the modal
console.log("opened portal");
setOpen(true);
}}
>
@@ -69,6 +71,9 @@ export const ProjectCard = function ProjectCard(props: ProjectProps) {
actions={modalButtons}
>
<div class="space-y-3">
{props.images && props.images.length > 0 && (
<ImageCarousel images={props.images} />
)}
<p class="text-sm text-gray-800 dark:text-gray-200">
{props.description}
</p>
@@ -90,4 +95,5 @@ type ProjectProps = {
description?: string;
tech: string;
wip?: boolean;
images?: string[];
};

View File

@@ -1,8 +1,11 @@
import { useEffect, useRef, useState } from "preact/hooks";
import type { ComponentChildren } from "preact";
import * as hi from "jsr:@preact-icons/hi2";
export type ModalAction = {
label: string;
// deno-lint-ignore no-explicit-any
icon?: any;
onClick: () => void;
variant?: "primary" | "secondary" | "link";
};
@@ -62,6 +65,7 @@ export const ProjectModal = function ProjectModal({
...actions,
{
label: "Close",
icon: <hi.HiXCircle />,
onClick: startCloseFlow,
variant: "secondary",
},
@@ -89,7 +93,7 @@ export const ProjectModal = function ProjectModal({
const renderActionButton = (action: ModalAction) => {
const base =
"flex-1 w-full px-4 py-2 rounded-md font-semibold focus:outline-none";
"flex-1 w-full px-4 py-2 rounded-md font-semibold focus:outline-none flex items-center justify-center gap-2";
const styles =
action.variant === "primary"
? "bg-[#94e2d5] text-black hover:brightness-95 border-b-4 border-b-[#364153] shadow-xl"
@@ -104,7 +108,7 @@ export const ProjectModal = function ProjectModal({
class={`${base} ${styles}`}
type="button"
>
{action.label}
{action.icon} {action.label}
</button>
);
};

View File

@@ -10,23 +10,22 @@ interface ProjectData {
tech: string;
wip?: boolean;
created_at: string;
images: Array<string>;
}
export const handler: Handlers<ProjectData> = {
export const handler: Handlers<Array<ProjectData>> = {
async GET(_req: Request, ctx: FreshContext) {
const projectResult = await fetch(
`${Deno.env.get("BASE_URI_API")}/projects`,
);
const projectData = await projectResult.json();
return ctx.render({
projectData,
});
return ctx.render(projectData);
},
};
export default function Projects({ data }: PageProps<ProjectData>) {
const { projectData: projects } = data;
export default function Projects(props: PageProps<Array<ProjectData>>) {
const projects = props.data;
return (
<div class="space-y-12 px-10 py-8 sm:min-h-screen bg-[#313244]">
@@ -56,6 +55,7 @@ export default function Projects({ data }: PageProps<ProjectData>) {
}
tech={project.tech}
wip={project.wip ?? true}
images={project.images}
/>
);
})}