diff --git a/backend/public/src/datasources/projects.rs b/backend/public/src/datasources/projects.rs index ca9a0b0..41f6708 100644 --- a/backend/public/src/datasources/projects.rs +++ b/backend/public/src/datasources/projects.rs @@ -1,4 +1,4 @@ -use sqlx::{FromRow, Pool, Postgres, Row}; +use sqlx::{Pool, Postgres}; use crate::routes::projects::Project; @@ -7,7 +7,7 @@ impl ProjectsDatasource { pub async fn get_all(pool: &Pool) -> Result, sqlx::Error> { sqlx::query_as!( Project, - "SELECT project_id, title, repo, summary, tech, wip, created_at FROM projects p WHERE deleted_at IS NULL ORDER BY p.created_at DESC" + "SELECT project_id, title, repo, summary, description, tech, wip, created_at FROM projects p WHERE deleted_at IS NULL ORDER BY p.created_at DESC" ) .fetch_all(pool) .await diff --git a/backend/public/src/routes/projects.rs b/backend/public/src/routes/projects.rs index 5eb78f1..41ac46a 100644 --- a/backend/public/src/routes/projects.rs +++ b/backend/public/src/routes/projects.rs @@ -9,6 +9,7 @@ pub struct Project { pub title: String, pub repo: Option, pub summary: String, + pub description: Option, pub tech: String, pub wip: Option, #[serde(serialize_with = "serialize_datetime")] @@ -66,5 +67,3 @@ impl ProjectsRoute { } } } - - diff --git a/frontend/fresh.gen.ts b/frontend/fresh.gen.ts index e177290..cfeec90 100644 --- a/frontend/fresh.gen.ts +++ b/frontend/fresh.gen.ts @@ -15,9 +15,9 @@ 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 $Portal from "./islands/Portal.tsx"; import * as $ProjectCard from "./islands/ProjectCard.tsx"; -import * as $modal from "./islands/modal.tsx"; -import * as $portal from "./islands/portal.tsx"; +import * as $ProjectModal from "./islands/ProjectModal.tsx"; import { type Manifest } from "$fresh/server.ts"; const manifest = { @@ -37,9 +37,9 @@ const manifest = { }, islands: { "./islands/Counter.tsx": $Counter, + "./islands/Portal.tsx": $Portal, "./islands/ProjectCard.tsx": $ProjectCard, - "./islands/modal.tsx": $modal, - "./islands/portal.tsx": $portal, + "./islands/ProjectModal.tsx": $ProjectModal, }, baseUrl: import.meta.url, } satisfies Manifest; diff --git a/frontend/islands/portal.tsx b/frontend/islands/Portal.tsx similarity index 100% rename from frontend/islands/portal.tsx rename to frontend/islands/Portal.tsx diff --git a/frontend/islands/ProjectCard.tsx b/frontend/islands/ProjectCard.tsx index 5198623..c6bdbba 100644 --- a/frontend/islands/ProjectCard.tsx +++ b/frontend/islands/ProjectCard.tsx @@ -1,10 +1,20 @@ import { useState } from "preact/hooks"; -import { Portal } from "./portal.tsx"; -import { Modal } from "./modal.tsx"; +import { Portal } from "./Portal.tsx"; +import { type ModalAction, ProjectModal } from "./ProjectModal.tsx"; export const ProjectCard = function ProjectCard(props: ProjectProps) { const [open, setOpen] = useState(false); + const modalButtons: Array = [ + { + label: "Open repository", + onClick: () => { + if (props.repo) globalThis.open(props.repo, "_blank"); + }, + variant: "primary", + }, + ]; + return (
- setOpen(false)} - actions={[ - { - label: "Open repository", - onClick: () => { - if (props.repo) window.open(props.repo, "_blank"); - }, - variant: "primary", - }, - { - label: "Close", - onClick: () => setOpen(false), - variant: "secondary", - }, - ]} + actions={modalButtons} >

- {props.summary} + {props.description}

Technologies used: {props.tech}

-
+ ) : null}
@@ -90,6 +87,7 @@ type ProjectProps = { title: string; repo?: string; summary: string; + description?: string; tech: string; wip?: boolean; }; diff --git a/frontend/islands/modal.tsx b/frontend/islands/ProjectModal.tsx similarity index 72% rename from frontend/islands/modal.tsx rename to frontend/islands/ProjectModal.tsx index 816f30e..c64788c 100644 --- a/frontend/islands/modal.tsx +++ b/frontend/islands/ProjectModal.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from "preact/hooks"; import type { ComponentChildren } from "preact"; -type ModalAction = { +export type ModalAction = { label: string; onClick: () => void; variant?: "primary" | "secondary" | "link"; @@ -24,7 +24,7 @@ type ModalProps = { animationDurationMs?: number; }; -export function Modal({ +export const ProjectModal = function ProjectModal({ title, onClose, children, @@ -32,15 +32,11 @@ export function Modal({ actions, animationDurationMs = 200, }: ModalProps) { - // Controls the entrance/exit animation state. true => visible (enter), false => hidden (exit) const [isVisible, setIsVisible] = useState(false); - // Prevent double-triggering the close flow const closingRef = useRef(false); - // Hold the timeout id for cleanup const timeoutRef = useRef(null); useEffect(() => { - // Defer to next frame so initial "hidden" styles are applied before animating to visible. const raf = requestAnimationFrame(() => setIsVisible(true)); function onKey(e: KeyboardEvent) { @@ -60,52 +56,55 @@ export function Modal({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // If no actions provided, render a single Close button const footerActions: ModalAction[] = actions && actions.length > 0 - ? actions + ? [ + ...actions, + { + label: "Close", + onClick: startCloseFlow, + variant: "secondary", + }, + ] : [{ label: "Close", onClick: startCloseFlow, variant: "primary" }]; - // Start exit animation and call onClose after animationDurationMs function startCloseFlow() { if (closingRef.current) return; closingRef.current = true; setIsVisible(false); - // Wait for the CSS transition to finish before signalling parent to actually unmount - timeoutRef.current = window.setTimeout(() => { + timeoutRef.current = globalThis.setTimeout(() => { timeoutRef.current = null; onClose(); }, animationDurationMs); } - // Animation classes (enter & exit): - // - panel: transitions opacity + transform for a subtle fade + pop - // - backdrop: transitions opacity for fade const panelBase = "relative z-10 max-w-lg w-full bg-white dark:bg-[#1f2937] rounded-lg shadow-xl p-6 mx-4 transform transition-all"; - // We explicitly set the CSS transition duration inline to keep class + timeout in sync. const panelVisible = "opacity-100 translate-y-0 scale-100"; const panelHidden = "opacity-0 translate-y-2 scale-95"; - const backdropBase = "absolute inset-0 bg-black/50 backdrop-blur-sm transition-opacity"; const backdropVisible = "opacity-100"; const backdropHidden = "opacity-0"; - // Footer button class generator - const renderActionButton = (act: ModalAction) => { + const renderActionButton = (action: ModalAction) => { const base = "flex-1 w-full px-4 py-2 rounded-md font-semibold focus:outline-none"; const styles = - act.variant === "primary" - ? "bg-[#94e2d5] text-black hover:brightness-95" - : act.variant === "link" + action.variant === "primary" + ? "bg-[#94e2d5] text-black hover:brightness-95 border-b-4 border-b-[#364153] shadow-xl" + : action.variant === "link" ? "bg-transparent text-[#075985] underline" - : "bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:brightness-95"; + : "bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:brightness-95 border-b-4 border-b-[#94e2d5] shadow-xl"; return ( - ); }; @@ -117,14 +116,12 @@ export function Modal({ role="dialog" aria-label={ariaLabel ?? title ?? "Modal dialog"} > - {/* Backdrop */}
- {/* Modal panel */}
✕ @@ -155,4 +153,4 @@ export function Modal({
); -} +}; diff --git a/frontend/routes/projects/index.tsx b/frontend/routes/projects/index.tsx index 40b50e8..1f2f12f 100644 --- a/frontend/routes/projects/index.tsx +++ b/frontend/routes/projects/index.tsx @@ -6,6 +6,7 @@ interface ProjectData { title: string; repo?: string; summary: string; + description?: string; tech: string; wip?: boolean; created_at: string; @@ -43,12 +44,16 @@ export default function Projects({ data }: PageProps) { challenges that keep me busy when I'm not doing "real work" stuff!

- {projects.map((project: any) => { + {projects.map((project: ProjectData) => { return (