feat: user activation and emails
This commit is contained in:
42
app/api/confirmation/route.ts
Normal file
42
app/api/confirmation/route.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { z } from 'zod';
|
||||
import prisma from '../../../prisma/prisma';
|
||||
import { ApiResponse } from '../../../utils/apiResponse';
|
||||
import { ConfirmationSchema, ResponseSchema } from '../../../utils/types';
|
||||
|
||||
export const dynamic = 'force-dynamic'; // defaults to force-static
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json();
|
||||
const validation = ConfirmationSchema.safeParse(body);
|
||||
if (!validation.success || !validation.data.code) {
|
||||
return ApiResponse(400, 'Bad request');
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
code: validation.data.code
|
||||
}
|
||||
});
|
||||
|
||||
if (user) {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
code: validation.data.code
|
||||
},
|
||||
data: {
|
||||
confirmed: true
|
||||
}
|
||||
});
|
||||
|
||||
const message: z.infer<typeof ResponseSchema> = {
|
||||
message: `Thank you for confirming the subscripion!`
|
||||
};
|
||||
|
||||
return ApiResponse(200, JSON.stringify(message));
|
||||
}
|
||||
|
||||
const message: z.infer<typeof ResponseSchema> = {
|
||||
message: `Nothing to see here...`
|
||||
};
|
||||
|
||||
return ApiResponse(200, JSON.stringify(message));
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import NewsletterEmail from '../../../components/emails/newsletter';
|
||||
import prisma from '../../../prisma/prisma';
|
||||
import { NewsSchema } from '../../utils/types';
|
||||
import { singleNews, topNews } from '../../utils/urls';
|
||||
import { sendEmail } from '../../../utils/sender';
|
||||
import { NewsSchema } from '../../../utils/types';
|
||||
import { singleNews, topNews } from '../../../utils/urls';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
if (
|
||||
@@ -11,47 +13,22 @@ export async function GET(request: Request) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const response = await hackernewsApi();
|
||||
|
||||
return new NextResponse(JSON.stringify(response), {
|
||||
status: 200
|
||||
});
|
||||
}
|
||||
|
||||
async function hackernewsApi() {
|
||||
const topstories: number[] = await fetch(topNews).then(res => res.json());
|
||||
|
||||
console.log('topstories', topstories);
|
||||
|
||||
const newsPromises = topstories
|
||||
.splice(0, Number(process.env.NEWS_LIMIT))
|
||||
.map(async id => {
|
||||
console.log('id', id);
|
||||
const sourceNews: z.infer<typeof NewsSchema> = await fetch(
|
||||
singleNews(id)
|
||||
).then(res => res.json());
|
||||
|
||||
console.log('sourceNews', sourceNews);
|
||||
|
||||
return await prisma.news.upsert({
|
||||
create: {
|
||||
id,
|
||||
title: sourceNews.title,
|
||||
text: sourceNews.text,
|
||||
type: sourceNews.type,
|
||||
by: sourceNews.by,
|
||||
time: sourceNews.time,
|
||||
url: sourceNews.url,
|
||||
score: sourceNews.score
|
||||
...sourceNews,
|
||||
id
|
||||
},
|
||||
update: {
|
||||
title: sourceNews.title,
|
||||
text: sourceNews.text,
|
||||
type: sourceNews.type,
|
||||
by: sourceNews.by,
|
||||
time: sourceNews.time,
|
||||
url: sourceNews.url,
|
||||
score: sourceNews.score
|
||||
...sourceNews
|
||||
},
|
||||
where: {
|
||||
id
|
||||
@@ -64,5 +41,30 @@ async function hackernewsApi() {
|
||||
|
||||
const newsIds = await Promise.all(newsPromises);
|
||||
|
||||
return newsIds.map(news => news.id);
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
email: 'riccardo.s@hey.com',
|
||||
confirmed: true,
|
||||
deleted: false
|
||||
},
|
||||
select: {
|
||||
email: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!users) {
|
||||
return new NextResponse('No users.', {
|
||||
status: 200
|
||||
});
|
||||
}
|
||||
|
||||
await sendEmail(
|
||||
users.map(user => user.email),
|
||||
`What's new from Hackernews?`,
|
||||
NewsletterEmail(newsIds.map(news => news.id))
|
||||
);
|
||||
|
||||
return new NextResponse(`Newsletter sent to ${users.length} addresses.`, {
|
||||
status: 200
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,34 +1,70 @@
|
||||
import * as crypto from 'crypto';
|
||||
import { z } from 'zod';
|
||||
import { fromZodError } from 'zod-validation-error';
|
||||
import SubscribeEmail from '../../../components/emails/subscribe';
|
||||
import prisma from '../../../prisma/prisma';
|
||||
import { ResponseSchema, SubscribeFormSchema } from '../../utils/types';
|
||||
import { ApiResponse } from '../../../utils/apiResponse';
|
||||
import { sendEmail } from '../../../utils/sender';
|
||||
import { ResponseSchema, SubscribeFormSchema } from '../../../utils/types';
|
||||
|
||||
export const dynamic = 'force-dynamic'; // defaults to force-static
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json();
|
||||
const validation = SubscribeFormSchema.safeParse(body);
|
||||
if (!validation.success) {
|
||||
const message = fromZodError(validation.error);
|
||||
return new Response(message.message, { status: 400 });
|
||||
return ApiResponse(400, 'Bad request');
|
||||
}
|
||||
|
||||
const { email, targetingAllowed } = validation.data;
|
||||
const { email } = validation.data;
|
||||
|
||||
const userAlreadyConfirmed = await prisma.user.findUnique({
|
||||
where: {
|
||||
email,
|
||||
confirmed: true
|
||||
}
|
||||
});
|
||||
|
||||
if (userAlreadyConfirmed) {
|
||||
if (userAlreadyConfirmed.deleted) {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
email
|
||||
},
|
||||
data: {
|
||||
deleted: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const message: z.infer<typeof ResponseSchema> = {
|
||||
message: `Thank you for subscribing!`
|
||||
};
|
||||
|
||||
return ApiResponse(200, JSON.stringify(message));
|
||||
}
|
||||
|
||||
const code = crypto
|
||||
.createHash('sha256')
|
||||
.update(`${process.env.SECRET_HASH}${email}}`)
|
||||
.digest('hex');
|
||||
|
||||
await prisma.user.upsert({
|
||||
create: {
|
||||
email,
|
||||
targetingAllowed
|
||||
code
|
||||
},
|
||||
update: {
|
||||
targetingAllowed
|
||||
code
|
||||
},
|
||||
where: {
|
||||
email
|
||||
}
|
||||
});
|
||||
|
||||
await sendEmail([email], 'Welcome!', SubscribeEmail(code));
|
||||
|
||||
const message: z.infer<typeof ResponseSchema> = {
|
||||
message: `${email} subscribed!`
|
||||
message: `Thank you! You will now receive an email to ${email} to confirm the subscription.`
|
||||
};
|
||||
return new Response(JSON.stringify(message), { status: 200 });
|
||||
|
||||
return ApiResponse(200, JSON.stringify(message));
|
||||
}
|
||||
|
||||
@@ -1,31 +1,42 @@
|
||||
import { z } from 'zod';
|
||||
import { fromZodError } from 'zod-validation-error';
|
||||
import UnsubscribeEmail from '../../../components/emails/unsubscribe';
|
||||
import prisma from '../../../prisma/prisma';
|
||||
import { ResponseSchema, UnsubscribeFormSchema } from '../../utils/types';
|
||||
import { ApiResponse } from '../../../utils/apiResponse';
|
||||
import { sendEmail } from '../../../utils/sender';
|
||||
import { ResponseSchema, UnsubscribeFormSchema } from '../../../utils/types';
|
||||
|
||||
export const dynamic = 'force-dynamic'; // defaults to force-static
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json();
|
||||
const validation = UnsubscribeFormSchema.safeParse(body);
|
||||
if (!validation.success) {
|
||||
const message = fromZodError(validation.error);
|
||||
return new Response(message.message, { status: 400 });
|
||||
return ApiResponse(400, 'Bad request');
|
||||
}
|
||||
|
||||
const { email } = validation.data;
|
||||
|
||||
try {
|
||||
await prisma.user.delete({
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email
|
||||
}
|
||||
});
|
||||
|
||||
if (user && !user.deleted) {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
email
|
||||
},
|
||||
data: {
|
||||
deleted: true
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
|
||||
sendEmail([email], 'Unsubscribe confirmation', UnsubscribeEmail());
|
||||
}
|
||||
|
||||
const message: z.infer<typeof ResponseSchema> = {
|
||||
message: `${email} unsubscribe!`
|
||||
message: `${email} unsubscribed!`
|
||||
};
|
||||
return new Response(JSON.stringify(message), { status: 200 });
|
||||
|
||||
return ApiResponse(200, JSON.stringify(message));
|
||||
}
|
||||
|
||||
5
app/confirmation/page.tsx
Normal file
5
app/confirmation/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ConfirmationPage } from '../../components/pages/confirmation';
|
||||
|
||||
export default function Confirmation() {
|
||||
return <ConfirmationPage />;
|
||||
}
|
||||
@@ -7,7 +7,8 @@ const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app'
|
||||
description: 'Generated by create next app',
|
||||
keywords: 'newsletter, hackernews, technology, coding, programming, news'
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
20
app/manifest.ts
Normal file
20
app/manifest.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
|
||||
export default function manifest(): MetadataRoute.Manifest {
|
||||
return {
|
||||
name: 'Newsletter',
|
||||
short_name: 'Newsletter',
|
||||
description: 'Newsletter with Hackernews top stories',
|
||||
start_url: '/',
|
||||
display: 'standalone',
|
||||
background_color: '#fff',
|
||||
theme_color: '#fff',
|
||||
icons: [
|
||||
{
|
||||
src: '/favicon.ico',
|
||||
sizes: 'any',
|
||||
type: 'image/x-icon'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
17
app/page.tsx
17
app/page.tsx
@@ -1,16 +1,11 @@
|
||||
'use client';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button } from '../components/Button';
|
||||
import { VerticalLayout } from '../components/VerticalLayout';
|
||||
import { CustomLink } from '../components/elements/customLink';
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<VerticalLayout>
|
||||
<h1>Home</h1>
|
||||
<Button label='Subscribe' onClick={() => router.push('/subscribe')} />
|
||||
<Button label='Unsubscribe' onClick={() => router.push('/unsubscribe')} />
|
||||
</VerticalLayout>
|
||||
<div>
|
||||
<CustomLink path='/subscribe' text='Subscribe' />
|
||||
<CustomLink path='/unsubscribe' text='Unsubscribe' />
|
||||
<CustomLink path='/privacy' text='Privacy' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
414
app/privacy/page.tsx
Normal file
414
app/privacy/page.tsx
Normal file
@@ -0,0 +1,414 @@
|
||||
export default function Privacy() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Privacy Policy</h1>
|
||||
<p>Last updated: December 03, 2023</p>
|
||||
<p>
|
||||
This Privacy Policy describes Our policies and procedures on the
|
||||
collection, use and disclosure of Your information when You use the
|
||||
Service and tells You about Your privacy rights and how the law protects
|
||||
You.
|
||||
</p>
|
||||
<p>
|
||||
We use Your Personal data to provide and improve the Service. By using
|
||||
the Service, You agree to the collection and use of information in
|
||||
accordance with this Privacy Policy. This Privacy Policy has been
|
||||
created with the help of the{' '}
|
||||
<a
|
||||
href='https://www.termsfeed.com/privacy-policy-generator/'
|
||||
target='_blank'
|
||||
>
|
||||
Privacy Policy Generator
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<h2>Interpretation and Definitions</h2>
|
||||
<h3>Interpretation</h3>
|
||||
<p>
|
||||
The words of which the initial letter is capitalized have meanings
|
||||
defined under the following conditions. The following definitions shall
|
||||
have the same meaning regardless of whether they appear in singular or
|
||||
in plural.
|
||||
</p>
|
||||
<h3>Definitions</h3>
|
||||
<p>For the purposes of this Privacy Policy:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Account</strong> means a unique account created for You to
|
||||
access our Service or parts of our Service.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Affiliate</strong> means an entity that controls, is
|
||||
controlled by or is under common control with a party, where
|
||||
"control" means ownership of 50% or more of the shares,
|
||||
equity interest or other securities entitled to vote for election of
|
||||
directors or other managing authority.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Application</strong> refers to FromPixels, the software
|
||||
program provided by the Company.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Company</strong> (referred to as either "the
|
||||
Company", "We", "Us" or "Our" in
|
||||
this Agreement) refers to FromPixels.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Country</strong> refers to: Italy
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Device</strong> means any device that can access the Service
|
||||
such as a computer, a cellphone or a digital tablet.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Personal Data</strong> is any information that relates to an
|
||||
identified or identifiable individual.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Service</strong> refers to the Application.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Service Provider</strong> means any natural or legal person
|
||||
who processes the data on behalf of the Company. It refers to
|
||||
third-party companies or individuals employed by the Company to
|
||||
facilitate the Service, to provide the Service on behalf of the
|
||||
Company, to perform services related to the Service or to assist the
|
||||
Company in analyzing how the Service is used.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Usage Data</strong> refers to data collected automatically,
|
||||
either generated by the use of the Service or from the Service
|
||||
infrastructure itself (for example, the duration of a page visit).
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>You</strong> means the individual accessing or using the
|
||||
Service, or the company, or other legal entity on behalf of which
|
||||
such individual is accessing or using the Service, as applicable.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Collecting and Using Your Personal Data</h2>
|
||||
<h3>Types of Data Collected</h3>
|
||||
<h4>Personal Data</h4>
|
||||
<p>
|
||||
While using Our Service, We may ask You to provide Us with certain
|
||||
personally identifiable information that can be used to contact or
|
||||
identify You. Personally identifiable information may include, but is
|
||||
not limited to:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>Email address</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>Usage Data</p>
|
||||
</li>
|
||||
</ul>
|
||||
<h4>Usage Data</h4>
|
||||
<p>Usage Data is collected automatically when using the Service.</p>
|
||||
<p>
|
||||
Usage Data may include information such as Your Device&aposs Internet
|
||||
Protocol address (e.g. IP address), browser type, browser version, the
|
||||
pages of our Service that You visit, the time and date of Your visit,
|
||||
the time spent on those pages, unique device identifiers and other
|
||||
diagnostic data.
|
||||
</p>
|
||||
<p>
|
||||
When You access the Service by or through a mobile device, We may
|
||||
collect certain information automatically, including, but not limited
|
||||
to, the type of mobile device You use, Your mobile device unique ID, the
|
||||
IP address of Your mobile device, Your mobile operating system, the type
|
||||
of mobile Internet browser You use, unique device identifiers and other
|
||||
diagnostic data.
|
||||
</p>
|
||||
<p>
|
||||
We may also collect information that Your browser sends whenever You
|
||||
visit our Service or when You access the Service by or through a mobile
|
||||
device.
|
||||
</p>
|
||||
<h3>Use of Your Personal Data</h3>
|
||||
<p>The Company may use Personal Data for the following purposes:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<p>
|
||||
<strong>To provide and maintain our Service</strong>, including to
|
||||
monitor the usage of our Service.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>To manage Your Account:</strong> to manage Your registration
|
||||
as a user of the Service. The Personal Data You provide can give You
|
||||
access to different functionalities of the Service that are
|
||||
available to You as a registered user.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>For the performance of a contract:</strong> the development,
|
||||
compliance and undertaking of the purchase contract for the
|
||||
products, items or services You have purchased or of any other
|
||||
contract with Us through the Service.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>To contact You:</strong> To contact You by email, telephone
|
||||
calls, SMS, or other equivalent forms of electronic communication,
|
||||
such as a mobile application&aposs push notifications regarding
|
||||
updates or informative communications related to the
|
||||
functionalities, products or contracted services, including the
|
||||
security updates, when necessary or reasonable for their
|
||||
implementation.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>To provide You</strong> with news, special offers and
|
||||
general information about other goods, services and events which we
|
||||
offer that are similar to those that you have already purchased or
|
||||
enquired about unless You have opted not to receive such
|
||||
information.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>To manage Your requests:</strong> To attend and manage Your
|
||||
requests to Us.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>For business transfers:</strong> We may use Your information
|
||||
to evaluate or conduct a merger, divestiture, restructuring,
|
||||
reorganization, dissolution, or other sale or transfer of some or
|
||||
all of Our assets, whether as a going concern or as part of
|
||||
bankruptcy, liquidation, or similar proceeding, in which Personal
|
||||
Data held by Us about our Service users is among the assets
|
||||
transferred.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<strong>For other purposes</strong>: We may use Your information for
|
||||
other purposes, such as data analysis, identifying usage trends,
|
||||
determining the effectiveness of our promotional campaigns and to
|
||||
evaluate and improve our Service, products, services, marketing and
|
||||
your experience.
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<p>We may share Your personal information in the following situations:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>With Service Providers:</strong> We may share Your personal
|
||||
information with Service Providers to monitor and analyze the use of
|
||||
our Service, to contact You.
|
||||
</li>
|
||||
<li>
|
||||
<strong>For business transfers:</strong> We may share or transfer Your
|
||||
personal information in connection with, or during negotiations of,
|
||||
any merger, sale of Company assets, financing, or acquisition of all
|
||||
or a portion of Our business to another company.
|
||||
</li>
|
||||
<li>
|
||||
<strong>With Affiliates:</strong> We may share Your information with
|
||||
Our affiliates, in which case we will require those affiliates to
|
||||
honor this Privacy Policy. Affiliates include Our parent company and
|
||||
any other subsidiaries, joint venture partners or other companies that
|
||||
We control or that are under common control with Us.
|
||||
</li>
|
||||
<li>
|
||||
<strong>With business partners:</strong> We may share Your information
|
||||
with Our business partners to offer You certain products, services or
|
||||
promotions.
|
||||
</li>
|
||||
<li>
|
||||
<strong>With other users:</strong> when You share personal information
|
||||
or otherwise interact in the public areas with other users, such
|
||||
information may be viewed by all users and may be publicly distributed
|
||||
outside.
|
||||
</li>
|
||||
<li>
|
||||
<strong>With Your consent</strong>: We may disclose Your personal
|
||||
information for any other purpose with Your consent.
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Retention of Your Personal Data</h3>
|
||||
<p>
|
||||
The Company will retain Your Personal Data only for as long as is
|
||||
necessary for the purposes set out in this Privacy Policy. We will
|
||||
retain and use Your Personal Data to the extent necessary to comply with
|
||||
our legal obligations (for example, if we are required to retain your
|
||||
data to comply with applicable laws), resolve disputes, and enforce our
|
||||
legal agreements and policies.
|
||||
</p>
|
||||
<p>
|
||||
The Company will also retain Usage Data for internal analysis purposes.
|
||||
Usage Data is generally retained for a shorter period of time, except
|
||||
when this data is used to strengthen the security or to improve the
|
||||
functionality of Our Service, or We are legally obligated to retain this
|
||||
data for longer time periods.
|
||||
</p>
|
||||
<h3>Transfer of Your Personal Data</h3>
|
||||
<p>
|
||||
Your information, including Personal Data, is processed at the
|
||||
Company&aposs operating offices and in any other places where the
|
||||
parties involved in the processing are located. It means that this
|
||||
information may be transferred to — and maintained on — computers
|
||||
located outside of Your state, province, country or other governmental
|
||||
jurisdiction where the data protection laws may differ than those from
|
||||
Your jurisdiction.
|
||||
</p>
|
||||
<p>
|
||||
Your consent to this Privacy Policy followed by Your submission of such
|
||||
information represents Your agreement to that transfer.
|
||||
</p>
|
||||
<p>
|
||||
The Company will take all steps reasonably necessary to ensure that Your
|
||||
data is treated securely and in accordance with this Privacy Policy and
|
||||
no transfer of Your Personal Data will take place to an organization or
|
||||
a country unless there are adequate controls in place including the
|
||||
security of Your data and other personal information.
|
||||
</p>
|
||||
<h3>Delete Your Personal Data</h3>
|
||||
<p>
|
||||
You have the right to delete or request that We assist in deleting the
|
||||
Personal Data that We have collected about You.
|
||||
</p>
|
||||
<p>
|
||||
Our Service may give You the ability to delete certain information about
|
||||
You from within the Service.
|
||||
</p>
|
||||
<p>
|
||||
You may update, amend, or delete Your information at any time by signing
|
||||
in to Your Account, if you have one, and visiting the account settings
|
||||
section that allows you to manage Your personal information. You may
|
||||
also contact Us to request access to, correct, or delete any personal
|
||||
information that You have provided to Us.
|
||||
</p>
|
||||
<p>
|
||||
Please note, however, that We may need to retain certain information
|
||||
when we have a legal obligation or lawful basis to do so.
|
||||
</p>
|
||||
<h3>Disclosure of Your Personal Data</h3>
|
||||
<h4>Business Transactions</h4>
|
||||
<p>
|
||||
If the Company is involved in a merger, acquisition or asset sale, Your
|
||||
Personal Data may be transferred. We will provide notice before Your
|
||||
Personal Data is transferred and becomes subject to a different Privacy
|
||||
Policy.
|
||||
</p>
|
||||
<h4>Law enforcement</h4>
|
||||
<p>
|
||||
Under certain circumstances, the Company may be required to disclose
|
||||
Your Personal Data if required to do so by law or in response to valid
|
||||
requests by public authorities (e.g. a court or a government agency).
|
||||
</p>
|
||||
<h4>Other legal requirements</h4>
|
||||
<p>
|
||||
The Company may disclose Your Personal Data in the good faith belief
|
||||
that such action is necessary to:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Comply with a legal obligation</li>
|
||||
<li>Protect and defend the rights or property of the Company</li>
|
||||
<li>
|
||||
Prevent or investigate possible wrongdoing in connection with the
|
||||
Service
|
||||
</li>
|
||||
<li>
|
||||
Protect the personal safety of Users of the Service or the public
|
||||
</li>
|
||||
<li>Protect against legal liability</li>
|
||||
</ul>
|
||||
<h3>Security of Your Personal Data</h3>
|
||||
<p>
|
||||
The security of Your Personal Data is important to Us, but remember that
|
||||
no method of transmission over the Internet, or method of electronic
|
||||
storage is 100% secure. While We strive to use commercially acceptable
|
||||
means to protect Your Personal Data, We cannot guarantee its absolute
|
||||
security.
|
||||
</p>
|
||||
<h2>Children&aposs Privacy</h2>
|
||||
<p>
|
||||
Our Service does not address anyone under the age of 13. We do not
|
||||
knowingly collect personally identifiable information from anyone under
|
||||
the age of 13. If You are a parent or guardian and You are aware that
|
||||
Your child has provided Us with Personal Data, please contact Us. If We
|
||||
become aware that We have collected Personal Data from anyone under the
|
||||
age of 13 without verification of parental consent, We take steps to
|
||||
remove that information from Our servers.
|
||||
</p>
|
||||
<p>
|
||||
If We need to rely on consent as a legal basis for processing Your
|
||||
information and Your country requires consent from a parent, We may
|
||||
require Your parent&aposs consent before We collect and use that
|
||||
information.
|
||||
</p>
|
||||
<h2>Links to Other Websites</h2>
|
||||
<p>
|
||||
Our Service may contain links to other websites that are not operated by
|
||||
Us. If You click on a third party link, You will be directed to that
|
||||
third party&aposs site. We strongly advise You to review the Privacy
|
||||
Policy of every site You visit.
|
||||
</p>
|
||||
<p>
|
||||
We have no control over and assume no responsibility for the content,
|
||||
privacy policies or practices of any third party sites or services.
|
||||
</p>
|
||||
<h2>Changes to this Privacy Policy</h2>
|
||||
<p>
|
||||
We may update Our Privacy Policy from time to time. We will notify You
|
||||
of any changes by posting the new Privacy Policy on this page.
|
||||
</p>
|
||||
<p>
|
||||
We will let You know via email and/or a prominent notice on Our Service,
|
||||
prior to the change becoming effective and update the "Last
|
||||
updated" date at the top of this Privacy Policy.
|
||||
</p>
|
||||
<p>
|
||||
You are advised to review this Privacy Policy periodically for any
|
||||
changes. Changes to this Privacy Policy are effective when they are
|
||||
posted on this page.
|
||||
</p>
|
||||
<h2>Contact Us</h2>
|
||||
<p>
|
||||
If you have any questions about this Privacy Policy, You can contact us:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
By visiting this page on our website:{' '}
|
||||
<a
|
||||
href={`${process.env.HOME_URL}/privacy`}
|
||||
rel='external nofollow noopener'
|
||||
target='_blank'
|
||||
>
|
||||
{`${process.env.HOME_URL}/privacy`}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
app/robots.ts
Normal file
11
app/robots.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: '*',
|
||||
disallow: '/'
|
||||
},
|
||||
sitemap: `${process.env.HOME_URL!}/sitemap.xml`
|
||||
};
|
||||
}
|
||||
24
app/sitemap.ts
Normal file
24
app/sitemap.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
return [
|
||||
{
|
||||
url: process.env.HOME_URL!,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'yearly',
|
||||
priority: 1
|
||||
},
|
||||
{
|
||||
url: `${process.env.HOME_URL!}/subscribe`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'yearly',
|
||||
priority: 0.8
|
||||
},
|
||||
{
|
||||
url: `${process.env.HOME_URL!}/unsubscribe`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'yearly',
|
||||
priority: 0.5
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -1,81 +1,5 @@
|
||||
'use client';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React, { useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { Button } from '../../components/Button';
|
||||
import { VerticalLayout } from '../../components/VerticalLayout';
|
||||
import { ResponseSchema } from './../utils/types';
|
||||
import { SubscribeForm } from '../../components/pages/subscribe';
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.currentTarget);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/subscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: data.get('email'),
|
||||
targetingAllowed: isChecked
|
||||
})
|
||||
});
|
||||
|
||||
if (!response?.ok) {
|
||||
throw new Error(`Invalid response: ${response.status}`);
|
||||
}
|
||||
|
||||
const formResponse: z.infer<typeof ResponseSchema> =
|
||||
await response.json();
|
||||
|
||||
console.log(formResponse);
|
||||
|
||||
router.push('/success');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
alert('Error');
|
||||
}
|
||||
}
|
||||
|
||||
function handleCheckboxChange() {
|
||||
setIsChecked(!isChecked);
|
||||
}
|
||||
|
||||
return (
|
||||
<VerticalLayout>
|
||||
<form className='container' onSubmit={handleSubmit}>
|
||||
<h1>Subscribe to newsletter</h1>
|
||||
<div className='email block'>
|
||||
<label htmlFor='frm-email'>Email</label>
|
||||
<input
|
||||
placeholder='example@email.com'
|
||||
id='email'
|
||||
type='email'
|
||||
name='email'
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className='checkbox block'>
|
||||
<label htmlFor='frm-checkbox'>Allow advertising</label>
|
||||
<input
|
||||
id='targetingAllowed'
|
||||
type='checkbox'
|
||||
name='targetingAllowed'
|
||||
checked={isChecked}
|
||||
onChange={handleCheckboxChange}
|
||||
/>
|
||||
</div>
|
||||
<div className='button block'>
|
||||
<button type='submit'>Subscribe</button>
|
||||
</div>
|
||||
</form>
|
||||
<Button label='Home' onClick={() => router.push('/')} />
|
||||
<Button label='Unsubscribe' onClick={() => router.push('/unsubscribe')} />
|
||||
</VerticalLayout>
|
||||
);
|
||||
export default function Subscribe() {
|
||||
return <SubscribeForm />;
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
'use client';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button } from '../../components/Button';
|
||||
import { VerticalLayout } from '../../components/VerticalLayout';
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<VerticalLayout>
|
||||
<h1>Success!</h1>
|
||||
<Button label='Home' onClick={() => router.push('/')} />
|
||||
</VerticalLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,65 +1,5 @@
|
||||
'use client';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React from 'react';
|
||||
import { z } from 'zod';
|
||||
import { Button } from '../../components/Button';
|
||||
import { VerticalLayout } from '../../components/VerticalLayout';
|
||||
import { ResponseSchema } from './../utils/types';
|
||||
import { UnsubscribeForm } from '../../components/pages/unsubscribe';
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.currentTarget);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/unsubscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: data.get('email')
|
||||
})
|
||||
});
|
||||
|
||||
if (!response?.ok) {
|
||||
throw new Error(`Invalid response: ${response.status}`);
|
||||
}
|
||||
|
||||
const formResponse: z.infer<typeof ResponseSchema> =
|
||||
await response.json();
|
||||
|
||||
console.log(formResponse);
|
||||
|
||||
router.push('/success');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
alert('Error');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<VerticalLayout>
|
||||
<form className='container' onSubmit={handleSubmit}>
|
||||
<h1>Unsubscribe newsletter</h1>
|
||||
<div className='email block'>
|
||||
<label htmlFor='frm-email'>Email</label>
|
||||
<input
|
||||
placeholder='example@email.com'
|
||||
id='email'
|
||||
type='email'
|
||||
name='email'
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className='button block'>
|
||||
<button type='submit'>Unsubscribe</button>
|
||||
</div>
|
||||
</form>
|
||||
<Button label='Home' onClick={() => router.push('/')} />
|
||||
<Button label='Subscribe' onClick={() => router.push('/subscribe')} />
|
||||
</VerticalLayout>
|
||||
);
|
||||
export default function Unsubscribe() {
|
||||
return <UnsubscribeForm />;
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ResponseSchema = z.object({
|
||||
message: z.string()
|
||||
});
|
||||
|
||||
export const SubscribeFormSchema = z.object({
|
||||
email: z.string().email(),
|
||||
targetingAllowed: z.boolean()
|
||||
});
|
||||
|
||||
export const UnsubscribeFormSchema = z.object({
|
||||
email: z.string().email()
|
||||
});
|
||||
|
||||
export const NewsSchema = z.object({
|
||||
id: z.number(),
|
||||
title: z.string(),
|
||||
text: z.string().optional(),
|
||||
type: z.string(),
|
||||
by: z.string(),
|
||||
time: z.number(),
|
||||
url: z.string().optional(),
|
||||
score: z.number()
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
export const topNews = 'https://hacker-news.firebaseio.com/v0/topstories.json';
|
||||
export const singleNews = (id: number) =>
|
||||
`https://hacker-news.firebaseio.com/v0/item/${id}.json`;
|
||||
Reference in New Issue
Block a user