Skip to content

Commit f0c752a

Browse files
committed
feat(shadcn): Add local dev setup & example flow
1 parent b9cda45 commit f0c752a

30 files changed

+796
-29
lines changed

examples/shadcn/.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

examples/shadcn/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
TODO

examples/shadcn/components.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema.json",
3+
"style": "new-york",
4+
"rsc": false,
5+
"tsx": true,
6+
"tailwind": {
7+
"config": "",
8+
"css": "src/index.css",
9+
"baseColor": "neutral",
10+
"cssVariables": true,
11+
"prefix": ""
12+
},
13+
"iconLibrary": "lucide",
14+
"aliases": {
15+
"components": "@/components",
16+
"utils": "@/lib/utils",
17+
"ui": "@/components/ui",
18+
"lib": "@/lib",
19+
"hooks": "@/hooks"
20+
},
21+
"registries": {
22+
"@dev": "http://localhost:5177/{name}.json",
23+
"@firebase-ui": "https://ui.firebase.com/{name}.json"
24+
}
25+
}
File renamed without changes.

examples/shadcn/package.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "shadcn",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite"
8+
},
9+
"dependencies": {
10+
"@firebase-ui/core": "workspace:*",
11+
"@firebase-ui/react": "workspace:*",
12+
"@hookform/resolvers": "^5.2.2",
13+
"@radix-ui/react-label": "^2.1.7",
14+
"@radix-ui/react-slot": "^1.2.3",
15+
"class-variance-authority": "^0.7.1",
16+
"clsx": "^2.1.1",
17+
"lucide-react": "^0.544.0",
18+
"react": "catalog:",
19+
"react-dom": "catalog:",
20+
"react-hook-form": "^7.64.0",
21+
"tailwind-merge": "^3.3.1",
22+
"zod": "catalog:"
23+
},
24+
"devDependencies": {
25+
"@tailwindcss/vite": "catalog:",
26+
"@types/node": "catalog:",
27+
"@types/react": "catalog:",
28+
"@types/react-dom": "catalog:",
29+
"@vitejs/plugin-react": "catalog:",
30+
"tailwindcss": "catalog:",
31+
"tw-animate-css": "^1.4.0",
32+
"typescript": "catalog:",
33+
"vite": "catalog:"
34+
}
35+
}

examples/shadcn/public/vite.svg

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
function App() {
2-
return <></>;
2+
return <>TODO</>;
33
}
44

55
export default App;
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { cn } from "@/lib/utils";
2+
import { getTranslation } from "@firebase-ui/core";
3+
import { useUI, PolicyContext } from "@firebase-ui/react";
4+
import { cloneElement, useContext } from "react";
5+
6+
export function Policies() {
7+
const ui = useUI();
8+
const policies = useContext(PolicyContext);
9+
10+
if (!policies) {
11+
return null;
12+
}
13+
14+
const { termsOfServiceUrl, privacyPolicyUrl, onNavigate } = policies;
15+
const termsAndPrivacyText = getTranslation(ui, "messages", "termsAndPrivacy");
16+
const parts = termsAndPrivacyText.split(/(\{tos\}|\{privacy\})/);
17+
18+
const className = cn("hover:underline font-semibold");
19+
const Handler = onNavigate ? (
20+
<button className={className} />
21+
) : (
22+
<a target="_blank" rel="noopener noreferrer" className={className} />
23+
);
24+
25+
return (
26+
<div className="text-text-muted text-center text-xs">
27+
{parts.map((part: string, index: number) => {
28+
if (part === "{tos}") {
29+
return cloneElement(Handler, {
30+
key: index,
31+
onClick: onNavigate ? () => onNavigate(termsOfServiceUrl) : undefined,
32+
href: onNavigate ? undefined : termsOfServiceUrl,
33+
children: getTranslation(ui, "labels", "termsOfService"),
34+
});
35+
}
36+
37+
if (part === "{privacy}") {
38+
return cloneElement(Handler, {
39+
key: index,
40+
onClick: onNavigate ? () => onNavigate(privacyPolicyUrl) : undefined,
41+
href: onNavigate ? undefined : privacyPolicyUrl,
42+
children: getTranslation(ui, "labels", "privacyPolicy"),
43+
});
44+
}
45+
46+
return <span key={index}>{part}</span>;
47+
})}
48+
</div>
49+
);
50+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"use client";
2+
3+
import type { SignInAuthFormSchema } from "@firebase-ui/core";
4+
import { useSignInAuthFormAction, useSignInAuthFormSchema, useUI, SignInAuthFormProps } from "@firebase-ui/react";
5+
import { useForm } from "react-hook-form";
6+
import { standardSchemaResolver } from "@hookform/resolvers/standard-schema";
7+
import { FirebaseUIError, getTranslation } from "@firebase-ui/core";
8+
9+
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
10+
import { Input } from "@/components/ui/input";
11+
import { Button } from "@/components/ui/button";
12+
import { Policies } from "./policies";
13+
14+
export type { SignInAuthFormProps };
15+
16+
export function SignInAuthForm(props: SignInAuthFormProps) {
17+
const ui = useUI();
18+
const schema = useSignInAuthFormSchema();
19+
const action = useSignInAuthFormAction();
20+
21+
const form = useForm<SignInAuthFormSchema>({
22+
resolver: standardSchemaResolver(schema),
23+
defaultValues: {
24+
email: "",
25+
password: "",
26+
},
27+
});
28+
29+
async function onSubmit(values: SignInAuthFormSchema) {
30+
try {
31+
const credential = await action(values);
32+
props.onSignIn?.(credential);
33+
} catch (error) {
34+
const message = error instanceof FirebaseUIError ? error.message : String(error);
35+
form.setError("root", { message });
36+
}
37+
}
38+
39+
return (
40+
<Form {...form}>
41+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
42+
<FormField
43+
control={form.control}
44+
name="email"
45+
render={({ field }) => (
46+
<FormItem>
47+
<FormLabel>{getTranslation(ui, "labels", "emailAddress")}</FormLabel>
48+
<FormControl>
49+
<Input {...field} type="email" />
50+
</FormControl>
51+
<FormMessage />
52+
</FormItem>
53+
)}
54+
/>
55+
<FormField
56+
control={form.control}
57+
name="password"
58+
render={({ field }) => (
59+
<FormItem>
60+
<FormLabel>{getTranslation(ui, "labels", "password")}</FormLabel>
61+
<FormControl>
62+
<div className="flex items-center gap-2">
63+
<Input {...field} type="password" className="flex-grow" />
64+
{props.onForgotPasswordClick ? (
65+
<Button type="button" variant="secondary" onClick={props.onForgotPasswordClick}>
66+
{getTranslation(ui, "labels", "forgotPassword")}
67+
</Button>
68+
) : null}
69+
</div>
70+
</FormControl>
71+
<FormMessage />
72+
</FormItem>
73+
)}
74+
/>
75+
<Policies />
76+
<Button type="submit" disabled={ui.state !== "idle"}>
77+
{getTranslation(ui, "labels", "signIn")}
78+
</Button>
79+
{form.formState.errors.root && <FormMessage>{form.formState.errors.root.message}</FormMessage>}
80+
{props.onRegisterClick ? (
81+
<>
82+
<Button type="button" variant="secondary" onClick={props.onRegisterClick}>
83+
{getTranslation(ui, "prompts", "noAccount")} {getTranslation(ui, "labels", "register")}
84+
</Button>
85+
</>
86+
) : null}
87+
</form>
88+
</Form>
89+
);
90+
}

0 commit comments

Comments
 (0)