chore: code cleaning (#21)

This commit is contained in:
Riccardo Senica
2024-11-23 09:13:15 +01:00
committed by GitHub
parent c4f03feffe
commit c300b2501d
31 changed files with 903 additions and 872 deletions

View File

@@ -1,5 +1,5 @@
import prisma from '@prisma/prisma'; import prisma from '@prisma/prisma';
import { ApiResponse } from '@utils/apiResponse'; import { formatApiResponse } from '@utils/formatApiResponse';
import { import {
BAD_REQUEST, BAD_REQUEST,
INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR,
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
const body = await request.json(); const body = await request.json();
const validation = ConfirmationSchema.safeParse(body); const validation = ConfirmationSchema.safeParse(body);
if (!validation.success || !validation.data.code) { if (!validation.success || !validation.data.code) {
return ApiResponse(STATUS_BAD_REQUEST, BAD_REQUEST); return formatApiResponse(STATUS_BAD_REQUEST, BAD_REQUEST);
} }
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
@@ -53,10 +53,13 @@ export async function POST(request: NextRequest) {
message: `Thank you for confirming the subscription, ${user.email}!` message: `Thank you for confirming the subscription, ${user.email}!`
}; };
return ApiResponse(STATUS_OK, message); return formatApiResponse(STATUS_OK, message);
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return ApiResponse(STATUS_INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR); return formatApiResponse(
STATUS_INTERNAL_SERVER_ERROR,
INTERNAL_SERVER_ERROR
);
} }
} }

View File

@@ -1,5 +1,5 @@
import prisma from '@prisma/prisma'; import prisma from '@prisma/prisma';
import { ApiResponse } from '@utils/apiResponse'; import { formatApiResponse } from '@utils/formatApiResponse';
import { import {
INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR,
STATUS_INTERNAL_SERVER_ERROR, STATUS_INTERNAL_SERVER_ERROR,
@@ -15,7 +15,7 @@ export async function GET(request: NextRequest) {
if ( if (
request.headers.get('Authorization') !== `Bearer ${process.env.CRON_SECRET}` request.headers.get('Authorization') !== `Bearer ${process.env.CRON_SECRET}`
) { ) {
return ApiResponse(STATUS_UNAUTHORIZED, 'Unauthorized'); return formatApiResponse(STATUS_UNAUTHORIZED, 'Unauthorized');
} }
try { try {
@@ -76,9 +76,15 @@ export async function GET(request: NextRequest) {
}); });
} }
return ApiResponse(STATUS_OK, `Imported ${newsPromises.length} news.`); return formatApiResponse(
STATUS_OK,
`Imported ${newsPromises.length} news.`
);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return ApiResponse(STATUS_INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR); return formatApiResponse(
STATUS_INTERNAL_SERVER_ERROR,
INTERNAL_SERVER_ERROR
);
} }
} }

View File

