Skip to content

Commit 6b19c56

Browse files
committed
feat(resources): add ability to choose package manager
1 parent 5263bb4 commit 6b19c56

File tree

4 files changed

+146
-27
lines changed

4 files changed

+146
-27
lines changed

app/lib/transformNpmCommand.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export type PackageManager = "npm" | "yarn" | "pnpm" | "bun";
2+
3+
export function transformNpmCommand(
4+
prefix: string,
5+
cmd: string,
6+
packageManagerTarget: "yarn" | "bun" | "pnpm" | "npm",
7+
) {
8+
if (prefix === "npm") {
9+
if (cmd.split(" ")[0] === "install" && packageManagerTarget === "yarn") {
10+
return `${packageManagerTarget} ${cmd.replace("install", "add")}`;
11+
}
12+
return `${packageManagerTarget} ${cmd}`;
13+
}
14+
switch (packageManagerTarget) {
15+
case "bun":
16+
return `bunx ${cmd}`;
17+
case "pnpm":
18+
return `pnpm dlx ${cmd}`;
19+
}
20+
return `${prefix} ${cmd}`;
21+
}

app/styles/resources.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
}
4242

4343
& [data-code-block-copy] {
44-
@apply absolute right-4 top-[1.125rem] h-5 w-5 cursor-pointer bg-white/80 opacity-0 dark:bg-gray-900/80;
44+
@apply absolute top-[1.125rem] h-5 w-5 cursor-pointer bg-white/80 opacity-0 dark:bg-gray-900/80;
4545
}
4646

4747
&:hover [data-code-block-copy],

app/ui/details-menu.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { forwardRef, useState, useRef, useEffect } from "react";
2+
import type { PropsWithChildren } from "react";
23
import { useLocation, useNavigation } from "@remix-run/react";
4+
import cx from "clsx";
35

