Skip to content

Commit bbf8b9b

Browse files
Magic links (#199)
* wip on magic link support * Switch to nodemailer / resend for transactional mail * Further cleanup * Add stylized email using react-email * fix
1 parent f652ca5 commit bbf8b9b

File tree

21 files changed

+1519
-284
lines changed

21 files changed

+1519
-284
lines changed

packages/web/package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"build": "next build",
88
"start": "next start",
99
"lint": "next lint",
10-
"test": "vitest"
10+
"test": "vitest",
11+
"dev:emails": "email dev --dir ./src/emails"
1112
},
1213
"dependencies": {
1314
"@auth/prisma-adapter": "^2.7.4",
@@ -56,6 +57,8 @@
5657
"@radix-ui/react-toast": "^1.2.2",
5758
"@radix-ui/react-toggle": "^1.1.0",
5859
"@radix-ui/react-tooltip": "^1.1.4",
60+
"@react-email/components": "^0.0.33",
61+
"@react-email/render": "^1.0.5",
5962
"@replit/codemirror-lang-csharp": "^6.2.0",
6063
"@replit/codemirror-lang-nix": "^6.0.1",
6164
"@replit/codemirror-lang-solidity": "^6.0.2",
@@ -107,6 +110,7 @@
107110
"next": "14.2.21",
108111
"next-auth": "^5.0.0-beta.25",
109112
"next-themes": "^0.3.0",
113+
"nodemailer": "^6.10.0",
110114
"posthog-js": "^1.161.5",
111115
"pretty-bytes": "^6.1.1",
112116
"psl": "^1.15.0",
@@ -128,6 +132,7 @@
128132
"devDependencies": {
129133
"@types/bcrypt": "^5.0.2",
130134
"@types/node": "^20",
135+
"@types/nodemailer": "^6.4.17",
131136
"@types/psl": "^1.1.3",
132137
"@types/react": "^18",
133138
"@types/react-dom": "^18",
@@ -140,6 +145,7 @@
140145
"jsdom": "^25.0.1",
141146
"npm-run-all": "^4.1.5",
142147
"postcss": "^8",
148+
"react-email": "3.0.3",
143149
"tailwindcss": "^3.4.1",
144150
"tsx": "^4.19.2",
145151
"typescript": "^5",

packages/web/src/app/[domain]/components/navigationMenu.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@ import { Button } from "@/components/ui/button";
22
import { NavigationMenu as NavigationMenuBase, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
33
import Link from "next/link";
44
import { Separator } from "@/components/ui/separator";
5-
import Image from "next/image";
6-
import logoDark from "@/public/sb_logo_dark_small.png";
7-
import logoLight from "@/public/sb_logo_light_small.png";
85
import { SettingsDropdown } from "./settingsDropdown";
96
import { GitHubLogoIcon, DiscordLogoIcon } from "@radix-ui/react-icons";
107
import { redirect } from "next/navigation";
118
import { OrgSelector } from "./orgSelector";
129
import { getSubscriptionData } from "@/actions";
1310
import { isServiceError } from "@/lib/utils";
11+
import { SourcebotLogo } from "@/app/components/sourcebotLogo";
12+
1413
const SOURCEBOT_DISCORD_URL = "https://discord.gg/6Fhp27x7Pb";
1514
const SOURCEBOT_GITHUB_URL = "https://github.com/sourcebot-dev/sourcebot";
1615

@@ -31,17 +30,9 @@ export const NavigationMenu = async ({
3130
href={`/${domain}`}
3231
className="mr-3 cursor-pointer"
3332
>
34-
<Image
35-
src={logoDark}
36-
className="h-11 w-auto hidden dark:block"
37-
alt={"Sourcebot logo"}
38-
priority={true}
39-
/>
40-
<Image
41-
src={logoLight}
42-
className="h-11 w-auto block dark:hidden"
43-
alt={"Sourcebot logo"}
44-
priority={true}
33+
<SourcebotLogo
34+
className="h-11"
35+
size="small"
4536
/>
4637
</Link>
4738

packages/web/src/app/[domain]/components/payWall/paywallCard.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
22
import { Check } from "lucide-react"
33
import { EnterpriseContactUsButton } from "./enterpriseContactUsButton"
44
import { CheckoutButton } from "./checkoutButton"
5-
import Image from "next/image";
6-
import logoDark from "@/public/sb_logo_dark_large.png";
7-
import logoLight from "@/public/sb_logo_light_large.png";
5+
import { SourcebotLogo } from "@/app/components/sourcebotLogo";
86

97
const teamFeatures = [
108
"Index hundreds of repos from multiple code hosts (GitHub, GitLab, Gerrit, Gitea, etc.). Self-hosted code sources supported",
@@ -24,17 +22,9 @@ export async function PaywallCard({ domain }: { domain: string }) {
2422
return (
2523
<div className="max-w-4xl mx-auto px-4 py-8">
2624
<div className="max-h-44 w-auto mb-4 flex justify-center">
27-
<Image
28-
src={logoDark}
29-
className="h-18 md:h-40 w-auto hidden dark:block"
30-
alt={"Sourcebot logo"}
31-
priority={true}
32-
/>
33-
<Image
34-
src={logoLight}
35-
className="h-18 md:h-40 w-auto block dark:hidden"
36-
alt={"Sourcebot logo"}
37-
priority={true}
25+
<SourcebotLogo
26+
className="h-18 md:h-40"
27+
size="large"
3828
/>
3929
</div>
4030
<h2 className="text-3xl font-bold text-center mb-8 text-primary">

packages/web/src/app/[domain]/connections/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default function Layout({
1111
return (
1212
<div className="min-h-screen flex flex-col">
1313
<NavigationMenu domain={domain} />
14-
<main className="flex-grow flex justify-center p-4 bg-[#fafafa] dark:bg-background relative">
14+
<main className="flex-grow flex justify-center p-4 bg-backgroundSecondary relative">
1515
<div className="w-full max-w-6xl rounded-lg p-6">{children}</div>
1616
</main>
1717
</div>

packages/web/src/app/[domain]/page.tsx

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import { listRepositories } from "@/lib/server/searchService";
22
import { isServiceError } from "@/lib/utils";
3-
import Image from "next/image";
43
import { Suspense } from "react";
5-
import logoDark from "@/public/sb_logo_dark_large.png";
6-
import logoLight from "@/public/sb_logo_light_large.png";
74
import { NavigationMenu } from "./components/navigationMenu";
85
import { RepositoryCarousel } from "./components/repositoryCarousel";
96
import { SearchBar } from "./components/searchBar";
@@ -14,6 +11,7 @@ import Link from "next/link";
1411
import { getOrgFromDomain } from "@/data/org";
1512
import { PageNotFound } from "./components/pageNotFound";
1613
import { Footer } from "./components/footer";
14+
import { SourcebotLogo } from "../components/sourcebotLogo";
1715

1816

1917
export default async function Home({ params: { domain } }: { params: { domain: string } }) {
@@ -30,17 +28,8 @@ export default async function Home({ params: { domain } }: { params: { domain: s
3028
<UpgradeToast />
3129
<div className="flex flex-col justify-center items-center mt-8 mb-8 md:mt-18 w-full px-5">
3230
<div className="max-h-44 w-auto">
33-
<Image
34-
src={logoDark}
35-
className="h-18 md:h-40 w-auto hidden dark:block"
36-
alt={"Sourcebot logo"}
37-
priority={true}
38-
/>
39-
<Image
40-
src={logoLight}
41-
className="h-18 md:h-40 w-auto block dark:hidden"
42-
alt={"Sourcebot logo"}
43-
priority={true}
31+
<SourcebotLogo
32+
className="h-18 md:h-40 w-auto"
4433
/>
4534
</div>
4635
<SearchBar
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import logoDarkLarge from "@/public/sb_logo_dark_large.png";
2+
import logoLightLarge from "@/public/sb_logo_light_large.png";
3+
import logoDarkSmall from "@/public/sb_logo_dark_small.png";
4+
import logoLightSmall from "@/public/sb_logo_light_small.png";
5+
import Image from "next/image";
6+
import { cn } from "@/lib/utils";
7+
8+
interface SourcebotLogoProps {
9+
className?: string;
10+
size?: "small" | "large";
11+
}
12+
13+
export const SourcebotLogo = ({ className, size = "large" }: SourcebotLogoProps) => {
14+
return (
15+
<>
16+
<Image
17+
src={size === "large" ? logoDarkLarge : logoDarkSmall}
18+
className={cn("h-16 w-auto hidden dark:block", className)}
19+
alt={"Sourcebot logo"}
20+
priority={true}
21+
/>
22+
<Image
23+
src={size === "large" ? logoLightLarge : logoLightSmall}
24+
className={cn("h-16 w-auto block dark:hidden", className)}
25+
alt={"Sourcebot logo"}
26+
priority={true}
27+
/>
28+
</>
29+
)
30+
}

packages/web/src/app/globals.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
@layer base {
66
:root {
77
--background: 0 0% 100%;
8+
--background-secondary: 0, 0%, 98%;
89
--foreground: 222.2 84% 4.9%;
910
--card: 0 0% 100%;
1011
--card-foreground: 222.2 84% 4.9%;
@@ -42,6 +43,7 @@
4243

4344
.dark {
4445
--background: 222.2 84% 4.9%;
46+
--background-secondary: 222.2 84% 4.9%;
4547
--foreground: 210 40% 98%;
4648
--card: 222.2 84% 4.9%;
4749
--card-foreground: 210 40% 98%;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
'use client';
2+
3+
import { Button } from "@/components/ui/button";
4+
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
5+
import { Input } from "@/components/ui/input";
6+
import { useForm } from "react-hook-form";
7+
import { zodResolver } from "@hookform/resolvers/zod";
8+
import { z } from "zod";
9+
import { signIn } from "next-auth/react";
10+
import { verifyCredentialsRequestSchema } from "@/lib/schemas";
11+
import { useState } from "react";
12+
import { Loader2 } from "lucide-react";
13+
14+
interface CredentialsFormProps {
15+
callbackUrl?: string;
16+
}
17+
18+
export const CredentialsForm = ({ callbackUrl }: CredentialsFormProps) => {
19+
const [isLoading, setIsLoading] = useState(false);
20+
const form = useForm<z.infer<typeof verifyCredentialsRequestSchema>>({
21+
resolver: zodResolver(verifyCredentialsRequestSchema),
22+
defaultValues: {
23+
email: "",
24+
password: "",
25+
},
26+
});
27+
28+
const onSubmit = (values: z.infer<typeof verifyCredentialsRequestSchema>) => {
29+
setIsLoading(true);
30+
signIn("credentials", {
31+
email: values.email,
32+
password: values.password,
33+
redirectTo: callbackUrl ?? "/"
34+
})
35+
.finally(() => {
36+
setIsLoading(false);
37+
});
38+
}
39+
40+
return (
41+
<Form {...form}>
42+
<form
43+
onSubmit={form.handleSubmit(onSubmit)}
44+
className="w-full"
45+
>
46+
<FormField
47+
control={form.control}
48+
name="email"
49+
render={({ field }) => (
50+
<FormItem className="mb-4">
51+
<FormLabel>Email</FormLabel>
52+
<FormControl>
53+
<Input placeholder="[email protected]" {...field} />
54+
</FormControl>
55+
<FormMessage />
56+
</FormItem>
57+
)}
58+
/>
59+
<FormField
60+
control={form.control}
61+
name="password"
62+
render={({ field }) => (
63+
<FormItem className="mb-8">
64+
<FormLabel>Password</FormLabel>
65+
<FormControl>
66+
<Input type="password" {...field} />
67+
</FormControl>
68+
<FormMessage />
69+
</FormItem>
70+
)}
71+
/>
72+
<Button
73+
type="submit"
74+
className="w-full"
75+
variant="outline"
76+
disabled={isLoading}
77+
>
78+
{isLoading ? <Loader2 className="animate-spin mr-2" /> : ""}
79+
Sign in with credentials
80+
</Button>
81+
</form>
82+
</Form>
83+
);
84+
}

0 commit comments

Comments
 (0)