@@ -1,7 +1,7 @@
import NewsletterTemplate from '@components/email/Newsletter'; import { NewsletterTemplate } from '@components/email/Newsletter';
import prisma from '@prisma/prisma'; import prisma from '@prisma/prisma';
import { ApiResponse } from '@utils/apiResponse'; import { formatApiResponse } from '@utils/formatApiResponse';
import { sender } from '@utils/sender'; import { sender } from '@utils/resendClient';
import { import {
INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR,
STATUS_INTERNAL_SERVER_ERROR, STATUS_INTERNAL_SERVER_ERROR,
@@ -17,7 +17,7 @@ export async function GET(request: NextRequest) {
if ( if (
request.headers.get('Authorization') !== `Bearer ${process.env.CRON_SECRET}` request.headers.get('Authorization') !== `Bearer ${process.env.CRON_SECRET}`
) { ) {
return ApiResponse(STATUS_UNAUTHORIZED, 'Unauthorized'); return formatApiResponse(STATUS_UNAUTHORIZED, 'Unauthorized');
} }
if (!process.env.NEWS_TO_USE) { if (!process.env.NEWS_TO_USE) {
@@ -52,7 +52,7 @@ export async function GET(request: NextRequest) {
console.info(`Found ${users.length} users to mail to.`); console.info(`Found ${users.length} users to mail to.`);
if (users.length === 0) { if (users.length === 0) {
return ApiResponse(STATUS_OK, 'No user to mail to.'); return formatApiResponse(STATUS_OK, 'No user to mail to.');
} }
const news = await prisma.news.findMany({ const news = await prisma.news.findMany({
@@ -70,7 +70,7 @@ export async function GET(request: NextRequest) {
console.info(`Found ${news.length} news to include in the newsletter.`); console.info(`Found ${news.length} news to include in the newsletter.`);
if (news.length === 0) { if (news.length === 0) {
return ApiResponse(STATUS_OK, 'No news to include in newsletter.'); return formatApiResponse(STATUS_OK, 'No news to include in newsletter.');
} }
const validRankedNews = news.sort((a, b) => b.score - a.score); const validRankedNews = news.sort((a, b) => b.score - a.score);
@@ -83,7 +83,10 @@ export async function GET(request: NextRequest) {
); );
if (!sent) { if (!sent) {
return ApiResponse(STATUS_INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR); return formatApiResponse(
STATUS_INTERNAL_SERVER_ERROR,
INTERNAL_SERVER_ERROR
);
} }
// update users so they don't get the newsletter again // update users so they don't get the newsletter again
@@ -98,12 +101,15 @@ export async function GET(request: NextRequest) {
} }
}); });
return ApiResponse( return formatApiResponse(
STATUS_OK, STATUS_OK,
`Newsletter sent to ${users.length} addresses.` `Newsletter sent to ${users.length} addresses.`
); );
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return ApiResponse(STATUS_INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR); return formatApiResponse(
STATUS_INTERNAL_SERVER_ERROR,
INTERNAL_SERVER_ERROR
);
} }
} }

View File

@@ -1,5 +1,5 @@
import prisma from '@prisma/prisma'; import prisma from '@prisma/prisma';
import { ApiResponse } from '@utils/apiResponse'; import { formatApiResponse } from '@utils/formatApiResponse';
import { import {
INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR,
STATUS_INTERNAL_SERVER_ERROR, STATUS_INTERNAL_SERVER_ERROR,
@@ -21,10 +21,13 @@ export async function GET() {
}); });
if (news) { if (news) {
return ApiResponse(STATUS_OK, news); return formatApiResponse(STATUS_OK, news);
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return ApiResponse(STATUS_INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR); return formatApiResponse(
STATUS_INTERNAL_SERVER_ERROR,
INTERNAL_SERVER_ERROR
);
} }
} }

View File

