1 Commits

Author SHA1 Message Date
5396d44189 added multi stage builds for oci containers
Some checks failed
Build and Release Docker Images / build-and-push (./backend, public/Dockerfile, my-website-v2_public) (pull_request) Failing after 23m10s
Build and Release Docker Images / build-and-push (./backend, task/Dockerfile, my-website-v2_task) (pull_request) Failing after 24m49s
Build and Release Docker Images / build-and-push (./frontend, Dockerfile, my-website-v2_frontend) (pull_request) Failing after 20m2s
Build and Release Docker Images / create-release (pull_request) Has been skipped
it should help from builds from failing in addition to slimming down the
artifact size
2025-12-21 15:36:00 -05:00
11 changed files with 52 additions and 268 deletions

View File

@@ -1,4 +1,5 @@
FROM rust:1.88.0
# Build stage
FROM rust:1.88.0 AS builder
WORKDIR /app
@@ -7,6 +8,16 @@ COPY ./cache ./cache
RUN cargo build --release --manifest-path ./public/Cargo.toml
# Runtime stage with Alpine
FROM alpine:latest
WORKDIR /app
RUN apk add --no-cache ca-certificates libgcc
COPY --from=builder /app/public/target/release/public /app/public
COPY --from=builder /app/cache ./cache
EXPOSE 3000
CMD ["/app/public/target/release/public"]
CMD ["/app/public"]

View File

@@ -1,4 +1,4 @@
use sqlx::{Pool, Postgres};
use sqlx::{FromRow, Pool, Postgres, Row};
use crate::routes::projects::Project;
@@ -7,7 +7,7 @@ impl ProjectsDatasource {
pub async fn get_all(pool: &Pool<Postgres>) -> Result<Vec<Project>, sqlx::Error> {
sqlx::query_as!(
Project,
"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"
"SELECT project_id, title, repo, summary, tech, wip, created_at FROM projects p WHERE deleted_at IS NULL ORDER BY p.created_at DESC"
)
.fetch_all(pool)
.await

View File

@@ -9,7 +9,6 @@ pub struct Project {
pub title: String,
pub repo: Option<String>,
pub summary: String,
pub description: Option<String>,
pub tech: String,
pub wip: Option<bool>,
#[serde(serialize_with = "serialize_datetime")]
@@ -67,3 +66,5 @@ impl ProjectsRoute {
}
}
}

View File

@@ -1,4 +1,5 @@
FROM rust:1.88.0
# Build stage
FROM rust:1.88.0 AS builder
WORKDIR /app
@@ -6,9 +7,23 @@ COPY ./task ./task
COPY ./cache ./cache
COPY ./storage ./storage
RUN mkdir /app/posts
RUN cargo build --release --manifest-path ./task/Cargo.toml
# Runtime stage
FROM debian:bookworm-slim
WORKDIR /app
RUN apt-get update && apt-get install -y \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /app/posts
COPY --from=builder /app/task/target/release/task /app/task
COPY --from=builder /app/cache ./cache
COPY --from=builder /app/storage ./storage
EXPOSE 3000
CMD ["/app/task/target/release/task"]
CMD ["/app/task"]

View File

@@ -76,10 +76,8 @@
RUST_BACKTRACE = "1";
RUST_SRC_PATH = "${pkgs.rustToolchain}/lib/rustlib/src/rust/library";
PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig";
NIX_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [
pkgs.stdenv.cc.cc
];
NIX_LD = pkgs.lib.fileContents "${pkgs.stdenv.cc}/nix-support/dynamic-linker";
# ZELLIJ_CONFIG_FILE = "config.kdl";
# PATH = "$PATH:$HOME/.local/share/nvim/mason/bin/deno";
};
};
});

View File

@@ -4,10 +4,10 @@ RUN apk add bash
# USER deno
COPY . .
RUN deno cache --reload deno.json
COPY . .
RUN bash -c 'deno cache main.ts'
RUN bash -c 'deno task build'

