From 02d1c4b784f3f4ae104b150e873bd57978d3df04 Mon Sep 17 00:00:00 2001
From: "Wyatt J. Miller"
Date: Sun, 21 Dec 2025 15:19:30 -0500
Subject: [PATCH] added project modal, portal, and backend code to get
description
---
backend/public/src/datasources/projects.rs | 4 +-
backend/public/src/routes/projects.rs | 3 +-
frontend/fresh.gen.ts | 8 +--
frontend/islands/{portal.tsx => Portal.tsx} | 0
frontend/islands/ProjectCard.tsx | 36 ++++++------
.../islands/{modal.tsx => ProjectModal.tsx} | 58 +++++++++----------
frontend/routes/projects/index.tsx | 7 ++-
7 files changed, 58 insertions(+), 58 deletions(-)
rename frontend/islands/{portal.tsx => Portal.tsx} (100%)
rename frontend/islands/{modal.tsx => ProjectModal.tsx} (72%)
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 (
-
- {projects.map((project: any) => {
+ {projects.map((project: ProjectData) => {
return (