Skip to content

Commit

Permalink
feat: setup resend (#235)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jayllyz authored Jul 15, 2024
1 parent a43f671 commit 0e88653
Show file tree
Hide file tree
Showing 12 changed files with 358 additions and 23 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ ATHLONIX_API_URL=
NEXT_PUBLIC_API_URL=
STRIPE_API_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
RESEND_KEY=re_xxx
ENABLE_EMAILS='false'
16 changes: 11 additions & 5 deletions apps/admin/app/(dashboard)/dashboard/assemblies/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { Input } from '@ui/components/ui/input';
import { Label } from '@ui/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ui/components/ui/select';
import { toast } from '@ui/components/ui/sonner';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@ui/components/ui/table';
import { Textarea } from '@ui/components/ui/textarea';
import { EditIcon } from 'lucide-react';
Expand Down Expand Up @@ -46,10 +47,15 @@ export default function AssembliesPage(): JSX.Element {
async function handleAddAssembly(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
await createAssembly(formData);
const assemblies = await getAssemblies();
setAssemblies(assemblies.data);
setCount(assemblies.count);
try {
await createAssembly(formData);
const assemblies = await getAssemblies();
setAssemblies(assemblies.data);
setCount(assemblies.count);
toast.success("L'assemblée a été créée avec succès");
} catch (_error) {
toast.error("Erreur lors de la création de l'assemblée");
}
setOpen(false);
}

Expand Down Expand Up @@ -119,7 +125,7 @@ export default function AssembliesPage(): JSX.Element {
/>
</div>
<DialogFooter>
<Button type="submit" disabled={loading}>
<Button type="submit" disabled={loading || !location}>
Enregistrer
</Button>
</DialogFooter>
Expand Down
12 changes: 10 additions & 2 deletions apps/admin/app/(dashboard)/dashboard/assemblies/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,27 @@ export async function createAssembly(formData: FormData): Promise<void> {
const data = {
name: formData.get('name'),
description: formData.get('description'),
date: formData.get('date'),
date: new Date(formData.get('date') as string).toISOString(),
location: Number(formData.get('location')) || null,
lawsuit: null,
};
const token = cookies().get('access_token')?.value;
await fetch(`${urlApi}/assemblies`, {
if (!token) {
throw new Error('No token found');
}

const resp = await fetch(`${urlApi}/assemblies`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(data),
});

if (!resp.ok) {
throw new Error('Failed to create assembly');
}
}

export async function updateAssembly(formData: FormData, id: number): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion apps/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.4",
"tailwindcss": "^3.4.5",
"typescript": "^5.5.3"
}
}
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@repo/types": "workspace:*",
"@supabase/supabase-js": "^2.44.4",
"hono": "^4.4.13",
"resend": "^3.4.0",
"socket.io": "^4.7.5",
"stripe": "^16.2.0",
"zod": "^3.23.8",
Expand Down
5 changes: 5 additions & 0 deletions apps/api/src/handlers/assemblies.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import crypto from 'node:crypto';
import { OpenAPIHono } from '@hono/zod-openapi';
import { sendNewAssemblyEmail } from '../libs/email.js';
import { supabase } from '../libs/supabase.js';
import { zodErrorHook } from '../libs/zodError.js';
import {
Expand Down Expand Up @@ -97,6 +98,10 @@ assemblies.openapi(createAssembly, async (c) => {
closed: data.closed,
};

if (process.env.ENABLE_EMAILS === 'true') {
await sendNewAssemblyEmail(format.name, format.date, format.location);
}

return c.json(format, 201);
});

Expand Down
5 changes: 5 additions & 0 deletions apps/api/src/handlers/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { OpenAPIHono } from '@hono/zod-openapi';
import { deleteCookie, getCookie, setCookie } from 'hono/cookie';
import { HTTPException } from 'hono/http-exception';
import { sendWelcomeEmail } from '../libs/email.js';
import { supAdmin, supabase } from '../libs/supabase.js';
import { zodErrorHook } from '../libs/zodError.js';
import { loginUser, logoutUser, signupUser } from '../routes/auth.js';
Expand Down Expand Up @@ -48,6 +49,10 @@ auth.openapi(signupUser, async (c) => {
return c.json({ error: 'Error while creating user' }, 400);
}

if (process.env.ENABLE_EMAILS === 'true') {
await sendWelcomeEmail({ email: user.email, first_name: user.first_name });
}

return c.json(user, 201);
});

Expand Down
103 changes: 103 additions & 0 deletions apps/api/src/libs/email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import type { Database } from '@repo/types';
import { Resend } from 'resend';
import { supabase } from './supabase.js';

type location = Database['public']['Tables']['ADDRESSES']['Row'];
type user = Database['public']['Tables']['USERS']['Row'];

async function getApprovedMembers(): Promise<user[] | null> {
const selectedRoles = ['MEMBER'];
const { data, error } = await supabase
.from('USERS')
.select('*, roles:ROLES!inner(id, name)')
.filter('deleted_at', 'is', null)
.eq('status', 'approved')
.in('roles.name', selectedRoles);

if (error) {
return null;
}

return data;
}

export async function sendNewAssemblyEmail(name: string, date: string, location: location | null) {
const resend = new Resend(process.env.RESEND_KEY);

if (!resend.emails) {
throw new Error('Emails feature is not enabled');
}

const members = await getApprovedMembers();
if (!members) {
return;
}

const formattedDate = new Date(date).toLocaleDateString('fr-FR', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});

const address =
location === null
? 'En ligne'
: `${location?.number} ${location?.road}, ${location?.city} ${location?.postal_code}`;

for (const member of members) {
await resend.emails.send({
from: 'onboarding@resend.dev',
to: member.email,
subject: 'ATHLONIX - Nouvelle Assemblée Générale',
html: `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<h2>Nouvelle Assemblée Générale : ${name}</h2>
<p>Cher(e) ${member.first_name},</p>
<p>Nous avons le plaisir de vous informer qu'une nouvelle assemblée générale a été ajoutée au planning :</p>
<ul>
<li><strong>Date :</strong> ${formattedDate}</li>
<li><strong>Lieu :</strong> ${address}</li>
</ul>
<p>Votre présence est importante pour notre association. Si vous ne pouvez pas assister à cette assemblée, merci de nous en informer dès que possible.</p>
<p>Cordialement,<br>L'équipe d'Athlonix</p>
</body>
</html>
`,
});
}
}

