chore: some refactor and cleaning

This commit is contained in:
2024-12-30 06:54:01 +01:00
parent 5c75e9390e
commit e39026c259
15 changed files with 5029 additions and 7233 deletions

View File

@@ -1,12 +1,13 @@
ADMIN_EMAIL="" ADMIN_EMAIL=""
ANTHROPIC_API_KEY=""
CRON_SECRET="" CRON_SECRET=""
HOME_URL="" HOME_URL=""
MAINTENANCE_MODE="" MAINTENANCE_MODE="0"
NEWS_LIMIT="" NEWS_LIMIT="50"
NEWS_TO_USE="10"
NEXT_PUBLIC_BRAND_COUNTRY="" NEXT_PUBLIC_BRAND_COUNTRY=""
NEXT_PUBLIC_BRAND_EMAIL="" NEXT_PUBLIC_BRAND_EMAIL=""
NEXT_PUBLIC_BRAND_NAME="" NEXT_PUBLIC_BRAND_NAME=""
NX_DAEMON=""
POSTGRES_DATABASE="" POSTGRES_DATABASE=""
POSTGRES_HOST="" POSTGRES_HOST=""
POSTGRES_PASSWORD="" POSTGRES_PASSWORD=""
@@ -14,26 +15,11 @@ POSTGRES_PRISMA_URL=""
POSTGRES_URL="" POSTGRES_URL=""
POSTGRES_URL_NON_POOLING="" POSTGRES_URL_NON_POOLING=""
POSTGRES_USER="" POSTGRES_USER=""
RESEND_AUDIENCE=""
RESEND_FROM="" RESEND_FROM=""
RESEND_KEY="" RESEND_KEY=""
SECRET_HASH="" SECRET_HASH=""
TURBO_REMOTE_ONLY="" SMTP_FROM=""
TURBO_RUN_SUMMARY="" SMTP_HOST=""
VERCEL="1" SMTP_PASSWORD=""
VERCEL_ENV="development" SMTP_PORT=""
VERCEL_GIT_COMMIT_AUTHOR_LOGIN="" SMTP_USER=""
VERCEL_GIT_COMMIT_AUTHOR_NAME=""
VERCEL_GIT_COMMIT_MESSAGE=""
VERCEL_GIT_COMMIT_REF=""
VERCEL_GIT_COMMIT_SHA=""
VERCEL_GIT_PREVIOUS_SHA=""
VERCEL_GIT_PROVIDER=""
VERCEL_GIT_PULL_REQUEST_ID=""
VERCEL_GIT_REPO_ID=""
VERCEL_GIT_REPO_OWNER=""
VERCEL_GIT_REPO_SLUG=""
VERCEL_URL=""
ANALYZE="false"
ANTHROPIC_API_KEY=""
NEWS_TO_USE=10

View File

