chore: code cleaning (#21)
This commit is contained in:
@@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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'
|
||||||
};
|
};
|
||||||
|
|||||||
14
app/page.tsx
14
app/page.tsx
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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 :(';
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|||||||
@@ -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) {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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[]) {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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() {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
@@ -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 });
|
||||||
@@ -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'
|
||||||
@@ -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'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='[LINK]' target='_blank' rel='noopener noreferrer'>[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='margin-top: 24px; padding: 20px; background: #F8FAFC; border-left: 3px solid #386FA4; border-radius: 4px;'>. Inside this div, start with an <h3 style='margin: 0 0 12px 0; color: #386FA4; font-size: 18px; font-weight: 600;'>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'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='[LINK]' target='_blank' rel='noopener noreferrer'>[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='margin-top: 24px; padding: 20px; background: #F8FAFC; border-left: 3px solid #386FA4; border-radius: 4px;'>. Inside this div, start with an <h3 style='margin: 0 0 12px 0; color: #386FA4; font-size: 18px; font-weight: 600;'>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 };
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user