export async function sendWelcomeEmail(user: { email: string; first_name: string }) {
const resend = new Resend(process.env.RESEND_KEY);

if (!resend.emails) {
throw new Error('Emails feature is not enabled');
}

await resend.emails.send({
from: 'onboarding@resend.dev',
to: user.email,
subject: 'Bienvenue chez Athlonix !',
html: `
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<h2>Bienvenue chez Athlonix !</h2>
<p>Cher(e) ${user.first_name},</p>
<p>Nous sommes ravis de vous accueillir au sein de notre association sportive. Votre compte a été créé avec succès.</p>
<h3>Prochaines étapes :</h3>
<ol>
<li>Complétez votre profil en ligne</li>
<li>Explorez notre calendrier d'événements</li>
<li>Prenez connaissance de nos activités et cours proposés</li>
<li>Souscrivez à notre abonnement annuel pour devenir membre officiel</li>
</ol>
<p>Si vous avez des questions, n'hésitez pas à contacter notre équipe de support à support@votre-association.fr.</p>
<p>Nous vous souhaitons une excellente expérience parmi nous !</p>
<p>Sportivement,<br>L'équipe d'Athlonix</p>
</body>
</html>
`,
});
}
2 changes: 1 addition & 1 deletion apps/api/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default defineConfig({
},
},
ignoreEmptyLines: true,
exclude: ['**/edm.ts', '**/stripe.ts', '**/storage.ts', ...coverageConfigDefaults.exclude],
exclude: ['**/edm.ts', '**/stripe.ts', '**/storage.ts', '**/email.ts', ...coverageConfigDefaults.exclude],
reporter: ['text', 'html', 'json', 'json-summary'],
},
},
Expand Down
2 changes: 1 addition & 1 deletion apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.4",
"tailwindcss": "^3.4.5",
"typescript": "^5.5.3"
}
}
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"autoprefixer": "^10.4.19",
"postcss": "^8.4.39",
"react": "^18.3.1",
"tailwindcss": "^3.4.4",
"tailwindcss": "^3.4.5",
"typescript": "^5.5.3"
},
"dependencies": {
Expand Down
Loading

0 comments on commit 0e88653

Please sign in to comment.