diff --git a/app/components/ui/breadcrumb.tsx b/app/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..4170095
--- /dev/null
+++ b/app/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react"
+import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
+import { Slot } from "@radix-ui/react-slot"
+
+import { cn } from "~/utils/cn"
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = "Breadcrumb"
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = "BreadcrumbList"
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = "BreadcrumbPage"
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:size-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/app/components/ui/dropdown-menu.tsx b/app/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..d9cf9f1
--- /dev/null
+++ b/app/components/ui/dropdown-menu.tsx
@@ -0,0 +1,203 @@
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import {
+ CheckIcon,
+ ChevronRightIcon,
+ DotFilledIcon,
+} from "@radix-ui/react-icons"
+
+import { cn } from "~/utils/cn"
+
+const DropdownMenu = DropdownMenuPrimitive.Root
+
+const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
+
+const DropdownMenuGroup = DropdownMenuPrimitive.Group
+
+const DropdownMenuPortal = DropdownMenuPrimitive.Portal
+
+const DropdownMenuSub = DropdownMenuPrimitive.Sub
+
+const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName
+
+const DropdownMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSubContent.displayName =
+ DropdownMenuPrimitive.SubContent.displayName
+
+const DropdownMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
+
+const DropdownMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
+
+const DropdownMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
+const DropdownMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, ...props }, ref) => (
+
+))
+DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
+
+const DropdownMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
+
+const DropdownMenuShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
+
+export {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuGroup,
+ DropdownMenuPortal,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
+}
diff --git a/app/components/workflow-dialog.tsx b/app/components/workflow-detail-card.tsx
similarity index 60%
rename from app/components/workflow-dialog.tsx
rename to app/components/workflow-detail-card.tsx
index d896ada..e7c28f7 100644
--- a/app/components/workflow-dialog.tsx
+++ b/app/components/workflow-detail-card.tsx
@@ -2,9 +2,7 @@ import { CopyIcon } from "@radix-ui/react-icons";
import { Link } from "@remix-run/react";
import { useState } from "react";
import Markdown from "react-markdown";
-import { useMediaQuery } from "usehooks-ts";
-import { Dialog, DialogContent, DialogHeader, DialogTitle } from "./ui/dialog";
-import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "./ui/drawer";
+import { Card, CardContent, CardHeader } from "./ui/card";
import { WorkflowIcon } from "./workflow-icon";
type Props = Readonly<{
@@ -15,10 +13,9 @@ type Props = Readonly<{
readme: string;
installCommand: string;
}>;
- onClose: () => void;
}>;
-function Instructions({ workflow, onClose }: Props) {
+function Instructions({ workflow }: Props) {
const [wasCopied, setWasCopied] = useState(false);
function handleCopy(contents: string) {
@@ -57,6 +54,9 @@ function Instructions({ workflow, onClose }: Props) {
{children}
),
+ p: ({ node, children }) => (
+ {children}
+ ),
}}
>
{workflow.readme}
@@ -105,57 +105,23 @@ function Instructions({ workflow, onClose }: Props) {
);
}
-export function WorkflowDialog(props: Props) {
- const [open, setOpen] = useState(true);
-
- const { workflow, onClose } = props;
- const isDesktop = useMediaQuery("(min-width: 768px)");
-
- async function onOpenChange(open: boolean) {
- setOpen(open);
-
- if (!open) {
- // Give the close animation a little bit more time to finish
- await new Promise((resolve) => setTimeout(resolve, 500));
-
- onClose();
- }
- }
-
- if (!isDesktop) {
- return (
-
-
-
-
-
-
-
- {workflow.title}
-
-
-
-
-
-
-
- );
- }
-
+export function WorkflowDetailCard({ workflow }: Props) {
return (
-
+
+
+
+
+
+
+
+
+ {workflow.title}
+
+
+
+
+
+
+
);
}
diff --git a/app/routes/_frontend.$category.$slug.details/model.ts b/app/routes/_frontend.$category.$slug.details/model.ts
index f4a969f..f0dbaf3 100644
--- a/app/routes/_frontend.$category.$slug.details/model.ts
+++ b/app/routes/_frontend.$category.$slug.details/model.ts
@@ -1,6 +1,8 @@
import { z } from "zod";
-export const Workflow = z.object({
+const Category = z.object({ id: z.string(), name: z.string() });
+
+const Workflow = z.object({
id: z.string(),
category: z.string(),
name: z.string(),
@@ -9,4 +11,11 @@ export const Workflow = z.object({
installCommand: z.string(),
});
-export type Workflow = z.infer;
+export const Model = z.object({
+ currentCategory: Category,
+ categories: z.array(Category),
+ currentWorkflow: Workflow,
+ workflows: z.array(Workflow),
+});
+
+export type Model = z.infer;
diff --git a/app/routes/_frontend.$category.$slug.details/query.ts b/app/routes/_frontend.$category.$slug.details/query.ts
index 03b79f5..272daf2 100644
--- a/app/routes/_frontend.$category.$slug.details/query.ts
+++ b/app/routes/_frontend.$category.$slug.details/query.ts
@@ -1,34 +1,49 @@
-import { findById } from "#workflows";
+import { findByCategory, findById, getCategories } from "#workflows";
import { err, ok } from "neverthrow";
import { getBaseUrl } from "~/utils/get-base-url.server";
-import { Workflow } from "./model";
+import { Model } from "./model";
-export async function getWorkflow(
+function createInstallCommand(baseUrl: string, workflowId: string) {
+ return `curl -s ${baseUrl}/${workflowId} | sh`;
+}
+
+export async function getModel(
request: Request,
category: string,
slug: string,
) {
const baseUrl = getBaseUrl(request);
- const resultOfGettingWorkflow = await findById(`${category}/${slug}`);
+ const currentWorkflowDao = findById(`${category}/${slug}`);
+ const categoriesDao = getCategories();
- if (resultOfGettingWorkflow.isErr()) {
- return err(resultOfGettingWorkflow.error);
- }
+ const currentCategoryDao = categoriesDao.find((c) => c.id === category);
- const dao = resultOfGettingWorkflow.value;
+ const workflowDaos = findByCategory(category);
- const workflow = Workflow.safeParse({
- id: dao.id,
- category: dao.category,
- name: dao.name,
- title: dao.title,
- readme: dao.readme,
- installCommand: `curl -s ${baseUrl}/${dao.id} | bash`,
+ const model = Model.safeParse({
+ currentCategory: currentCategoryDao,
+ categories: categoriesDao,
+ currentWorkflow: {
+ id: currentWorkflowDao.id,
+ category: currentWorkflowDao.category,
+ name: currentWorkflowDao.name,
+ title: currentWorkflowDao.title,
+ readme: currentWorkflowDao.readme,
+ installCommand: createInstallCommand(baseUrl, currentWorkflowDao.id),
+ },
+ workflows: workflowDaos.map((dao) => ({
+ id: dao.id,
+ category: dao.category,
+ name: dao.name,
+ title: dao.title,
+ readme: dao.readme,
+ installCommand: createInstallCommand(baseUrl, dao.id),
+ })),
});
- if (!workflow.success) {
- return err(new Error(`failed to prepare view model: ${workflow.error}`));
+ if (!model.success) {
+ return err(new Error(`failed to prepare view model: ${model.error}`));
}
- return ok(workflow.data);
+ return ok(model.data);
}
diff --git a/app/routes/_frontend.$category.$slug.details/route.tsx b/app/routes/_frontend.$category.$slug.details/route.tsx
index 072c4b1..c907f29 100644
--- a/app/routes/_frontend.$category.$slug.details/route.tsx
+++ b/app/routes/_frontend.$category.$slug.details/route.tsx
@@ -1,15 +1,27 @@
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
-import { json } from "@remix-run/node";
-
-import { useLoaderData, useNavigate } from "@remix-run/react";
-import { WorkflowDialog } from "~/components/workflow-dialog";
-import { getWorkflow } from "./query";
+import { ChevronDownIcon } from "@radix-ui/react-icons";
+import { Link, json, useLoaderData } from "@remix-run/react";
+import {
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbSeparator,
+} from "~/components/ui/breadcrumb";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "~/components/ui/dropdown-menu";
+import { WorkflowDetailCard } from "~/components/workflow-detail-card";
+import { getModel } from "./query";
export const meta: MetaFunction = ({ data }) => {
return [
{
- title: `GitHub Actions Starter Workflows: ${data?.workflow.title} - getactions.dev`,
+ title: `GitHub Actions Starter Workflows: ${data?.currentWorkflow.title} - getactions.dev`,
},
];
};
@@ -22,28 +34,78 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
throw new Response("Bad Request", { status: 400 });
}
- const resultOfGettingWorkflow = await getWorkflow(request, category, slug);
+ const result = await getModel(request, category, slug);
- if (resultOfGettingWorkflow.isErr()) {
- console.error(resultOfGettingWorkflow.error);
+ if (result.isErr()) {
+ console.error(result.error);
throw new Response("Internal Server Error", { status: 500 });
}
- const workflow = resultOfGettingWorkflow.value;
+ const model = result.value;
- return json({ workflow });
+ return json(model);
}
export default function WorkflowDetails() {
- const navigate = useNavigate();
const loaderData = useLoaderData();
- function handleClose() {
- navigate(`/${loaderData.workflow.category}`, { preventScrollReset: true });
- }
-
return (
-
+
+
+
+
+
+ All Workflows
+
+
+
+
+
+ {loaderData.currentCategory.name}
+
+
+
+ {loaderData.categories.map((category) => (
+
+
+ {category.name}
+
+
+ ))}
+
+
+
+
+
+
+
+ {loaderData.currentWorkflow.title}
+
+
+
+ {loaderData.workflows.map((workflow) => (
+
+
+ {workflow.title}
+
+
+ ))}
+
+
+
+
+
+
+
+
);
}
diff --git a/app/routes/_frontend.$category/model.ts b/app/routes/_frontend.$category._index/model.ts
similarity index 100%
rename from app/routes/_frontend.$category/model.ts
rename to app/routes/_frontend.$category._index/model.ts
diff --git a/app/routes/_frontend.$category/query.ts b/app/routes/_frontend.$category._index/query.ts
similarity index 70%
rename from app/routes/_frontend.$category/query.ts
rename to app/routes/_frontend.$category._index/query.ts
index fdc5ed6..385caf9 100644
--- a/app/routes/_frontend.$category/query.ts
+++ b/app/routes/_frontend.$category._index/query.ts
@@ -1,18 +1,9 @@
import { findByCategory } from "#workflows";
import { err, ok } from "neverthrow";
-import { getBaseUrl } from "~/utils/get-base-url.server";
import { Workflows } from "./model";
export async function getWorkflows(request: Request, category: string) {
- const baseUrl = getBaseUrl(request);
-
- const result = await findByCategory(category);
-
- if (result.isErr()) {
- return err(result.error);
- }
-
- const daos = result.value;
+ const daos = findByCategory(category);
const validation = Workflows.safeParse(
daos.map((dao) => ({
diff --git a/app/routes/_frontend.$category/route.tsx b/app/routes/_frontend.$category._index/route.tsx
similarity index 85%
rename from app/routes/_frontend.$category/route.tsx
rename to app/routes/_frontend.$category._index/route.tsx
index 251356c..4bca4e2 100644
--- a/app/routes/_frontend.$category/route.tsx
+++ b/app/routes/_frontend.$category._index/route.tsx
@@ -1,7 +1,8 @@
import { getCategories } from "#workflows";
import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
-import { Link, Outlet, json, redirect, useLoaderData } from "@remix-run/react";
+import { Link, json, redirect, useLoaderData } from "@remix-run/react";
import { WorkflowCard } from "~/components/workflow-card";
+import { WorkflowSwitcher } from "~/components/workflow-switcher";
import { getWorkflows } from "./query";
export const meta: MetaFunction = ({ params, data }) => {
@@ -41,7 +42,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
(category) => category.id === requestedCategory,
);
- return json({ category, workflows });
+ return json({ category, categories, workflows });
}
export default function Index() {
@@ -49,6 +50,9 @@ export default function Index() {
return (
<>
+
+
+
{loaderData.workflows.map((workflow) => (
))}
-
>
);
}
diff --git a/app/routes/_frontend/route.tsx b/app/routes/_frontend/route.tsx
index 39cabf3..7879c5a 100644
--- a/app/routes/_frontend/route.tsx
+++ b/app/routes/_frontend/route.tsx
@@ -5,7 +5,6 @@ import {
type MetaFunction,
} from "@remix-run/react";
import { Footer } from "~/components/footer";
-import { WorkflowSwitcher } from "~/components/workflow-switcher";
import { getWorkflowCategories } from "./query";
export async function loader() {
@@ -45,10 +44,6 @@ export default function FrontendLayout() {
-
-
-
-
diff --git a/bun.lockb b/bun.lockb
index a5e0fd1..2805907 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index 5491b1f..fb2dbc8 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
},
"dependencies": {
"@radix-ui/react-dialog": "^1.0.5",
+ "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2",
diff --git a/workflows/index.ts b/workflows/index.ts
index c756cdf..0599c99 100644
--- a/workflows/index.ts
+++ b/workflows/index.ts
@@ -1,4 +1,3 @@
-import { ok } from "neverthrow";
import fs from "node:fs";
import path from "node:path";
@@ -113,19 +112,18 @@ const workflows = result
.map((workflows) => workflows)
.reduce((acc, workflow) => Object.assign(acc, workflow), {});
-export async function getCategories() {
+export function getCategories() {
return categories;
}
-export async function findById(id: string) {
- return ok(workflows[id]);
+export function findById(id: string) {
+ return workflows[id];
}
-export async function findByCategory(category: string) {
+export function findByCategory(category: string) {
const workflowsInCategory = Object.keys(workflows)
-
.filter((id) => workflows[id].category === category)
.map((id) => workflows[id]);
- return ok(workflowsInCategory);
+ return workflowsInCategory;
}