Skip to content

Commit 9d4f148

Browse files
committed
feat: add welcome email functionality and integrate @vercel/functions for improved session handling
1 parent cc9f366 commit 9d4f148

File tree

4 files changed

+228
-140
lines changed

4 files changed

+228
-140
lines changed

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"@supabase/supabase-js": "2.52.0",
3535
"@tanstack/react-query": "5.84.1",
3636
"@tech-companies-portugal/analytics": "*",
37+
"@vercel/functions": "3.1.1",
3738
"cheerio": "1.0.0-rc.12",
3839
"class-variance-authority": "0.7.1",
3940
"clsx": "2.1.0",
Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,73 @@
1+
import WelcomeEmail from "@/emails/templates/welcome";
2+
import { emailService } from "@/lib/email";
13
import { createClient } from "@/lib/supabase/server";
4+
import { render } from "@react-email/components";
5+
import { waitUntil } from "@vercel/functions";
6+
import { differenceInSeconds } from "date-fns";
27
import { NextResponse } from "next/server";
38

49
export async function GET(request: Request) {
510
const { searchParams, origin } = new URL(request.url);
611
const code = searchParams.get("code");
12+
// if "next" is in params, use it as the redirect URL
713
const next = searchParams.get("next") ?? "/";
814

915
if (code) {
1016
const supabase = await createClient();
11-
const { error } = await supabase.auth.exchangeCodeForSession(code);
17+
const { error, data } = await supabase.auth.exchangeCodeForSession(code);
1218

13-
if (error) {
14-
console.error("Error exchanging code for session:", error);
15-
return NextResponse.redirect(`${origin}/auth/auth-code-error`);
19+
if (!error) {
20+
const forwardedHost = request.headers.get("x-forwarded-host");
21+
const isLocalEnv = process.env.NODE_ENV === "development";
22+
23+
// if the user was created in the last 15 seconds, send the welcome email
24+
if (
25+
data?.session?.user.created_at &&
26+
differenceInSeconds(
27+
new Date(),
28+
new Date(data.session.user.created_at),
29+
) < 15 &&
30+
data?.session?.user.email
31+
) {
32+
// this will be run in the background
33+
waitUntil(
34+
sendWelcomeEmail(
35+
data?.session?.user.email,
36+
data?.session?.user.user_metadata.full_name,
37+
),
38+
);
39+
}
40+
41+
if (isLocalEnv) {
42+
// we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
43+
return NextResponse.redirect(`${origin}${next}`);
44+
}
45+
46+
if (forwardedHost) {
47+
// in case we user load balancer or proxy, we need to redirect to the correct host
48+
return NextResponse.redirect(`https://${forwardedHost}${next}`);
49+
}
50+
51+
return NextResponse.redirect(`${origin}${next}`);
1652
}
17-
}
1853

19-
return NextResponse.redirect(`${origin}${next}`);
54+
// if there is an error, redirect to the auth code error page with possible instructions
55+
return NextResponse.redirect(`${origin}/auth/auth-code-error`);
56+
}
2057
}
58+
59+
const sendWelcomeEmail = async (email: string, name: string) => {
60+
const emailHtml = await render(
61+
WelcomeEmail({
62+
userFirstname: name,
63+
}),
64+
);
65+
66+
emailService.sendEmail({
67+
to: email,
68+
// Default is: Tech Companies Portugal <[email protected]>
69+
subject: "Welcome to Tech Companies Portugal",
70+
body: emailHtml,
71+
subscribed: true,
72+
});
73+
};

apps/web/src/emails/templates/welcome.tsx

Lines changed: 137 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -15,155 +15,158 @@ import { Footer } from "../components/footer";
1515
import { Logo } from "../components/logo";
1616

1717
interface WelcomeEmailProps {
18-
userFirstname: string;
18+
userFirstname?: string;
1919
}
2020