@@ -1,7 +1,7 @@
import ConfirmationTemplate from '@components/email/Confirmation'; import { ConfirmationTemplate } from '@components/email/Confirmation';
import prisma from '@prisma/prisma'; import prisma from '@prisma/prisma';
import { ApiResponse } from '@utils/apiResponse'; import { formatApiResponse } from '@utils/formatApiResponse';
import { sender } from '@utils/sender'; import { sender } from '@utils/resendClient';
import { import {
BAD_REQUEST, BAD_REQUEST,
INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR,
@@ -25,7 +25,7 @@ export async function POST(request: NextRequest) {
const validation = SubscribeFormSchema.safeParse(body); const validation = SubscribeFormSchema.safeParse(body);
if (!validation.success) { if (!validation.success) {
return ApiResponse(STATUS_BAD_REQUEST, BAD_REQUEST); return formatApiResponse(STATUS_BAD_REQUEST, BAD_REQUEST);
} }
const { email } = validation.data; const { email } = validation.data;
@@ -73,7 +73,7 @@ export async function POST(request: NextRequest) {
message: `Thank you for subscribing!` message: `Thank you for subscribing!`
}; };
return ApiResponse(STATUS_OK, message); return formatApiResponse(STATUS_OK, message);
} else if (user && !user.confirmed) { } else if (user && !user.confirmed) {
await prisma.user.update({ await prisma.user.update({
where: { where: {
@@ -106,7 +106,10 @@ export async function POST(request: NextRequest) {
const sent = await sender([email], ConfirmationTemplate(code)); const sent = await sender([email], ConfirmationTemplate(code));
if (!sent) { if (!sent) {
return ApiResponse(STATUS_INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR); return formatApiResponse(
STATUS_INTERNAL_SERVER_ERROR,
INTERNAL_SERVER_ERROR
);
} }
const message: ResponseType = { const message: ResponseType = {
@@ -114,9 +117,12 @@ export async function POST(request: NextRequest) {
message: `Thank you! You will now receive an email to ${email} to confirm the subscription.` message: `Thank you! You will now receive an email to ${email} to confirm the subscription.`
}; };
return ApiResponse(STATUS_OK, message); return formatApiResponse(STATUS_OK, message);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return ApiResponse(STATUS_INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR); return formatApiResponse(
STATUS_INTERNAL_SERVER_ERROR,
INTERNAL_SERVER_ERROR
);
} }
} }

View File

@@ -1,7 +1,7 @@
import UnsubscribeTemplate from '@components/email/Unsubscribe'; import { UnsubscribeTemplate } from '@components/email/Unsubscribe';
import prisma from '@prisma/prisma'; import prisma from '@prisma/prisma';
import { ApiResponse } from '@utils/apiResponse'; import { formatApiResponse } from '@utils/formatApiResponse';
import { sender } from '@utils/sender'; import { sender } from '@utils/resendClient';
import { import {
BAD_REQUEST, BAD_REQUEST,
INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR,
@@ -23,7 +23,7 @@ export async function POST(request: NextRequest) {
const body = await request.json(); const body = await request.json();
const validation = UnsubscribeFormSchema.safeParse(body); const validation = UnsubscribeFormSchema.safeParse(body);
if (!validation.success) { if (!validation.success) {
return ApiResponse(STATUS_BAD_REQUEST, BAD_REQUEST); return formatApiResponse(STATUS_BAD_REQUEST, BAD_REQUEST);
} }
const { email } = validation.data; const { email } = validation.data;
@@ -55,7 +55,7 @@ export async function POST(request: NextRequest) {
const sent = await sender([email], UnsubscribeTemplate()); const sent = await sender([email], UnsubscribeTemplate());
if (!sent) { if (!sent) {
return ApiResponse( return formatApiResponse(
STATUS_INTERNAL_SERVER_ERROR, STATUS_INTERNAL_SERVER_ERROR,
'Internal server error' 'Internal server error'
); );
@@ -67,9 +67,12 @@ export async function POST(request: NextRequest) {
message: `${email} unsubscribed.` message: `${email} unsubscribed.`
}; };
return ApiResponse(STATUS_OK, message); return formatApiResponse(STATUS_OK, message);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return ApiResponse(STATUS_INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR); return formatApiResponse(
STATUS_INTERNAL_SERVER_ERROR,
INTERNAL_SERVER_ERROR
);
} }
} }

View File

@@ -1,13 +1,13 @@
'use client'; 'use client';
import { CardDescription } from '@components/Card'; import { CardDescription } from '@components/Card';
import CustomCard from '@components/CustomCard'; import { CustomCard } from '@components/CustomCard';
import Schema from '@components/SchemaOrg'; import { SchemaOrg } from '@components/SchemaOrg';
import { ResponseType } from '@utils/validationSchemas'; import { ResponseType } from '@utils/validationSchemas';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { Suspense, useEffect, useState } from 'react'; import { Suspense, useEffect, useState } from 'react';
function ConfirmationPage() { const ConfirmationPage = () => {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -78,9 +78,9 @@ function ConfirmationPage() {
footer={false} footer={false}
/> />
); );
} };
export default function Confirmation() { const Confirmation = () => {
const schema = { const schema = {
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'WebSite', '@type': 'WebSite',
@@ -91,10 +91,12 @@ export default function Confirmation() {
return ( return (
<> <>
<Schema schema={schema} /> <SchemaOrg schema={schema} />
<Suspense fallback={<>Loading...</>}> <Suspense fallback={<>Loading...</>}>
<ConfirmationPage /> <ConfirmationPage />
</Suspense> </Suspense>
</> </>
); );
} };
export default Confirmation;

View File

@@ -1,4 +1,4 @@
import Tiles from '@components/tiles/Tiles'; import { Tiles } from '@components/tiles/Tiles';
import { cn } from '@utils/cn'; import { cn } from '@utils/cn';
import { Analytics } from '@vercel/analytics/react'; import { Analytics } from '@vercel/analytics/react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
@@ -6,7 +6,7 @@ import { Inter as FontSans } from 'next/font/google';
import './globals.css'; import './globals.css';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Hacker News newsletter by FromPixels', title: `Hacker News newsletter by ${process.env.NEXT_PUBLIC_BRAND_NAME}`,
description: 'Newsletter delivering the best posts from Hacker News', description: 'Newsletter delivering the best posts from Hacker News',
keywords: 'newsletter, hackernews, technology, coding, programming, news' keywords: 'newsletter, hackernews, technology, coding, programming, news'
}; };

View File

@@ -2,12 +2,12 @@
import { Button } from '@components/Button'; import { Button } from '@components/Button';
import { CardDescription } from '@components/Card'; import { CardDescription } from '@components/Card';
import CustomCard from '@components/CustomCard'; import { CustomCard } from '@components/CustomCard';
import ErrorMessage from '@components/ErrorMessage'; import { ErrorMessage } from '@components/ErrorMessage';
import { FormControl } from '@components/form/FormControl'; import { FormControl } from '@components/form/FormControl';
import { FormMessage } from '@components/form/FormMessage'; import { FormMessage } from '@components/form/FormMessage';
import { Input } from '@components/Input'; import { Input } from '@components/Input';
import Schema from '@components/SchemaOrg'; import { SchemaOrg } from '@components/SchemaOrg';
import { FormField } from '@contexts/FormField/FormFieldProvider'; import { FormField } from '@contexts/FormField/FormFieldProvider';
import { FormItem } from '@contexts/FormItem/FormItemProvider'; import { FormItem } from '@contexts/FormItem/FormItemProvider';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
@@ -19,7 +19,7 @@ import {
import { useState } from 'react'; import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
export default function Home() { export const Home = () => {
const [completed, setCompleted] = useState(false); const [completed, setCompleted] = useState(false);
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [error, setError] = useState(false); const [error, setError] = useState(false);
@@ -119,7 +119,7 @@ export default function Home() {
return ( return (
<> <>
<Schema schema={schema} /> <SchemaOrg schema={schema} />
<CustomCard <CustomCard
className='max-90vw w-96' className='max-90vw w-96'
title='Interested in keeping up with the latest from the tech world? 👩‍💻' title='Interested in keeping up with the latest from the tech world? 👩‍💻'
@@ -128,4 +128,6 @@ export default function Home() {
/> />
</> </>
); );
} };
export default Home;

View File

@@ -1,10 +1,10 @@
'use client'; 'use client';
import CustomCard from '@components/CustomCard'; import { CustomCard } from '@components/CustomCard';
import Schema from '@components/SchemaOrg'; import { SchemaOrg } from '@components/SchemaOrg';
import Link from 'next/link'; import Link from 'next/link';
export default function Privacy() { const Privacy = () => {
const schema = { const schema = {
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'WebSite', '@type': 'WebSite',
@@ -449,7 +449,7 @@ export default function Privacy() {
return ( return (
<> <>
<Schema schema={schema} /> <SchemaOrg schema={schema} />
<CustomCard <CustomCard
className='max-90vh max-90vw' className='max-90vh max-90vw'
title='Privacy Policy' title='Privacy Policy'
@@ -458,4 +458,6 @@ export default function Privacy() {
/> />
</> </>
); );
} };
export default Privacy;

View File

@@ -2,12 +2,12 @@
import { Button } from '@components/Button'; import { Button } from '@components/Button';
import { CardDescription } from '@components/Card'; import { CardDescription } from '@components/Card';
import CustomCard from '@components/CustomCard'; import { CustomCard } from '@components/CustomCard';
import ErrorMessage from '@components/ErrorMessage'; import { ErrorMessage } from '@components/ErrorMessage';
import { FormControl } from '@components/form/FormControl'; import { FormControl } from '@components/form/FormControl';
import { FormMessage } from '@components/form/FormMessage'; import { FormMessage } from '@components/form/FormMessage';
import { Input } from '@components/Input'; import { Input } from '@components/Input';
import Schema from '@components/SchemaOrg'; import { SchemaOrg } from '@components/SchemaOrg';
import { FormField } from '@contexts/FormField/FormFieldProvider'; import { FormField } from '@contexts/FormField/FormFieldProvider';
import { FormItem } from '@contexts/FormItem/FormItemProvider'; import { FormItem } from '@contexts/FormItem/FormItemProvider';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
@@ -19,7 +19,7 @@ import {
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';
export default function Unsubscribe() { const Unsubscribe = () => {
const [completed, setCompleted] = useState(false); const [completed, setCompleted] = useState(false);
const [message, setMessage] = useState(''); const [message, setMessage] = useState('');
const [error, setError] = useState(false); const [error, setError] = useState(false);
@@ -122,7 +122,7 @@ export default function Unsubscribe() {
return ( return (
<> <>
<Schema schema={schema} /> <SchemaOrg schema={schema} />
<CustomCard <CustomCard
className='max-90vw w-96' className='max-90vw w-96'
title='Unsubscribe' title='Unsubscribe'
@@ -131,4 +131,6 @@ export default function Unsubscribe() {
/> />
</> </>
); );
} };
export default Unsubscribe;

View File

@@ -7,7 +7,7 @@ import {
CardHeader, CardHeader,
CardTitle CardTitle
} from './Card'; } from './Card';
import Footer from './Footer'; import { Footer } from './Footer';
interface CardProps { interface CardProps {
title: string; title: string;
@@ -17,13 +17,13 @@ interface CardProps {
footer?: boolean; footer?: boolean;
} }
export default function CustomCard({ export const CustomCard = ({
title, title,
description, description,
content, content,
className, className,
footer = true footer = true
}: CardProps) { }: CardProps) => {
const [isLoaded, setIsLoaded] = useState(false); const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => { useEffect(() => {
@@ -53,4 +53,4 @@ export default function CustomCard({
</Card> </Card>
</div> </div>
); );
} };

View File

@@ -1,5 +1,5 @@
'use client'; 'use client';
export default function ErrorMessage() { export const ErrorMessage = () => {
return 'Oops. Something went wrong. Please try later :('; return 'Oops. Something went wrong. Please try later :(';
} };

View File

@@ -5,7 +5,7 @@ import CustomLink from './CustomLink';
const links = [{ name: 'Subscribe', path: '/' }]; const links = [{ name: 'Subscribe', path: '/' }];
export default function Footer() { export const Footer = () => {
const pathname = usePathname(); const pathname = usePathname();
return ( return (
@@ -37,4 +37,4 @@ export default function Footer() {
)} )}
</div> </div>
); );
} };

View File

@@ -1,7 +1,7 @@
import Head from 'next/head'; import Head from 'next/head';
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const SchemaOrg = ({ schema }: Record<string, any>) => ( export const SchemaOrg = ({ schema }: Record<string, any>) => (
<Head> <Head>
<script <script
type='application/ld+json' type='application/ld+json'
@@ -9,5 +9,3 @@ const SchemaOrg = ({ schema }: Record<string, any>) => (
/> />
</Head> </Head>
); );
export default SchemaOrg;

View File

@@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import Note from './components/Note'; import { Note } from './components/Note';
import Template from './Template'; import { Template } from './Template';
export default function ConfirmationTemplate(code: string) { export const ConfirmationTemplate = (code: string) => {
return { return {
subject: 'Welcome!', subject: 'Welcome!',
template: ( template: (
@@ -46,4 +46,4 @@ export default function ConfirmationTemplate(code: string) {
/> />
) )
}; };
} };

View File

@@ -2,10 +2,10 @@ import React from 'react';
import { summirize } from '@utils/summarize'; import { summirize } from '@utils/summarize';
import { NewsType } from '@utils/validationSchemas'; import { NewsType } from '@utils/validationSchemas';
import createDOMPurify from 'isomorphic-dompurify'; import createDOMPurify from 'isomorphic-dompurify';
import Template from './Template'; import getNewsletterSubject from '@utils/getNewsletterSubject';
import newsletterSubject from '@utils/newsletterSubject'; import { Template } from './Template';
export default async function NewsletterTemplate(stories: NewsType[]) { export const NewsletterTemplate = async (stories: NewsType[]) => {
const summary = await summirize(stories); const summary = await summirize(stories);
const sanitizedSummary = createDOMPurify.sanitize(summary, { const sanitizedSummary = createDOMPurify.sanitize(summary, {
USE_PROFILES: { html: true }, USE_PROFILES: { html: true },
@@ -17,7 +17,7 @@ export default async function NewsletterTemplate(stories: NewsType[]) {
throw new Error('Failed to sanitize summary'); throw new Error('Failed to sanitize summary');
} }
const topic = newsletterSubject(sanitizedSummary); const topic = getNewsletterSubject(sanitizedSummary);
return { return {
subject: topic, subject: topic,
@@ -66,4 +66,4 @@ export default async function NewsletterTemplate(stories: NewsType[]) {
/> />
) )
}; };
} };

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import Footer from './components/Footer'; import { Footer } from './components/Footer';
interface TemplateProps { interface TemplateProps {
title: string; title: string;
@@ -7,11 +7,11 @@ interface TemplateProps {
variant?: string; variant?: string;
} }
export default function Template({ export const Template = ({
title, title,
body, body,
variant = 'default' variant = 'default'
}: TemplateProps) { }: TemplateProps) => {
const isNewsletter = variant === 'newsletter'; const isNewsletter = variant === 'newsletter';
return ( return (
@@ -74,4 +74,4 @@ export default function Template({
<Footer /> <Footer />
</div> </div>
); );
} };

View File

@@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import Note from './components/Note'; import { Note } from './components/Note';
import Template from './Template'; import { Template } from './Template';
export default function UnsubscribeTemplate() { export const UnsubscribeTemplate = () => {
return { return {
subject: 'Unsubscribe confirmation', subject: 'Unsubscribe confirmation',
template: ( template: (
@@ -55,4 +55,4 @@ export default function UnsubscribeTemplate() {
/> />
) )
}; };
} };

View File

@@ -8,7 +8,7 @@ import {
Home Home
} from 'lucide-react'; } from 'lucide-react';
export default function Footer() { export const Footer = () => {
return ( return (
<footer <footer
style={{ style={{
@@ -167,4 +167,4 @@ export default function Footer() {
</div> </div>
</footer> </footer>
); );
} };

View File

@@ -2,10 +2,10 @@ interface NoteProps {
children: React.ReactNode; children: React.ReactNode;
} }
export default function Note({ children }: NoteProps) { export const Note = ({ children }: NoteProps) => {
return ( return (
<div className='mt-6 rounded-md bg-gray-50 p-4 text-sm text-gray-600'> <div className='mt-6 rounded-md bg-gray-50 p-4 text-sm text-gray-600'>
{children} {children}
</div> </div>
); );
} };

View File

@@ -3,13 +3,13 @@
import { NewsTileType } from '@utils/validationSchemas'; 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';
interface TilesProps { interface TilesProps {
children: React.ReactNode; children: React.ReactNode;
} }
export default function Tiles({ children }: TilesProps) { export const Tiles = ({ children }: TilesProps) => {
const pathname = usePathname(); const pathname = usePathname();
const [windowSize, setWindowSize] = useState<{ const [windowSize, setWindowSize] = useState<{
width: number; width: number;
@@ -101,4 +101,4 @@ export default function Tiles({ children }: TilesProps) {
if (pathname === '/maintenance') return <div>{children}</div>; if (pathname === '/maintenance') return <div>{children}</div>;
return <div className='flex h-[100vh] overflow-hidden'>{renderGrid()}</div>; return <div className='flex h-[100vh] overflow-hidden'>{renderGrid()}</div>;
} };

View File

@@ -1,7 +1,7 @@
import { getRandomGrey } from '@utils/getRandomGrey'; import { getRandomGrey } from '@utils/getRandomGrey';
import { NewsTileType } from '@utils/validationSchemas'; import { NewsTileType } from '@utils/validationSchemas';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import TileContent from './TileContent'; import { TileContent } from './TileContent';
interface CardProps { interface CardProps {
newsA?: NewsTileType; newsA?: NewsTileType;
@@ -11,7 +11,7 @@ interface CardProps {
const TEN_SECONDS = 10000; const TEN_SECONDS = 10000;
const HALF_SECOND = 500; const HALF_SECOND = 500;
export default function Tile({ newsA, newsB }: CardProps) { export const Tile = ({ newsA, newsB }: CardProps) => {
const [switched, setSwitched] = useState(false); const [switched, setSwitched] = useState(false);
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const [delayed, setDelayed] = useState(true); const [delayed, setDelayed] = useState(true);
@@ -65,4 +65,4 @@ export default function Tile({ newsA, newsB }: CardProps) {
</div> </div>
</div> </div>
); );
} };

View File

@@ -7,12 +7,12 @@ interface CardContentProps {
secondColor: string; secondColor: string;
} }
export default function TileContent({ export const TileContent = ({
story, story,
side, side,
firstColor, firstColor,
secondColor secondColor
}: CardContentProps) { }: CardContentProps) => {
const color = side ? firstColor : secondColor; const color = side ? firstColor : secondColor;
return ( return (
@@ -38,4 +38,4 @@ export default function TileContent({
></div> ></div>
</div> </div>
); );
} };

View File

@@ -3,7 +3,7 @@ import { FormItemContext } from '@contexts/FormItem/FormItemContext';
import * as React from 'react'; import * as React from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
const useFormField = () => { export const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext); const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext); const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext(); const { getFieldState, formState } = useFormContext();
@@ -25,5 +25,3 @@ const useFormField = () => {
...fieldState ...fieldState
}; };
}; };
export { useFormField };

View File

@@ -1,6 +1,6 @@
import Anthropic from '@anthropic-ai/sdk'; import Anthropic from '@anthropic-ai/sdk';
export async function message(text: string) { export async function getMessage(text: string) {
const anthropic = new Anthropic({ const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY apiKey: process.env.ANTHROPIC_API_KEY
}); });

View File

@@ -1,6 +1,6 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
export function ApiResponse(status: number, message: unknown) { export function formatApiResponse(status: number, message: unknown) {
const stringMessage = JSON.stringify(message); const stringMessage = JSON.stringify(message);
return new NextResponse(stringMessage, { status }); return new NextResponse(stringMessage, { status });

View File

@@ -59,7 +59,7 @@ function extractMainTopic(summary: string): string {
return words.toLowerCase() || 'tech updates'; return words.toLowerCase() || 'tech updates';
} }
export default function newsletterSubject(summary: string) { export default function getNewsletterSubject(summary: string) {
const topic = extractMainTopic(summary); const topic = extractMainTopic(summary);
const title = const title =
topic === 'tech updates' topic === 'tech updates'

View File

@@ -1,4 +1,4 @@
import { message } from './anthropic'; import { getMessage } from './anthropicClient';
import { NewsType } from './validationSchemas'; import { NewsType } from './validationSchemas';
export async function summirize(news: NewsType[]) { export async function summirize(news: NewsType[]) {
@@ -9,7 +9,7 @@ export async function summirize(news: NewsType[]) {
const promptSetup = const promptSetup =
'You are a tech journalist with a technology degree and background. Summarize the following list of posts from an online forum as a TL;DR (Too Long; Didn&apos;t Read) summary. Your summary should:\n\n1. Be 300-400 words long (not counting the urls).\n\n2. Structure the content in 2-3 short paragraphs, with each paragraph focusing on a specific theme or technology area.\n\n3. Start with the 2-3 most significant or impactful news items in the first paragraph.\n\n4. Use HTML paragraph tags (<p>) to separate paragraphs for better readability.\n\n5. Use a tone that is informative and slightly enthusiastic, aimed at tech-savvy general readers.\n\n6. Incorporate links as follows, including at most 3-4 words: <a href=&apos;[LINK]&apos; target=&apos;_blank&apos; rel=&apos;noopener noreferrer&apos;>[linked text]</a>.\n\n7. Each mentioned news item must include its own url link.\n\n8. End with a section wrapped in a div with inline styles: <div style=&apos;margin-top: 24px; padding: 20px; background: #F8FAFC; border-left: 3px solid #386FA4; border-radius: 4px;&apos;>. Inside this div, start with an <h3 style=&apos;margin: 0 0 12px 0; color: #386FA4; font-size: 18px; font-weight: 600;&apos;>What to Watch</h3> followed by a paragraph highlighting emerging trends or developments to follow.</div>\n\nFocus on conveying the key points and their potential impact on the tech landscape. Your response should consist of the summary only.\n\nThe news items are structured as follows:\n\nTITLE: <title>\nCONTENT: <content>\nLINK: <link>\n\nPlease summarize the following news:'; 'You are a tech journalist with a technology degree and background. Summarize the following list of posts from an online forum as a TL;DR (Too Long; Didn&apos;t Read) summary. Your summary should:\n\n1. Be 300-400 words long (not counting the urls).\n\n2. Structure the content in 2-3 short paragraphs, with each paragraph focusing on a specific theme or technology area.\n\n3. Start with the 2-3 most significant or impactful news items in the first paragraph.\n\n4. Use HTML paragraph tags (<p>) to separate paragraphs for better readability.\n\n5. Use a tone that is informative and slightly enthusiastic, aimed at tech-savvy general readers.\n\n6. Incorporate links as follows, including at most 3-4 words: <a href=&apos;[LINK]&apos; target=&apos;_blank&apos; rel=&apos;noopener noreferrer&apos;>[linked text]</a>.\n\n7. Each mentioned news item must include its own url link.\n\n8. End with a section wrapped in a div with inline styles: <div style=&apos;margin-top: 24px; padding: 20px; background: #F8FAFC; border-left: 3px solid #386FA4; border-radius: 4px;&apos;>. Inside this div, start with an <h3 style=&apos;margin: 0 0 12px 0; color: #386FA4; font-size: 18px; font-weight: 600;&apos;>What to Watch</h3> followed by a paragraph highlighting emerging trends or developments to follow.</div>\n\nFocus on conveying the key points and their potential impact on the tech landscape. Your response should consist of the summary only.\n\nThe news items are structured as follows:\n\nTITLE: <title>\nCONTENT: <content>\nLINK: <link>\n\nPlease summarize the following news:';
try { try {
const response = await message(promptSetup + newsInput); const response = await getMessage(promptSetup + newsInput);
const summary = response.content[0] as { text: string }; const summary = response.content[0] as { text: string };

1524
yarn.lock

File diff suppressed because it is too large Load Diff