@@ -9,13 +9,12 @@ import {
} from '@utils/statusCodes'; } from '@utils/statusCodes';
import { ConfirmationSchema, ResponseType } from '@utils/validationSchemas'; import { ConfirmationSchema, ResponseType } from '@utils/validationSchemas';
import { NextRequest } from 'next/server'; import { NextRequest } from 'next/server';
import { Resend } from 'resend';
export const dynamic = 'force-dynamic'; // defaults to force-static export const dynamic = 'force-dynamic'; // defaults to force-static
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
if (!process.env.RESEND_KEY || !process.env.RESEND_AUDIENCE) { if (!process.env.RESEND_KEY) {
throw new Error('Resend variables not set'); throw new Error('Resend variables not set');
} }
const body = await request.json(); const body = await request.json();
@@ -31,14 +30,6 @@ export async function POST(request: NextRequest) {
}); });
if (user) { if (user) {
const resend = new Resend(process.env.RESEND_KEY);
await resend.contacts.update({
id: user.resendId,
audienceId: process.env.RESEND_AUDIENCE,
unsubscribed: false
});
await prisma.user.update({ await prisma.user.update({
where: { where: {
code: validation.data.code code: validation.data.code

View File

@@ -1,4 +1,5 @@
import prisma from '@prisma/prisma'; import prisma from '@prisma/prisma';
import axios from 'axios';
import { formatApiResponse } from '@utils/formatApiResponse'; import { formatApiResponse } from '@utils/formatApiResponse';
import { import {
INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR,
@@ -19,17 +20,22 @@ export async function GET(request: NextRequest) {
} }
try { try {
const topStories: number[] = await fetch(getTopNews, { const { data: topStories } = await axios.get<number[]>(getTopNews, {
cache: 'no-store' headers: {
}).then(res => res.json()); 'Cache-Control': 'no-store'
}
});
console.info(`Top stories ids: ${topStories}`); console.info(`Top stories ids: ${topStories}`);
const newsPromises = topStories const newsPromises = topStories
.slice(0, Number(process.env.NEWS_LIMIT)) .slice(0, Number(process.env.NEWS_LIMIT))
.map(id => fetch(getSingleNews(id)).then(res => res.json())); .map(id => axios.get<NewsDatabaseType>(getSingleNews(id)));
const news: NewsDatabaseType[] = await Promise.all(newsPromises); const newsResponses = await Promise.all(newsPromises);
const news: NewsDatabaseType[] = newsResponses.map(
response => response.data
);
const upsertPromises = news.map(async getSingleNews => { const upsertPromises = news.map(async getSingleNews => {
const validation = NewsDatabaseSchema.safeParse(getSingleNews); const validation = NewsDatabaseSchema.safeParse(getSingleNews);
@@ -81,7 +87,11 @@ export async function GET(request: NextRequest) {
`Imported ${newsPromises.length} news.` `Imported ${newsPromises.length} news.`
); );
} catch (error) { } catch (error) {
console.error(error); if (axios.isAxiosError(error)) {
console.error('Axios error:', error.response?.data || error.message);
} else {
console.error('Error:', error);
}
return formatApiResponse( return formatApiResponse(
STATUS_INTERNAL_SERVER_ERROR, STATUS_INTERNAL_SERVER_ERROR,
INTERNAL_SERVER_ERROR INTERNAL_SERVER_ERROR

View File

@@ -25,9 +25,6 @@ export async function GET(request: NextRequest) {
} }
try { try {
// send newsletter to users who didn't get it in the last 23h 50m, assuming a cron job every 10 minutes
// this is to avoid sending the newsletter to the same users multiple times
// this is not a perfect solution, but it's good enough for now
const users = await prisma.user.findMany({ const users = await prisma.user.findMany({
where: { where: {
confirmed: true, confirmed: true,

View File

@@ -12,11 +12,10 @@ import {
import { ResponseType, SubscribeFormSchema } from '@utils/validationSchemas'; import { ResponseType, SubscribeFormSchema } from '@utils/validationSchemas';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { NextRequest } from 'next/server'; import { NextRequest } from 'next/server';
import { Resend } from 'resend';
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
if (!process.env.RESEND_KEY || !process.env.RESEND_AUDIENCE) { if (!process.env.RESEND_KEY) {
throw new Error('RESEND_KEY is not set'); throw new Error('RESEND_KEY is not set');
} }
@@ -36,8 +35,6 @@ export async function POST(request: NextRequest) {
} }
}); });
const resend = new Resend(process.env.RESEND_KEY);
const code = crypto const code = crypto
.createHash('sha256') .createHash('sha256')
.update(`${process.env.SECRET_HASH}${email}}`) .update(`${process.env.SECRET_HASH}${email}}`)
@@ -53,19 +50,6 @@ export async function POST(request: NextRequest) {
deleted: false deleted: false
} }
}); });
const contact = await resend.contacts.get({
id: user.resendId,
audienceId: process.env.RESEND_AUDIENCE
});
if (!contact) {
await resend.contacts.update({
id: user.resendId,
audienceId: process.env.RESEND_AUDIENCE,
unsubscribed: true
});
}
} }
const message: ResponseType = { const message: ResponseType = {
@@ -84,21 +68,10 @@ export async function POST(request: NextRequest) {
} }
}); });
} else { } else {
const contact = await resend.contacts.create({
email: email,
audienceId: process.env.RESEND_AUDIENCE,
unsubscribed: true
});
if (!contact.data?.id) {
throw new Error('Failed to create Resend contact');
}
await prisma.user.create({ await prisma.user.create({
data: { data: {
email, email,
code, code
resendId: contact.data.id
} }
}); });
} }

View File

@@ -11,14 +11,11 @@ import {
} from '@utils/statusCodes'; } from '@utils/statusCodes';
import { ResponseType, UnsubscribeFormSchema } from '@utils/validationSchemas'; import { ResponseType, UnsubscribeFormSchema } from '@utils/validationSchemas';
import { NextRequest } from 'next/server'; import { NextRequest } from 'next/server';
import { Resend } from 'resend';
export const dynamic = 'force-dynamic'; // defaults to force-static
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
if (!process.env.RESEND_KEY || !process.env.RESEND_AUDIENCE) { if (!process.env.RESEND_KEY) {
throw new Error('RESEND_AUDIENCE is not set'); throw new Error('Resend variables not set');
} }
const body = await request.json(); const body = await request.json();
const validation = UnsubscribeFormSchema.safeParse(body); const validation = UnsubscribeFormSchema.safeParse(body);
@@ -44,14 +41,6 @@ export async function POST(request: NextRequest) {
} }
}); });
const resend = new Resend(process.env.RESEND_KEY);
await resend.contacts.update({
id: user.resendId,
audienceId: process.env.RESEND_AUDIENCE,
unsubscribed: true
});
const sent = await sender([email], UnsubscribeTemplate()); const sent = await sender([email], UnsubscribeTemplate());
if (!sent) { if (!sent) {

View File

@@ -4,6 +4,7 @@ import { CardDescription } from '@components/Card';
import { CustomCard } from '@components/CustomCard'; import { CustomCard } from '@components/CustomCard';
import { SchemaOrg } from '@components/SchemaOrg'; import { SchemaOrg } from '@components/SchemaOrg';
import { ResponseType } from '@utils/validationSchemas'; import { ResponseType } from '@utils/validationSchemas';
import axios from 'axios';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { Suspense, useEffect, useState } from 'react'; import { Suspense, useEffect, useState } from 'react';
@@ -23,32 +24,31 @@ const ConfirmationPage = () => {
} }
try { try {
const res = await fetch('/api/confirmation', { const { data } = await axios.post<ResponseType>(
method: 'POST', '/api/confirmation',
headers: { {
'Content-Type': 'application/json'
},
body: JSON.stringify({
code: code code: code
}) },
}); {
headers: {
'Content-Type': 'application/json'
}
}
);
if (!res.ok) { if (!data.success) {
router.push('/'); router.push('/');
return; return;
} }
const response: ResponseType = await res.json(); setMessage(data.message);
if (!response.success) {
router.push('/');
return;
}
setMessage(response.message);
setLoading(false); setLoading(false);
} catch (error) { } catch (error) {
console.error(error); if (axios.isAxiosError(error)) {
console.error('Axios error:', error.response?.data || error.message);
} else {
console.error('Error:', error);
}
router.push('/'); router.push('/');
} }
}; };

