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

View File

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

View File

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