21-
export const WelcomeEmail = ({ userFirstname }: WelcomeEmailProps) => (
22-
<Html>
23-
<Head>
24-
<Font
25-
fontFamily="Geist"
26-
fallbackFontFamily="Helvetica"
27-
webFont={{
28-
url: "https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap",
29-
format: "woff2",
30-
}}
31-
fontWeight={400}
32-
fontStyle="normal"
33-
/>
34-
</Head>
35-
<Preview>
36-
Welcome to Tech Companies Portugal - Discover your next career
37-
opportunity!
38-
</Preview>
39-
<Tailwind>
40-
<Body className="bg-slate-50">
41-
<Container className="flex flex-col max-w-[600px] mx-auto p-4">
42-
{/* Header Section */}
43-
<Section
44-
className="bg-white border-2 border-slate-200 rounded-none p-4 mb-5"
45-
style={{
46-
border: "2px solid #e2e8f0",
47-
backgroundColor: "#ffffff",
48-
borderBottom: "5px solid #f1f5f9",
49-
borderRight: "5px solid #f1f5f9",
50-
padding: "16px",
51-
marginBottom: "20px",
52-
}}
53-
>
54-
<Logo />
21+
export default function WelcomeEmail({
22+
userFirstname = "there",
23+
}: WelcomeEmailProps) {
24+
return (
25+
<Html>
26+
<Head>
27+
<Font
28+
fontFamily="Geist"
29+
fallbackFontFamily="Helvetica"
30+
webFont={{
31+
url: "https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap",
32+
format: "woff2",
33+
}}
34+
fontWeight={400}
35+
fontStyle="normal"
36+
/>
37+
</Head>
38+
<Preview>
39+
Welcome to Tech Companies Portugal - Discover your next career
40+
opportunity!
41+
</Preview>
42+
<Tailwind>
43+
<Body className="bg-slate-50">
44+
<Container className="flex flex-col max-w-[600px] mx-auto p-4">
45+
{/* Header Section */}
46+
<Section
47+
className="bg-white border-2 border-slate-200 rounded-none p-4 mb-5"
48+
style={{
49+
border: "2px solid #e2e8f0",
50+
backgroundColor: "#ffffff",
51+
borderBottom: "5px solid #f1f5f9",
52+
borderRight: "5px solid #f1f5f9",
53+
padding: "16px",
54+
marginBottom: "20px",
55+
}}
56+
>
57+
<Logo />
5558

56-
<Heading className="text-3xl font-bold text-gray-800 text-center mt-4 mb-4 leading-tight">
57-
Welcome to Tech Companies Portugal! 🇵🇹
58-
</Heading>
59+
<Heading className="text-3xl font-bold text-gray-800 text-center mt-4 mb-4 leading-tight">
60+
Welcome to Tech Companies Portugal! 🇵🇹
61+
</Heading>
5962

60-
<Text className="text-lg text-gray-600 text-center leading-relaxed">
61-
Hi {userFirstname}, thanks for joining the community 🎉
62-
</Text>
63-
</Section>
63+
<Text className="text-lg text-gray-600 text-center leading-relaxed">
64+
Hi {userFirstname}, thanks for joining the community 🎉
65+
</Text>
66+
</Section>
6467

65-
{/* Main Content */}
66-
<Section
67-
className="bg-orange-100 border-2 border-orange-200 rounded-none p-4 mb-5"
68-
style={{
69-
border: "2px solid #fed7aa",
70-
backgroundColor: "#ffedd5",
71-
borderBottom: "5px solid #fed7aa",
72-
borderRight: "5px solid #fed7aa",
73-
padding: "16px",
74-
marginBottom: "20px",
75-
}}
76-
>
77-
<Text className="text-base text-gray-700 leading-relaxed mb-6">
78-
🚀 <strong>Ready to start exploring?</strong>
79-
</Text>
68+
{/* Main Content */}
69+
<Section
70+
className="bg-orange-100 border-2 border-orange-200 rounded-none p-4 mb-5"
71+
style={{
72+
border: "2px solid #fed7aa",
73+
backgroundColor: "#ffedd5",
74+
borderBottom: "5px solid #fed7aa",
75+
borderRight: "5px solid #fed7aa",
76+
padding: "16px",
77+
marginBottom: "20px",
78+
}}
79+
>
80+
<Text className="text-base text-gray-700 leading-relaxed mb-6">
81+
🚀 <strong>Ready to start exploring?</strong>
82+
</Text>
8083

81-
<Text className="text-sm text-gray-600 leading-relaxed mb-3 pl-5">
82-
• Explore <strong>300+</strong> tech companies across Portugal
83-
</Text>
84+
<Text className="text-sm text-gray-600 leading-relaxed mb-3 pl-5">
85+
• Explore <strong>300+</strong> tech companies across Portugal
86+
</Text>
8487

85-
<Text className="text-sm text-gray-600 leading-relaxed mb-3 pl-5">
86-
• Filter by location, category and more!
87-
</Text>
88+
<Text className="text-sm text-gray-600 leading-relaxed mb-3 pl-5">
89+
• Filter by location, category and more!
90+
</Text>
8891

89-
<Text className="text-sm text-gray-600 leading-relaxed pl-5">
90-
• Reveive notifications when new companies are added (coming soon)
91-
</Text>
92+
<Text className="text-sm text-gray-600 leading-relaxed pl-5">
93+
• Reveive notifications when new companies are added (coming
94+
soon)
95+
</Text>
9296

93-
<Section className="text-center py-4">
94-
<Button
95-
href="https://techcompaniesportugal.fyi"
96-
className="bg-white text-slate-900 text-base font-semibold no-underline py-3.5 px-7 rounded-none border-2 border-slate-900 inline-block"
97-
style={{
98-
backgroundColor: "#ffffff",
99-
color: "#0f172a",
100-
textDecoration: "none",
101-
fontSize: "16px",
102-
fontWeight: "600",
103-
padding: "14px 28px",
104-
border: "2px solid #0f172a",
105-
borderBottom: "5px solid #0f172a",
106-
borderRight: "5px solid #0f172a",
107-
display: "inline-block",
108-
}}
109-
>
110-
Explore Companies Now →
111-
</Button>
97+
<Section className="text-center py-4">
98+
<Button
99+
href="https://techcompaniesportugal.fyi"
100+
className="bg-white text-slate-900 text-base font-semibold no-underline py-3.5 px-7 rounded-none border-2 border-slate-900 inline-block"
101+
style={{
102+
backgroundColor: "#ffffff",
103+
color: "#0f172a",
104+
textDecoration: "none",
105+
fontSize: "16px",
106+
fontWeight: "600",
107+
padding: "14px 28px",
108+
border: "2px solid #0f172a",
109+
borderBottom: "5px solid #0f172a",
110+
borderRight: "5px solid #0f172a",
111+
display: "inline-block",
112+
}}
113+
>
114+
Explore Companies Now →
115+
</Button>
116+
</Section>
112117
</Section>
113-
</Section>
114118

115-
{/* Features Section */}
116-
<Section
117-
className="bg-white border-2 border-slate-200 rounded-none p-4 mb-5"
118-
style={{
119-
border: "2px solid #e2e8f0",
120-
backgroundColor: "#ffffff",
121-
borderBottom: "5px solid #f1f5f9",
122-
borderRight: "5px solid #f1f5f9",
123-
padding: "16px",
124-
marginBottom: "20px",
125-
}}
126-
>
127-
<Heading className="text-xl font-semibold text-gray-800 mb-5">
128-
Open source 💙
129-
</Heading>
119+
{/* Features Section */}
120+
<Section
121+
className="bg-white border-2 border-slate-200 rounded-none p-4 mb-5"
122+
style={{
123+
border: "2px solid #e2e8f0",
124+
backgroundColor: "#ffffff",
125+
borderBottom: "5px solid #f1f5f9",
126+
borderRight: "5px solid #f1f5f9",
127+
padding: "16px",
128+
marginBottom: "20px",
129+
}}
130+
>
131+
<Heading className="text-xl font-semibold text-gray-800 mb-5">
132+
Open source 💙
133+
</Heading>
130134

131-
<Text className="text-sm text-gray-600 leading-relaxed mb-4">
132-
🌐 <strong>Open Source Platform:</strong> Our web application is
133-
completely open source and transparent:{" "}
134-
<a
135-
href="https://github.com/alexmarqs/frontend-tech-companies-portugal"
136-
target="_blank"
137-
rel="noreferrer"
138-
>
139-
tech-companies-portugal-app
140-
</a>
141-
</Text>
135+
<Text className="text-sm text-gray-600 leading-relaxed mb-4">
136+
🌐 <strong>Open Source Platform:</strong> Our web application is
137+
completely open source and transparent:{" "}
138+
<a
139+
href="https://github.com/alexmarqs/frontend-tech-companies-portugal"
140+
target="_blank"
141+
rel="noreferrer"
142+
>
143+
tech-companies-portugal-app
144+
</a>
145+
</Text>
142146

143-
<Text className="text-sm text-gray-600 leading-relaxed mb-4">
144-
🤝 <strong>Community Powered Data:</strong> We source our company
145-
data from the amazing open source repository maintained by the
146-
Portuguese tech community, keeping it always fresh through their
147-
contributions:{" "}
148-
<a
149-
href="https://github.com/marmelo/tech-companies-in-portugal"
150-
target="_blank"
151-
rel="noreferrer"
152-
>
153-
tech-companies-in-portugal
154-
</a>
155-
</Text>
156-
</Section>
147+
<Text className="text-sm text-gray-600 leading-relaxed mb-4">
148+
🤝 <strong>Community Powered Data:</strong> We source our
149+
company data from the amazing open source repository maintained
150+
by the Portuguese tech community, keeping it always fresh
151+
through their contributions:{" "}
152+
<a
153+
href="https://github.com/marmelo/tech-companies-in-portugal"
154+
target="_blank"
155+
rel="noreferrer"
156+
>
157+
tech-companies-in-portugal
158+
</a>
159+
</Text>
160+
</Section>
157161

158-
<Footer />
159-
</Container>
160-
</Body>
161-
</Tailwind>
162-
</Html>
163-
);
162+
<Footer />
163+
</Container>
164+
</Body>
165+
</Tailwind>
166+
</Html>
167+
);
168+
}
164169

165170
WelcomeEmail.PreviewProps = {
166171
userFirstname: "Alex",
167172
} as WelcomeEmailProps;
168-
169-
export default WelcomeEmail;

0 commit comments

Comments
 (0)