View File

@@ -15,9 +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 $Portal from "./islands/Portal.tsx";
import * as $ProjectCard from "./islands/ProjectCard.tsx";
import * as $ProjectModal from "./islands/ProjectModal.tsx";
import { type Manifest } from "$fresh/server.ts";
const manifest = {
@@ -37,9 +35,7 @@ const manifest = {
},
islands: {
"./islands/Counter.tsx": $Counter,
"./islands/Portal.tsx": $Portal,
"./islands/ProjectCard.tsx": $ProjectCard,
"./islands/ProjectModal.tsx": $ProjectModal,
},
baseUrl: import.meta.url,
} satisfies Manifest;

View File

@@ -1,38 +0,0 @@
import { useEffect, useState } from "preact/hooks";
import { createPortal } from "preact/compat";
import type { ComponentChildren } from "preact";
type PortalProps = {
into?: string | HTMLElement;
children: ComponentChildren;
};
export function Portal({ into = "body", children }: PortalProps) {
const [host, setHost] = useState<HTMLElement | null>(null);
useEffect(() => {
if (typeof document === "undefined") return;
let target: HTMLElement | null = null;
if (typeof into === "string") {
target = into === "body" ? document.body : document.querySelector(into);
} else {
target = into;
}
if (!target) target = document.body;
const wrapper = document.createElement("div");
wrapper.className = "preact-portal-root";
target.appendChild(wrapper);
setHost(wrapper);
return () => {
if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
setHost(null);
};
}, [into]);
if (!host) return null;
return createPortal(children, host);
}

View File

