Compare commits

...

4 Commits

Author SHA1 Message Date
Guido D'Orsi
48ac92bc67 fix: correctly setup the metro config on React Native templates 2025-02-12 19:22:23 +01:00
Trisha Lim
d3603625fd Fix copy password button 2025-02-12 19:43:20 +07:00
Trisha Lim
fa94d8c171 Show button on mobile 2025-02-12 19:43:20 +07:00
Trisha Lim
aeb094baa1 Add "use as template" button to examples 2025-02-12 19:43:20 +07:00
11 changed files with 264 additions and 49 deletions

View File

@@ -0,0 +1,5 @@
---
"create-jazz-app": patch
---
Correctly setup the metro config on React Native templates

View File

@@ -77,6 +77,7 @@ const icons = {
// copied from tailwind line height https://tailwindcss.com/docs/font-size
const sizes = {
"2xs": 14,
xs: 16,
sm: 20,
md: 24,
@@ -93,6 +94,7 @@ const sizes = {
};
const strokeWidths = {
"2xs": 2.5,
xs: 2,
sm: 2,
md: 1.5,

View File

@@ -1,18 +1,18 @@
"use client";
import { clsx } from "clsx";
import { useEffect, useRef, useState } from "react";
import { useEffect, useId, useRef, useState } from "react";
import { Icon } from "../atoms/Icon";
// TODO: add tabs feature, and remove CodeExampleTabs
function CopyButton({ code, size }: { code: string; size: "md" | "lg" }) {
let [copyCount, setCopyCount] = useState(0);
let copied = copyCount > 0;
const [copyCount, setCopyCount] = useState(0);
const copied = copyCount > 0;
useEffect(() => {
if (copyCount > 0) {
let timeout = setTimeout(() => setCopyCount(0), 1000);
const timeout = setTimeout(() => setCopyCount(0), 1000);
return () => {
clearTimeout(timeout);
};
@@ -23,7 +23,8 @@ function CopyButton({ code, size }: { code: string; size: "md" | "lg" }) {
<button
type="button"
className={clsx(
"group/button absolute overflow-hidden rounded text-2xs font-medium opacity-0 backdrop-blur transition focus:opacity-100 group-hover:opacity-100",
"group/button absolute overflow-hidden rounded text-2xs font-medium md:opacity-0 backdrop-blur transition md:focus:opacity-100 group-hover:opacity-100",
"right-[9px] top-[9px]",
copied
? "bg-emerald-400/10 ring-1 ring-inset ring-emerald-400/20"
: "bg-white/5 hover:bg-white/7.5 dark:bg-white/2.5 dark:hover:bg-white/5",
@@ -72,13 +73,13 @@ export function CodeGroup({
size = "md",
className,
}: {
children: React.ReactNode;
children?: React.ReactNode;
text?: string;
size?: "md" | "lg";
className?: string;
}) {
const textRef = useRef<HTMLPreElement | null>(null);
const [code, setCode] = useState<string>();
useEffect(() => {
if (textRef.current) {
setCode(textRef.current.innerText);

View File

@@ -0,0 +1,108 @@
import * as Headless from "@headlessui/react";
import clsx from "clsx";
import type React from "react";
const sizes = {
xs: "sm:max-w-xs",
sm: "sm:max-w-sm",
md: "sm:max-w-md",
lg: "sm:max-w-lg",
xl: "sm:max-w-xl",
"2xl": "sm:max-w-2xl",
"3xl": "sm:max-w-3xl",
"4xl": "sm:max-w-4xl",
"5xl": "sm:max-w-5xl",
};
export type DialogProps = {
size?: keyof typeof sizes;
className?: string;
children: React.ReactNode;
} & Omit<Headless.DialogProps, "as" | "className">;
export function Dialog({
size = "lg",
className,
children,
...props
}: DialogProps) {
return (
<Headless.Dialog {...props}>
<Headless.DialogBackdrop
transition
className="z-50 fixed inset-0 flex w-screen justify-center overflow-y-auto bg-stone-950/25 px-2 py-2 transition duration-100 focus:outline-0 data-[closed]:opacity-0 data-[enter]:ease-out data-[leave]:ease-in sm:px-6 sm:py-8 lg:px-8 lg:py-16 dark:bg-stone-950/70"
/>
<div className="z-50 fixed inset-0 w-screen overflow-y-auto pt-6 sm:pt-0">
<div className="grid min-h-full grid-rows-[1fr_auto] justify-items-center sm:grid-rows-[1fr_auto_3fr] sm:p-4">
<Headless.DialogPanel
transition
className={clsx(
className,
sizes[size],
"row-start-2 w-full min-w-0 rounded-t-3xl bg-white p-[--gutter] shadow-lg ring-1 ring-stone-950/10 [--gutter:theme(spacing.8)] sm:mb-auto sm:rounded-2xl dark:bg-stone-950 dark:ring-white/10 forced-colors:outline",
"transition duration-100 will-change-transform data-[closed]:translate-y-12 data-[closed]:opacity-0 data-[enter]:ease-out data-[leave]:ease-in sm:data-[closed]:translate-y-0 sm:data-[closed]:data-[enter]:scale-95",
)}
>
{children}
</Headless.DialogPanel>
</div>
</div>
</Headless.Dialog>
);
}
export function DialogTitle({
className,
...props
}: { className?: string } & Omit<
Headless.DialogTitleProps,
"as" | "className"
>) {
return (
<Headless.DialogTitle
{...props}
className={clsx(
className,
"text-balance text-lg/6 font-semibold text-stone-900 dark:text-white",
)}
/>
);
}
export function DialogDescription({
className,
...props
}: { className?: string } & Omit<
Headless.DescriptionProps,
"as" | "className"
>) {
return (
<Headless.Description
{...props}
className={clsx(className, "mt-2 text-pretty")}
/>
);
}
export function DialogBody({
className,
...props
}: React.ComponentPropsWithoutRef<"div">) {
return <div {...props} className={clsx(className, "mt-6")} />;
}
export function DialogActions({
className,
...props
}: React.ComponentPropsWithoutRef<"div">) {
return (
<div
{...props}
className={clsx(
className,
"mt-8 flex flex-col-reverse items-center justify-end gap-3 *:w-full sm:flex-row sm:*:w-auto",
)}
/>
);
}

View File

@@ -283,27 +283,31 @@ const PasswordManagerIllustration = () => (
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-2">user@gmail.com</td>
<td className="p-2">gmail.com</td>
<td className="p-2">
<MockButton>Copy password</MockButton>
</td>
</tr>
<tr className="border-b">
<td className="p-2">user@gmail.com</td>
<td className="p-2">fb.com</td>
<td className="p-2">
<MockButton>Copy password</MockButton>
</td>
</tr>
<tr className="border-b">
<td className="p-2">user@gmail.com</td>
<td className="p-2">x.com</td>
<td className="p-2">
<MockButton>Copy password</MockButton>
</td>
</tr>
{[
{
email: "user@gmail.com",
domain: "gmail.com",
},
{
email: "user@gmail.com",
domain: "fb.com",
},
{
email: "user@gmail.com",
domain: "x.com",
},
].map(({ email, domain }) => (
<tr className="border-b max-sm:last:hidden" key={domain}>
<td className="p-2">{email}</td>
<td className="p-2">{domain}</td>
<td className="p-2">
<MockButton>
<Icon name="copy" size="2xs" className="mr-1" />
Password
</MockButton>
</td>
</tr>
))}
</tbody>
</table>
</div>

View File

@@ -0,0 +1,3 @@
```sh
npx create-jazz-app@latest --example $EXAMPLE
```

View File

@@ -1,21 +1,66 @@
"use client";
import { Example } from "@/lib/example";
import { InterpolateInCode } from "@/mdx-components";
import { DialogDescription } from "@headlessui/react";
import { Button } from "gcmp-design-system/src/app/components/atoms/Button";
import { CodeGroup } from "gcmp-design-system/src/app/components/molecules/CodeGroup";
import {
Dialog,
DialogActions,
DialogBody,
DialogTitle,
} from "gcmp-design-system/src/app/components/organisms/Dialog";
import { useState } from "react";
import CreateJazzApp from "./CreateJazzApp.mdx";
export function ExampleLinks({ example }: { example: Example }) {
const { slug, demoUrl } = example;
const githubUrl = `https://github.com/gardencmp/jazz/tree/main/examples/${slug}`;
const [isOpen, setIsOpen] = useState(false);
return (
<div className="flex gap-2">
<Button href={githubUrl} newTab variant="secondary" size="sm">
View code
</Button>
{demoUrl && (
<Button href={demoUrl} newTab variant="secondary" size="sm">
View demo
<>
<div className="flex gap-2">
<Button variant="secondary" size="sm" onClick={() => setIsOpen(true)}>
Use as template
</Button>
)}
</div>
<Button href={githubUrl} newTab variant="secondary" size="sm">
<span className="md:hidden">Code</span>
<span className="hidden md:inline">View code</span>
</Button>
{demoUrl && (
<Button href={demoUrl} newTab variant="secondary" size="sm">
<span className="md:hidden">Demo</span>
<span className="hidden md:inline">View demo</span>
</Button>
)}
</div>
<Dialog onClose={() => setIsOpen(false)} open={isOpen}>
<DialogTitle>Use {example.name} example as a template</DialogTitle>
<DialogBody>
<div className="mb-6 aspect-[16/9] overflow-hidden w-full rounded-md bg-white border dark:bg-stone-925 sm:aspect-[2/1] md:aspect-[3/2]">
{example.illustration}
</div>
<p className="mb-3">
Generate a new Jazz app by running the command below.
</p>
<CodeGroup>
<CreateJazzApp
components={InterpolateInCode({
$EXAMPLE: example.slug,
})}
/>
</CodeGroup>
</DialogBody>
<DialogActions>
<Button onClick={() => setIsOpen(false)} variant="secondary">
Cancel
</Button>
</DialogActions>
</Dialog>
</>
);
}

View File

@@ -1,9 +1,31 @@
import { DocsLink } from "@/components/docs/DocsLink";
import type { MDXComponents } from "mdx/types";
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
a: (props) => <DocsLink {...props} />,
...components,
CodeWithInterpolation: ({
highlightedCode,
}: { highlightedCode: string }) => {
return <div dangerouslySetInnerHTML={{ __html: highlightedCode }} />;
},
};
}
export function InterpolateInCode(replace: { [key: string]: string }) {
return {
CodeWithInterpolation: ({
highlightedCode,
}: { highlightedCode: string }) => {
const newHighlightedCode = Object.entries(replace).reduce(
(acc, [key, value]) => {
return acc.replaceAll(
key.replaceAll("$", "&#36;").replaceAll("_", "&#95;"),
value,
);
},
highlightedCode,
);
return <div dangerouslySetInnerHTML={{ __html: newHighlightedCode }} />;
},
};
}

View File

@@ -38,7 +38,7 @@ function highlightPlugin() {
return async function transformer(tree) {
const highlighter = await getHighlighter({
langs: ["typescript", "bash", "tsx", "json", "svelte"],
theme: "css-variables", // use the theme
theme: "css-variables", // use css variables in shiki.css
});
visit(tree, "code", visitor);
@@ -116,7 +116,7 @@ function remarkHtmlToJsx() {
const [ast] = args;
visit(ast, "html", (node) => {
const escapedHtml = JSON.stringify(node.value);
const jsx = `<div dangerouslySetInnerHTML={{__html: ${escapedHtml} }}/>`;
const jsx = `<CodeWithInterpolation highlightedCode={${escapedHtml}}/>`;
const rawHtmlNode = fromMarkdown(jsx, {
extensions: [mdxjs()],
mdastExtensions: [mdxFromMarkdown()],

View File

@@ -67,35 +67,53 @@ export const configMap: ConfigStructure = {
},
};
export const PLATFORM = {
WEB: "web",
REACT_NATIVE: "react-native",
} as const;
export type FrameworkAuthPair =
`${ValidFramework<Environment, ValidEngine<Environment>>}-${ValidAuth<Environment, ValidEngine<Environment>, ValidFramework<Environment, ValidEngine<Environment>>>}-auth`;
export const frameworkToAuthExamples: Partial<
Record<FrameworkAuthPair, { name: string; repo: string | undefined }>
Record<
FrameworkAuthPair,
{
name: string;
repo: string | undefined;
platform: (typeof PLATFORM)[keyof typeof PLATFORM];
}
>
> = {
"react-demo-auth": {
name: "React + Jazz + Demo Auth + Tailwind",
repo: "garden-co/jazz/starters/react-demo-auth-tailwind",
platform: PLATFORM.WEB,
},
"react-passkey-auth": {
name: "React + Jazz + Passkey Auth",
repo: "garden-co/jazz/examples/passkey",
platform: PLATFORM.WEB,
},
"react-clerk-auth": {
name: "React + Jazz + Clerk Auth",
repo: "garden-co/jazz/examples/clerk",
platform: PLATFORM.WEB,
},
"vue-demo-auth": {
name: "Vue + Jazz + Demo Auth",
repo: "garden-co/jazz/examples/todo-vue",
platform: PLATFORM.WEB,
},
"svelte-passkey-auth": {
name: "Svelte + Jazz + Passkey Auth",
repo: "garden-co/jazz/examples/passkey-svelte",
platform: PLATFORM.WEB,
},
"rn-clerk-auth": {
name: "React Native Expo + Jazz + Clerk Auth",
repo: "garden-co/jazz/examples/chat-rn-clerk",
platform: PLATFORM.REACT_NATIVE,
},
};

View File

@@ -12,6 +12,7 @@ import ora from "ora";
import {
Framework,
type FrameworkAuthPair,
PLATFORM,
frameworkToAuthExamples,
frameworks,
} from "./config.js";
@@ -82,6 +83,10 @@ async function getLatestPackageVersions(
return versions;
}
function getPlatformFromTemplateName(template: string) {
return template.includes("-rn") ? PLATFORM.REACT_NATIVE : PLATFORM.WEB;
}
async function scaffoldProject({
template,
projectName,
@@ -92,12 +97,14 @@ async function scaffoldProject({
const starterConfig = frameworkToAuthExamples[
template as FrameworkAuthPair
] || { name: template, repo: "garden-co/jazz/examples/" + template };
if (!starterConfig) {
throw new Error(`Invalid template: ${template}`);
}
] || {
name: template,
repo: "garden-co/jazz/examples/" + template,
platform: getPlatformFromTemplateName(template),
};
const devCommand = template.includes("rn-clerk") ? "ios" : "dev";
const devCommand =
starterConfig.platform === PLATFORM.REACT_NATIVE ? "ios" : "dev";
if (!starterConfig.repo) {
throw new Error(
@@ -207,7 +214,7 @@ async function scaffoldProject({
}
// Additional setup for React Native
if (template === "react-native-expo-clerk-auth") {
if (starterConfig.platform === PLATFORM.REACT_NATIVE) {
const rnSpinner = ora({
text: chalk.blue("Setting up React Native project..."),
spinner: "dots",