style: general tweaking
This commit is contained in:
@@ -29,3 +29,5 @@ RESEND_FROM=""
|
|||||||
SECRET_HASH=""
|
SECRET_HASH=""
|
||||||
HOME_URL=""
|
HOME_URL=""
|
||||||
MAINTENANCE_MODE=0
|
MAINTENANCE_MODE=0
|
||||||
|
BRAND_NAME=""
|
||||||
|
BRAND_EMAIL=""
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
# Hackernews newsletter
|
# Hackernews newsletter
|
||||||
|
|
||||||
|
## Next up
|
||||||
|
|
||||||
|
- Batch email (Resend: ETA early 2024)
|
||||||
|
- Custom url shortener for links in the newsletter
|
||||||
|
- Cron every 10 minutes: people are more likely to open the newsletter if delivered around the time when they subscribed (if cron becomes not enough, then the cost of sending all the emails might be a bigger issue)
|
||||||
|
|
||||||
## Vercel basics
|
## Vercel basics
|
||||||
|
|
||||||
Install vercel cli
|
Install vercel cli
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ export async function GET() {
|
|||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc'
|
createdAt: 'desc'
|
||||||
},
|
},
|
||||||
take: 50
|
take: 100
|
||||||
});
|
});
|
||||||
|
|
||||||
if (news && news.length === 50) {
|
if (news && news.length === 100) {
|
||||||
return ApiResponse(200, JSON.stringify(news));
|
return ApiResponse(200, JSON.stringify(news));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { getRandomColor } from '../../../../utils/getRandomColor';
|
||||||
import { NewsSchema } from '../../../../utils/schemas';
|
import { NewsSchema } from '../../../../utils/schemas';
|
||||||
|
|
||||||
type CardContentProps = {
|
type CardContentProps = {
|
||||||
@@ -7,15 +8,6 @@ type CardContentProps = {
|
|||||||
side: boolean;
|
side: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getRandomColor() {
|
|
||||||
const letters = '456789ABCDEF';
|
|
||||||
let color = '#';
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
color += letters[Math.floor(Math.random() * 12)];
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TileContent({ story, side }: CardContentProps) {
|
export function TileContent({ story, side }: CardContentProps) {
|
||||||
const [firstColor, setFirstColor] = useState(getRandomColor());
|
const [firstColor, setFirstColor] = useState(getRandomColor());
|
||||||
const [secondColor, setSecondColor] = useState(getRandomColor());
|
const [secondColor, setSecondColor] = useState(getRandomColor());
|
||||||
@@ -32,14 +24,15 @@ export function TileContent({ story, side }: CardContentProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`h-40 w-40 overflow-hidden p-6 shadow-sm`}
|
className={`h-40 w-40 overflow-hidden rounded-lg p-6 shadow-sm`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: `${color}`
|
backgroundColor: `${color}`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h1 className='overflow-auto font-semibold'>{story.title}</h1>
|
<h1 className='overflow-auto font-semibold'>{story.title}</h1>
|
||||||
<p className='overflow-auto italic'>{story.by}</p>
|
<p className='overflow-auto italic'>by {story.by}</p>
|
||||||
<div
|
<div
|
||||||
|
className='rounded-lg'
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: 0,
|
left: 0,
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
|
import { getRandomColor } from '../../../utils/getRandomColor';
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
|
const background = getRandomColor();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className='mt-8 border-t border-gray-200 pt-6'>
|
<footer
|
||||||
<div className='mb-4 ml-8 flex items-center justify-between'>
|
className='mt-8 bg-blue-200 pt-6 text-black'
|
||||||
|
style={{ backgroundColor: `${background}` }}
|
||||||
|
>
|
||||||
|
<div className='ml-8 flex items-center justify-between pb-4'>
|
||||||
<div>
|
<div>
|
||||||
<h4 className='text-lg font-semibold text-gray-700'>Contact Us</h4>
|
<h4 className='text-lg font-semibold'>Contact Us</h4>
|
||||||
<p className='text-gray-600'>FromPixels</p>
|
<p>{process.env.BRAND_NAME}</p>
|
||||||
<p className='text-gray-600'>Email: info@frompixels.com</p>
|
<a href={`mailto:${process.env.BRAND_EMAIL}`}>
|
||||||
|
Email: {process.env.BRAND_EMAIL}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className='text-sm text-gray-500'></p>
|
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,13 @@ export default function ConfirmationTemplate(code: string) {
|
|||||||
<Email
|
<Email
|
||||||
title={'Welcome!'}
|
title={'Welcome!'}
|
||||||
body={
|
body={
|
||||||
<div className='mt-8'>
|
<div className='text-base text-gray-700 dark:text-gray-400'>
|
||||||
<p className='text-base text-gray-700 dark:text-gray-400'>
|
<p>Dear subscriber,</p>
|
||||||
Dear subscriber,
|
<p className='mt-2 '>
|
||||||
</p>
|
|
||||||
<p className='mt-2 text-base text-gray-700 dark:text-gray-400'>
|
|
||||||
thank you for subscribing to our newsletter! Please click the
|
thank you for subscribing to our newsletter! Please click the
|
||||||
button below to confirm your subscription.
|
button below to confirm your subscription.
|
||||||
</p>
|
</p>
|
||||||
<div className='mt-8 flex justify-center'>
|
<div className='my-8 flex justify-center'>
|
||||||
<Link
|
<Link
|
||||||
path={`${process.env.HOME_URL}/confirmation?code=${code}`}
|
path={`${process.env.HOME_URL}/confirmation?code=${code}`}
|
||||||
text='Confirm Subscription'
|
text='Confirm Subscription'
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { Container } from '@react-email/container';
|
|
||||||
import { Html } from '@react-email/html';
|
|
||||||
import { Section } from '@react-email/section';
|
|
||||||
import { Text } from '@react-email/text';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { getRandomColor } from '../../utils/getRandomColor';
|
||||||
import { NewsSchema } from '../../utils/schemas';
|
import { NewsSchema } from '../../utils/schemas';
|
||||||
import { Footer } from './components/footer';
|
import Email from './template';
|
||||||
|
|
||||||
export default function NewsletterTemplate(
|
export default function NewsletterTemplate(
|
||||||
stories: z.infer<typeof NewsSchema>[]
|
stories: z.infer<typeof NewsSchema>[]
|
||||||
@@ -26,36 +23,27 @@ export default function NewsletterTemplate(
|
|||||||
return {
|
return {
|
||||||
subject: `What's new from Hackernews?`,
|
subject: `What's new from Hackernews?`,
|
||||||
template: (
|
template: (
|
||||||
<Html>
|
<Email
|
||||||
<Section className='bg-white'>
|
title='Good day!'
|
||||||
<div className='mx-auto w-full max-w-2xl overflow-hidden rounded-lg bg-white shadow-lg'>
|
body={
|
||||||
<div className='text-center '>
|
<div className='text-base text-gray-700 dark:text-gray-400'>
|
||||||
<h1 className='my-4 text-3xl font-bold'>Good day!</h1>
|
<p className='flex justify-center'>
|
||||||
<p>
|
|
||||||
Here is something{' '}
|
Here is something{' '}
|
||||||
{sayings[Math.floor(Math.random() * sayings.length)]}:
|
{sayings[Math.floor(Math.random() * sayings.length)]}:
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
<Container
|
|
||||||
style={{
|
|
||||||
margin: '0 auto',
|
|
||||||
padding: '20px 0 48px',
|
|
||||||
width: '580px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text>
|
|
||||||
{stories.map(story => {
|
{stories.map(story => {
|
||||||
|
const background = getRandomColor();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={story.id}
|
key={story.id}
|
||||||
className='mt-8 rounded-lg border bg-card text-card-foreground shadow-sm'
|
className='mt-8 rounded-lg border bg-card text-card-foreground shadow-sm'
|
||||||
data-v0-t='card'
|
data-v0-t='card'
|
||||||
|
style={{ backgroundColor: `${background}` }}
|
||||||
>
|
>
|
||||||
<div className='flex flex-col space-y-1.5 p-6'>
|
<div className='flex flex-col space-y-1.5 px-6 pb-2 pt-6'>
|
||||||
<h2 className='text-2xl font-semibold'>
|
<h2 className='text-2xl font-semibold'>{story.title}</h2>
|
||||||
{story.title}
|
<p className='italic'>by {story.by}</p>
|
||||||
</h2>
|
|
||||||
<p>{story.by}</p>
|
|
||||||
</div>
|
</div>
|
||||||
{story.text && (
|
{story.text && (
|
||||||
<div className='px-6'>
|
<div className='px-6'>
|
||||||
@@ -71,26 +59,16 @@ export default function NewsletterTemplate(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{story.url && (
|
{story.url && (
|
||||||
<div className='p-4 text-right'>
|
<div className='p-6 text-right font-bold'>
|
||||||
<p>
|
<a href={story.url}>Read more</a>
|
||||||
<a
|
|
||||||
href={story.url}
|
|
||||||
className='inline-flex h-10 items-center justify-center rounded-md bg-blue-500 px-4 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-blue-700/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50'
|
|
||||||
>
|
|
||||||
Read more
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Text>
|
|
||||||
</Container>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
}
|
||||||
</Html>
|
/>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Container } from '@react-email/container';
|
|
||||||
import { Html } from '@react-email/html';
|
import { Html } from '@react-email/html';
|
||||||
import { Section } from '@react-email/section';
|
import { Section } from '@react-email/section';
|
||||||
import { Text } from '@react-email/text';
|
import { getRandomColor } from '../../utils/getRandomColor';
|
||||||
import { Footer } from './components/footer';
|
import { Footer } from './components/footer';
|
||||||
|
|
||||||
type EmailProps = {
|
type EmailProps = {
|
||||||
@@ -10,26 +9,18 @@ type EmailProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Email({ title, body }: EmailProps) {
|
export default function Email({ title, body }: EmailProps) {
|
||||||
|
const titleBackground = getRandomColor();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Section className='mx-auto w-full max-w-2xl overflow-hidden rounded-lg bg-white shadow-lg'>
|
<Section className='max-w-2xl overflow-hidden rounded-lg bg-white shadow-lg'>
|
||||||
<Container
|
<h1
|
||||||
style={{
|
className='p-8 text-center text-3xl font-bold text-black'
|
||||||
margin: '0 auto',
|
style={{ backgroundColor: `${titleBackground}` }}
|
||||||
padding: '20px 0 48px',
|
|
||||||
width: '580px'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<h1 className='mt-4 text-center text-3xl font-bold'>{title}</h1>
|
{title}
|
||||||
<Text
|
</h1>
|
||||||
style={{
|
<div className='m-8 p-8'>{body}</div>
|
||||||
fontSize: '16px',
|
|
||||||
marginBottom: '16px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{body}
|
|
||||||
</Text>
|
|
||||||
</Container>
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</Section>
|
</Section>
|
||||||
</Html>
|
</Html>
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export default function UnsubscribeTemplate() {
|
|||||||
<Email
|
<Email
|
||||||
title="We're sad you're leaving :("
|
title="We're sad you're leaving :("
|
||||||
body={
|
body={
|
||||||
<div className='mt-8'>
|
<div className='text-base text-gray-700 dark:text-gray-400'>
|
||||||
<p className='mt-2 text-base text-gray-700 dark:text-gray-400'>
|
<p className='mt-2 '>
|
||||||
You have been successfully unsubscribed from our newsletter. You
|
You have been successfully unsubscribed from our newsletter. You
|
||||||
won't receive any further communications from us unless you
|
won't receive any further communications from us unless you
|
||||||
explicitly opt-in again.
|
explicitly opt-in again.
|
||||||
|
|||||||
8
utils/getRandomColor.ts
Normal file
8
utils/getRandomColor.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export function getRandomColor() {
|
||||||
|
const letters = '6789ABCDEF';
|
||||||
|
let color = '#';
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
color += letters[Math.floor(Math.random() * 10)];
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user