46
/**
57
* An enhanced `<details>` component that's intended to be used as a menu (a bit
@@ -77,10 +79,24 @@ export const DetailsMenu = forwardRef<
7779
});
7880
DetailsMenu.displayName = "DetailsMenu";
7981

80-
export function DetailsPopup({ children }: { children: React.ReactNode }) {
82+
type DetailsPopupProps = {
83+
className?: string;
84+
childrenClassName?: string;
85+
};
86+
87+
export function DetailsPopup({
88+
children,
89+
className = "",
90+
childrenClassName = "",
91+
}: PropsWithChildren<DetailsPopupProps>) {
8192
return (
82-
<div className="absolute right-0 z-20 md:left-0">
83-
<div className="relative top-1 w-40 rounded-md border border-gray-100 bg-white p-1 shadow-sm dark:border-gray-800 dark:bg-gray-900 ">
93+
<div className={cx(className, "absolute right-0 z-20 md:left-0")}>
94+
<div
95+
className={cx(
96+
childrenClassName,
97+
"relative top-1 w-40 rounded-md border border-gray-100 bg-white p-1 shadow-sm dark:border-gray-800 dark:bg-gray-900",
98+
)}
99+
>
84100
{children}
85101
</div>
86102
</div>

app/ui/resources.tsx

Lines changed: 105 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { useEffect, useState } from "react";
22
import { type Resource } from "~/lib/resources.server";
3-
import { Link, useSearchParams } from "@remix-run/react";
3+
import { transformNpmCommand } from "~/lib/transformNpmCommand";
4+
import type { PackageManager } from "~/lib/transformNpmCommand";
5+
import { DetailsMenu, DetailsPopup } from "./details-menu";
6+
7+
import { Form, Link, useSearchParams, useSubmit } from "@remix-run/react";
48
import cx from "clsx";
59
import iconsHref from "~/icons.svg";
610

@@ -67,6 +71,13 @@ export function InitCodeblock({
6771
let [npxOrNpmMaybe, ...otherCode] = initCommand.trim().split(" ");
6872
let [copied, setCopied] = useState(false);
6973

74+
function handleCopied(copied: boolean, packageManager: PackageManager) {
75+
setCopied(copied);
76+
navigator.clipboard.writeText(
77+
transformNpmCommand(npxOrNpmMaybe, otherCode.join(" "), packageManager),
78+
);
79+
}
80+
7081
// Reset copied state after 4 seconds
7182
useEffect(() => {
7283
if (copied) {
@@ -106,30 +117,101 @@ export function InitCodeblock({
106117
</code>
107118
</pre>
108119

109-
<button
110-
type="button"
111-
onClick={() => {
112-
setCopied(true);
113-
navigator.clipboard.writeText(initCommand);
114-
}}
115-
data-code-block-copy
120+
<CopyCodeBlock setCopied={handleCopied} copied={copied} />
121+
</div>
122+
);
123+
}
124+
125+
type CopyCodeBlockProps = {
126+
copied: boolean;
127+
setCopied: (copied: boolean, packageManager: PackageManager) => void;
128+
};
129+
130+
function CopyCodeBlock({ copied, setCopied }: CopyCodeBlockProps) {
131+
const [isMenuOpen, setIsMenuOpen] = useState(false);
132+
const [searchParams] = useSearchParams();
133+
134+
return (
135+
<DetailsMenu
136+
className="absolute right-4 top-0 !opacity-100"
137+
data-copied={copied}
138+
onToggle={() => setIsMenuOpen((oldValue) => !oldValue)}
139+
>
140+
<summary
141+
className="_no-triangle absolute top-0 grid"
116142
data-copied={copied}
117-
className="outline-none"
118143
>
119-
{/* had to put these here instead of as a mask so we could add an opacity */}
120-
<svg
121-
aria-hidden
122-
className="h-5 w-5 text-gray-500 hover:text-black dark:text-gray-400 dark:hover:text-gray-100"
123-
viewBox="0 0 24 24"
144+
<span
145+
data-code-block-copy
146+
data-copied={copied}
147+
className={`absolute right-0 top-0 opacity-0 hover:opacity-100 ${copied || isMenuOpen ? "!opacity-100" : ""}`}
124148
>
125-
{copied ? (
126-
<use href={`${iconsHref}#check-mark`} />
127-
) : (
128-
<use href={`${iconsHref}#copy`} />
129-
)}
130-
</svg>
131-
<span className="sr-only">Copy code to clipboard</span>
132-
</button>
133-
</div>
149+
<svg
150+
aria-hidden
151+
className="h-5 w-5 text-gray-500 hover:text-black dark:text-gray-400 dark:hover:text-gray-100"
152+
viewBox="0 0 24 24"
153+
>
154+
{copied ? (
155+
<use href={`${iconsHref}#check-mark`} />
156+
) : (
157+
<use href={`${iconsHref}#copy`} />
158+
)}
159+
</svg>
160+
<span className="sr-only">Copy code to clipboard</span>
161+
</span>
162+
</summary>
163+
<DetailsPopup className="-bottom-28" childrenClassName="!w-20">
164+
<Form preventScrollReset replace className="flex flex-col gap-px">
165+
<input
166+
type="hidden"
167+
name="category"
168+
value={searchParams.get("category") || ""}
169+
/>
170+
171+
<PackageManagerButton
172+
packageManager="npm"
173+
setCopied={(copied) => setCopied(copied, "npm")}
174+
/>
175+
<PackageManagerButton
176+
packageManager="yarn"
177+
setCopied={(copied) => setCopied(copied, "yarn")}
178+
/>
179+
<PackageManagerButton
180+
packageManager="pnpm"
181+
setCopied={(copied) => setCopied(copied, "pnpm")}
182+
/>
183+
<PackageManagerButton
184+
packageManager="bun"
185+
setCopied={(copied) => setCopied(copied, "bun")}
186+
/>
187+
</Form>
188+
</DetailsPopup>
189+
</DetailsMenu>
190+
);
191+
}
192+
193+
type PackageManagerButtonProps = {
194+
packageManager: PackageManager;
195+
setCopied: (copied: boolean) => void;
196+
};
197+
198+
function PackageManagerButton({
199+
packageManager,
200+
setCopied,
201+
}: PackageManagerButtonProps) {
202+
const submit = useSubmit();
203+
return (
204+
<button
205+
className="rounded-sm hover:cursor-pointer hover:bg-gray-50"
206+
type="submit"
207+
onClick={(e) => {
208+
submit({
209+
preventScrollReset: false,
210+
});
211+
setCopied(true);
212+
}}
213+
>
214+
{packageManager}
215+
</button>
134216
);
135217
}

0 commit comments

Comments
 (0)