View File

@@ -18,6 +18,7 @@ import {
} from '@utils/validationSchemas'; } from '@utils/validationSchemas';
import { useState } from 'react'; import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import axios from 'axios';
export const Home = () => { export const Home = () => {
const [completed, setCompleted] = useState(false); const [completed, setCompleted] = useState(false);
@@ -45,29 +46,28 @@ export const Home = () => {
async function handleSubmit(values: SubscribeFormType) { async function handleSubmit(values: SubscribeFormType) {
setIsLoading(true); setIsLoading(true);
try { try {
const response = await fetch('/api/subscribe', { const { data } = await axios.post<ResponseType>(
method: 'POST', '/api/subscribe',
headers: { {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: values.email email: values.email
}) },
}); {
headers: {
'Content-Type': 'application/json'
}
}
);
if (!response?.ok) { if (!data.success) {
throw new Error(`Invalid response: ${response.status}`); throw new Error(data.message);
} }
const formResponse: ResponseType = await response.json(); setMessage(data.message);
if (!formResponse.success) {
throw Error(formResponse.message);
}
setMessage(formResponse.message);
setCompleted(true); setCompleted(true);
} catch (error) { } catch (error) {
if (axios.isAxiosError(error)) {
console.error('Axios error:', error.response?.data || error.message);
}
setError(true); setError(true);
} finally { } finally {
setIsLoading(false); setIsLoading(false);

View File

@@ -16,6 +16,8 @@ import {
UnsubscribeFormSchema, UnsubscribeFormSchema,
UnsubscribeFormType UnsubscribeFormType
} from '@utils/validationSchemas'; } from '@utils/validationSchemas';
import axios from 'axios';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
@@ -52,29 +54,28 @@ const Unsubscribe = () => {
async function handleSubmit(values: UnsubscribeFormType) { async function handleSubmit(values: UnsubscribeFormType) {
setIsLoading(true); setIsLoading(true);
try { try {
const response = await fetch('/api/unsubscribe', { const { data } = await axios.post<ResponseType>(
method: 'POST', '/api/unsubscribe',
headers: { {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: values.email email: values.email
}) },
}); {
headers: {
'Content-Type': 'application/json'
}
}
);
if (!response?.ok) { if (!data.success) {
throw new Error(`Invalid response: ${response.status}`); throw new Error(data.message);
} }
const formResponse: ResponseType = await response.json(); setMessage(data.message);
if (!formResponse.success) {
throw Error(formResponse.message);
}
setMessage(formResponse.message);
setCompleted(true); setCompleted(true);
} catch (error) { } catch (error) {
if (axios.isAxiosError(error)) {
console.error('Axios error:', error.response?.data || error.message);
}
setError(true); setError(true);
} finally { } finally {
setIsLoading(false); setIsLoading(false);

View File

@@ -4,6 +4,7 @@ import { NewsTileType } from '@utils/validationSchemas';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { Tile } from './components/Tile'; import { Tile } from './components/Tile';
import axios from 'axios';
interface TilesProps { interface TilesProps {
children: React.ReactNode; children: React.ReactNode;
@@ -22,11 +23,19 @@ export const Tiles = ({ children }: TilesProps) => {
useEffect(() => { useEffect(() => {
async function getNews() { async function getNews() {
const news: NewsTileType[] = await fetch('/api/news').then(res => try {
res.json() const { data: news } = await axios.get<NewsTileType[]>('/api/news');
); setNews(news);
} catch (error) {
setNews(news); if (axios.isAxiosError(error)) {
console.error(
'Failed to fetch news:',
error.response?.data || error.message
);
} else {
console.error('Failed to fetch news:', error);
}
}
} }
if (!news) { if (!news) {

View File

@@ -28,6 +28,7 @@
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@vercel/analytics": "^1.1.1", "@vercel/analytics": "^1.1.1",
"axios": "^1.7.9",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"isomorphic-dompurify": "^2.15.0", "isomorphic-dompurify": "^2.15.0",

View File

@@ -0,0 +1,8 @@
/*
Warnings:
- You are about to drop the column `resendId` on the `users` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "users" DROP COLUMN "resendId";

View File

@@ -9,13 +9,12 @@ datasource db {
} }
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
resendId String email String @unique
email String @unique code String @unique
code String @unique confirmed Boolean @default(false)
confirmed Boolean @default(false) deleted Boolean @default(false)
deleted Boolean @default(false) createdAt DateTime @default(now())
createdAt DateTime @default(now())
lastMail DateTime? lastMail DateTime?
@@map(name: "users") @@map(name: "users")

View File

@@ -20,7 +20,7 @@ export async function getMessage<T>(text: string, tool: BaseTool) {
try { try {
const data = response.content as [ const data = response.content as [
{ type: string; text: string }, { type: string; text: string },
{ type: string; input: object } // object of type V { type: string; input: object }
]; ];
return data[1].input as T; return data[1].input as T;

12000
yarn.lock

File diff suppressed because it is too large Load Diff