@@ -1,20 +1,4 @@
import { useState } from "preact/hooks";
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<ModalAction> = [
{
label: "Open repository",
onClick: () => {
if (props.repo) globalThis.open(props.repo, "_blank");
},
variant: "primary",
},
];
return (
<div
class={`md:m-8 group space-y-1 rounded-md ${
@@ -29,27 +13,25 @@ export const ProjectCard = function ProjectCard(props: ProjectProps) {
}
: {}
}
onClick={() => {
// clicking the card (not the link) opens the modal
console.log("opened portal");
setOpen(true);
}}
onClick={() => props.repo && open(props.repo, "_blank")}
>
<div class="flex items-center justify-between">
<h2 class="text-lg text-white font-black uppercase">
<a
href={props.repo}
target="_blank"
onClick={(e) => {
// clicking the link should not open the modal
e.stopPropagation();
}}
>
<a href={props.repo} target="_blank">
{props.title}
</a>
</h2>
<div class="bg-[#585b70] text-[#a6adc8] text-xs font-bold uppercase px-2.5 py-0.5 rounded-full">
{props.repo && <span>Active</span>}
{props.repo && (
<a
class="hover:underline"
href={props.repo}
target="_blank"
onClick={(e) => e.stopPropagation()}
>
Active
</a>
)}
{!props.repo && !props.wip && <span>Dead</span>}
{props.wip && <span>WIP</span>}
</div>
@@ -60,25 +42,6 @@ export const ProjectCard = function ProjectCard(props: ProjectProps) {
<p class="whitespace-pre-wrap text-sm font-semibold text-[#a6adc8]">
{props.tech}
</p>
{open && !props.wip ? (
<Portal into="body">
<ProjectModal
title={props.title}
onClose={() => setOpen(false)}
actions={modalButtons}
>
<div class="space-y-3">
<p class="text-sm text-gray-800 dark:text-gray-200">
{props.description}
</p>
<p class="text-xs font-mono text-gray-600 dark:text-gray-300">
Technologies used: {props.tech}
</p>
</div>
</ProjectModal>
</Portal>
) : null}
</div>
);
};
@@ -87,7 +50,6 @@ type ProjectProps = {
title: string;
repo?: string;
summary: string;
description?: string;
tech: string;
wip?: boolean;
};

View File

@@ -1,156 +0,0 @@
import { useEffect, useRef, useState } from "preact/hooks";
import type { ComponentChildren } from "preact";
export type ModalAction = {
label: string;
onClick: () => void;
variant?: "primary" | "secondary" | "link";
};
type ModalProps = {
title?: string;
/**
* Called after the modal has finished its exit animation.
* The Modal will run the exit animation internally and then call onClose().
*/
onClose: () => void;
children: ComponentChildren;
ariaLabel?: string;
actions?: ModalAction[]; // rendered in footer; each button will be given flex-1 so buttons fill the width together
/**
* Optional: duration (ms) of enter/exit animation. Defaults to 200.
* Keep this in sync with the CSS transition-duration classes used below.
*/
animationDurationMs?: number;
};
export const ProjectModal = function ProjectModal({
title,
onClose,
children,
ariaLabel,
actions,
animationDurationMs = 200,
}: ModalProps) {
const [isVisible, setIsVisible] = useState(false);
const closingRef = useRef(false);
const timeoutRef = useRef<number | null>(null);
useEffect(() => {
const raf = requestAnimationFrame(() => setIsVisible(true));
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") {
startCloseFlow();
}
}
document.addEventListener("keydown", onKey);
return () => {
cancelAnimationFrame(raf);
document.removeEventListener("keydown", onKey);
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const footerActions: ModalAction[] =
actions && actions.length > 0
? [
...actions,
{
label: "Close",
onClick: startCloseFlow,
variant: "secondary",
},
]
: [{ label: "Close", onClick: startCloseFlow, variant: "primary" }];
function startCloseFlow() {
if (closingRef.current) return;
closingRef.current = true;
setIsVisible(false);
timeoutRef.current = globalThis.setTimeout(() => {
timeoutRef.current = null;
onClose();
}, animationDurationMs);
}
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";
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";
const renderActionButton = (action: ModalAction) => {
const base =
"flex-1 w-full px-4 py-2 rounded-md font-semibold focus:outline-none";
const styles =
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 border-b-4 border-b-[#94e2d5] shadow-xl";
return (
<button
key={action.label}
onClick={action.onClick}
class={`${base} ${styles}`}
type="button"
>
{action.label}
</button>
);
};
return (
<div
class="fixed inset-0 z-50 flex items-center justify-center"
aria-modal="true"
role="dialog"
aria-label={ariaLabel ?? title ?? "Modal dialog"}
>
<div
style={{ transitionDuration: `${animationDurationMs}ms` }}
class={`${backdropBase} ${
isVisible ? backdropVisible : backdropHidden
}`}
/>
<div
style={{ transitionDuration: `${animationDurationMs}ms` }}
class={`${panelBase} ${isVisible ? panelVisible : panelHidden}`}
onClick={(e) => e.stopPropagation()}
>
<div class="flex items-start justify-between">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{title}
</h3>
<button
onClick={startCloseFlow}
aria-label="Close modal"
class="ml-4 rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 p-1"
type="button"
>
</button>
</div>
<div class="mt-4 text-sm text-gray-700 dark:text-gray-300">
{children}
</div>
<div class="mt-6">
<div class="flex gap-3 w-full">
{footerActions.map(renderActionButton)}
</div>
</div>
</div>
</div>
);
};

View File

@@ -6,7 +6,6 @@ interface ProjectData {
title: string;
repo?: string;
summary: string;
description?: string;
tech: string;
wip?: boolean;
created_at: string;
@@ -44,16 +43,12 @@ export default function Projects({ data }: PageProps<ProjectData>) {
challenges that keep me busy when I'm not doing "real work" stuff!
</p>
<div class="grid grid-cols-1 sm:grid-cols-2">
{projects.map((project: ProjectData) => {
{projects.map((project: any) => {
return (
<ProjectCard
title={project.title}
repo={project.repo ?? undefined}
summary={project.summary}
description={
project.description ??
"No description found. Perhaps you should check out the repository instead!"
}
tech={project.tech}
wip={project.wip ?? true}
/>