Privacy page restyling (#22)
This commit is contained in:
@@ -54,6 +54,22 @@
|
|||||||
h6 {
|
h6 {
|
||||||
@apply text-lg italic;
|
@apply text-lg italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.privacy-content h1 {
|
||||||
|
@apply mb-6 text-3xl font-bold text-gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-content h2 {
|
||||||
|
@apply mt-8 border-b border-gray-200 pb-2 text-2xl font-semibold text-gray-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-content h3 {
|
||||||
|
@apply mt-6 text-xl font-medium text-gray-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.privacy-content h4 {
|
||||||
|
@apply mt-4 text-lg font-medium text-gray-800;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card border gradient */
|
/* Card border gradient */
|
||||||
|
|||||||
21
app/page.tsx
21
app/page.tsx
@@ -1,16 +1,16 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
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 { FormErrorMessage } from '@components/form/FormErrorMessage';
|
||||||
import { Input } from '@components/Input';
|
import { Input } from '@components/Input';
|
||||||
import { SchemaOrg } 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';
|
||||||
|
import { LoadingButton } from '@components/LoadingButton';
|
||||||
import {
|
import {
|
||||||
ResponseType,
|
ResponseType,
|
||||||
SubscribeFormSchema,
|
SubscribeFormSchema,
|
||||||
@@ -23,6 +23,7 @@ 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);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
@@ -36,10 +37,13 @@ export const Home = () => {
|
|||||||
resolver: zodResolver(SubscribeFormSchema),
|
resolver: zodResolver(SubscribeFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: ''
|
email: ''
|
||||||
}
|
},
|
||||||
|
mode: 'onSubmit',
|
||||||
|
reValidateMode: 'onSubmit'
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleSubmit(values: SubscribeFormType) {
|
async function handleSubmit(values: SubscribeFormType) {
|
||||||
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/subscribe', {
|
const response = await fetch('/api/subscribe', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -65,6 +69,8 @@ export const Home = () => {
|
|||||||
setCompleted(true);
|
setCompleted(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(true);
|
setError(true);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,12 +98,13 @@ export const Home = () => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className='h-6'>
|
<div className='h-6'>
|
||||||
<FormMessage className='text-center' />
|
<FormErrorMessage className='text-center' />
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder='example@example.com'
|
placeholder='example@example.com'
|
||||||
className='text-center'
|
className='text-center'
|
||||||
|
disabled={isLoading}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -105,7 +112,9 @@ export const Home = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className='align-top'>
|
<div className='align-top'>
|
||||||
<Button type='submit'>Submit</Button>
|
<LoadingButton type='submit' loading={isLoading}>
|
||||||
|
Submit
|
||||||
|
</LoadingButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
@@ -123,7 +132,7 @@ export const Home = () => {
|
|||||||
<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? 👩💻'
|
||||||
description='Subscribe to our newsletter! The top stories from Hackernews for you. Once a day. Every day.'
|
description='Subscribe to our newsletter! Top stories from Hackernews for you. Once a day. Every day.'
|
||||||
content={renderContent()}
|
content={renderContent()}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { CustomCard } from '@components/CustomCard';
|
import { CustomCard } from '@components/CustomCard';
|
||||||
import { SchemaOrg } from '@components/SchemaOrg';
|
import { SchemaOrg } from '@components/SchemaOrg';
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
const Privacy = () => {
|
const Privacy = () => {
|
||||||
const schema = {
|
const schema = {
|
||||||
@@ -14,8 +13,8 @@ const Privacy = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const body = (
|
const body = (
|
||||||
<div className='my-2 max-h-[60vh] overflow-auto'>
|
<div className='privacy-content my-2 max-h-[60vh] space-y-1 overflow-auto'>
|
||||||
<p>
|
<p className='leading-relaxed'>
|
||||||
This Privacy Policy describes Our policies and procedures on the
|
This Privacy Policy describes Our policies and procedures on the
|
||||||
collection, use and disclosure of Your information when You use 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
|
Service and tells You about Your privacy rights and how the law protects
|
||||||
@@ -24,29 +23,23 @@ const Privacy = () => {
|
|||||||
<p>
|
<p>
|
||||||
We use Your Personal data to provide and improve the Service. By using
|
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
|
the Service, You agree to the collection and use of information in
|
||||||
accordance with this Privacy Policy. This Privacy Policy has been
|
accordance with this Privacy Policy.
|
||||||
created with the help of the{' '}
|
|
||||||
<Link
|
|
||||||
href='https://www.termsfeed.com/privacy-policy-generator/'
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
Privacy Policy Generator
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
</p>
|
||||||
<br />
|
|
||||||
<h2>Interpretation and Definitions</h2>
|
<h2>Interpretation and Definitions</h2>
|
||||||
<h3>Interpretation</h3>
|
<h3>Interpretation</h3>
|
||||||
<p>
|
<p className='leading-relaxed'>
|
||||||
The words of which the initial letter is capitalized have meanings
|
The words of which the initial letter is capitalized have meanings
|
||||||
defined under the following conditions. The following definitions shall
|
defined under the following conditions. The following definitions shall
|
||||||
have the same meaning regardless of whether they appear in singular or
|
have the same meaning regardless of whether they appear in singular or
|
||||||
in plural.
|
in plural.
|
||||||
</p>
|
</p>
|
||||||
<br />
|
|
||||||
<h4>Definitions</h4>
|
<h3>Definitions</h3>
|
||||||
<p>For the purposes of this Privacy Policy:</p>
|
<p className='leading-relaxed'>
|
||||||
<ul>
|
For the purposes of this Privacy Policy:
|
||||||
|
</p>
|
||||||
|
<ul className='list-disc space-y-4 pl-6'>
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
<strong>Account</strong> means a unique account created for You to
|
<strong>Account</strong> means a unique account created for You to
|
||||||
@@ -124,17 +117,18 @@ const Privacy = () => {
|
|||||||
</p>
|
</p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<br />
|
|
||||||
<h3>Collecting and Using Your Personal Data</h3>
|
<h2>Data Collection and Usage</h2>
|
||||||
<h4>Types of Data Collected</h4>
|
<h3>Types of Data Collected</h3>
|
||||||
<h6>Personal Data</h6>
|
|
||||||
<p>
|
<h4>Personal Data</h4>
|
||||||
|
<p className='leading-relaxed'>
|
||||||
While using Our Service, We may ask You to provide Us with certain
|
While using Our Service, We may ask You to provide Us with certain
|
||||||
personally identifiable information that can be used to contact or
|
personally identifiable information that can be used to contact or
|
||||||
identify You. Personally identifiable information may include, but is
|
identify You. Personally identifiable information may include, but is
|
||||||
not limited to:
|
not limited to:
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul className='list-disc space-y-4 pl-6'>
|
||||||
<li>
|
<li>
|
||||||
<p>Email address</p>
|
<p>Email address</p>
|
||||||
</li>
|
</li>
|
||||||
@@ -142,11 +136,13 @@ const Privacy = () => {
|
|||||||
<p>Usage Data</p>
|
<p>Usage Data</p>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<br />
|
|
||||||
<h6>Usage Data</h6>
|
<h4>Usage Data</h4>
|
||||||
<p>Usage Data is collected automatically when using the Service.</p>
|
<p className='leading-relaxed'>
|
||||||
|
Usage Data is collected automatically when using the Service.
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Usage Data may include information such as Your Device&aposs Internet
|
Usage Data may include information such as Your Device's Internet
|
||||||
Protocol address (e.g. IP address), browser type, browser version, the
|
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,
|
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
|
the time spent on those pages, unique device identifiers and other
|
||||||
@@ -165,10 +161,12 @@ const Privacy = () => {
|
|||||||
visit our Service or when You access the Service by or through a mobile
|
visit our Service or when You access the Service by or through a mobile
|
||||||
device.
|
device.
|
||||||
</p>
|
</p>
|
||||||
<br />
|
|
||||||
<h4>Use of Your Personal Data</h4>
|
<h2>Use of Your Personal Data</h2>
|
||||||
<p>The Company may use Personal Data for the following purposes:</p>
|
<p className='leading-relaxed'>
|
||||||
<ul>
|
The Company may use Personal Data for the following purposes:
|
||||||
|
</p>
|
||||||
|
<ul className='list-disc space-y-4 pl-6'>
|
||||||
<li>
|
<li>
|
||||||
<p>
|
<p>
|
||||||
<strong>To provide and maintain our Service</strong>, including to
|
<strong>To provide and maintain our Service</strong>, including to
|
||||||
@@ -195,7 +193,7 @@ const Privacy = () => {
|
|||||||
<p>
|
<p>
|
||||||
<strong>To contact You:</strong> To contact You by email, telephone
|
<strong>To contact You:</strong> To contact You by email, telephone
|
||||||
calls, SMS, or other equivalent forms of electronic communication,
|
calls, SMS, or other equivalent forms of electronic communication,
|
||||||
such as a mobile application&aposs push notifications regarding
|
such as a mobile application's push notifications regarding
|
||||||
updates or informative communications related to the
|
updates or informative communications related to the
|
||||||
functionalities, products or contracted services, including the
|
functionalities, products or contracted services, including the
|
||||||
security updates, when necessary or reasonable for their
|
security updates, when necessary or reasonable for their
|
||||||
@@ -239,7 +237,7 @@ const Privacy = () => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>We may share Your personal information in the following situations:</p>
|
<p>We may share Your personal information in the following situations:</p>
|
||||||
<ul>
|
<ul className='list-disc space-y-4 pl-6'>
|
||||||
<li>
|
<li>
|
||||||
<strong>With Service Providers:</strong> We may share Your personal
|
<strong>With Service Providers:</strong> We may share Your personal
|
||||||
information with Service Providers to monitor and analyze the use of
|
information with Service Providers to monitor and analyze the use of
|
||||||
@@ -274,9 +272,11 @@ const Privacy = () => {
|
|||||||
information for any other purpose with Your consent.
|
information for any other purpose with Your consent.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<br />
|
|
||||||
<h4>Retention of Your Personal Data</h4>
|
<h2>Data Handling and Security</h2>
|
||||||
<p>
|
|
||||||
|
<h3>Retention of Your Personal Data</h3>
|
||||||
|
<p className='leading-relaxed'>
|
||||||
The Company will retain Your Personal Data only for as long as is
|
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
|
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
|
retain and use Your Personal Data to the extent necessary to comply with
|
||||||
@@ -291,18 +291,20 @@ const Privacy = () => {
|
|||||||
functionality of Our Service, or We are legally obligated to retain this
|
functionality of Our Service, or We are legally obligated to retain this
|
||||||
data for longer time periods.
|
data for longer time periods.
|
||||||
</p>
|
</p>
|
||||||
<br />
|
|
||||||
<h4>Transfer of Your Personal Data</h4>
|
<h3>Transfer of Your Personal Data</h3>
|
||||||
<p>
|
<p className='leading-relaxed'>
|
||||||
Your information, including Personal Data, is processed at the
|
Your information, including Personal Data, is processed at the
|
||||||
Company&aposs operating offices and in any other places where the
|
Company's operating offices and in any other places where the
|
||||||
parties involved in the processing are located. It means that this
|
parties involved in the processing are located. It means that this
|
||||||
information may be transferred to — and maintained on — computers
|
information may be transferred to — and maintained on — computers
|
||||||
located outside of Your state, province, country or other governmental
|
located outside of Your state, province, country or other governmental
|
||||||
jurisdiction where the data protection laws may differ than those from
|
jurisdiction where the data protection laws may differ than those from
|
||||||
Your jurisdiction.
|
Your jurisdiction.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
|
<h3>Security of Your Personal Data</h3>
|
||||||
|
<p className='leading-relaxed'>
|
||||||
Your consent to this Privacy Policy followed by Your submission of such
|
Your consent to this Privacy Policy followed by Your submission of such
|
||||||
information represents Your agreement to that transfer.
|
information represents Your agreement to that transfer.
|
||||||
</p>
|
</p>
|
||||||
@@ -313,9 +315,11 @@ const Privacy = () => {
|
|||||||
a country unless there are adequate controls in place including the
|
a country unless there are adequate controls in place including the
|
||||||
security of Your data and other personal information.
|
security of Your data and other personal information.
|
||||||
</p>
|
</p>
|
||||||
<br />
|
|
||||||
<h4>Delete Your Personal Data</h4>
|
<h4 className='text-lg font-medium text-gray-800'>
|
||||||
<p>
|
Delete Your Personal Data
|
||||||
|
</h4>
|
||||||
|
<p className='leading-relaxed'>
|
||||||
You have the right to delete or request that We assist in deleting the
|
You have the right to delete or request that We assist in deleting the
|
||||||
Personal Data that We have collected about You.
|
Personal Data that We have collected about You.
|
||||||
</p>
|
</p>
|
||||||
@@ -334,29 +338,30 @@ const Privacy = () => {
|
|||||||
Please note, however, that We may need to retain certain information
|
Please note, however, that We may need to retain certain information
|
||||||
when we have a legal obligation or lawful basis to do so.
|
when we have a legal obligation or lawful basis to do so.
|
||||||
</p>
|
</p>
|
||||||
<br />
|
|
||||||
<h4>Disclosure of Your Personal Data</h4>
|
<h2>Legal Disclosures</h2>
|
||||||
<h6>Business Transactions</h6>
|
|
||||||
<p>
|
<h3>Business Transactions</h3>
|
||||||
|
<p className='leading-relaxed'>
|
||||||
If the Company is involved in a merger, acquisition or asset sale, Your
|
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 may be transferred. We will provide notice before Your
|
||||||
Personal Data is transferred and becomes subject to a different Privacy
|
Personal Data is transferred and becomes subject to a different Privacy
|
||||||
Policy.
|
Policy.
|
||||||
</p>
|
</p>
|
||||||
<br />
|
|
||||||
<h6>Law enforcement</h6>
|
<h3>Law Enforcement</h3>
|
||||||
<p>
|
<p className='leading-relaxed'>
|
||||||
Under certain circumstances, the Company may be required to disclose
|
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
|
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).
|
requests by public authorities (e.g. a court or a government agency).
|
||||||
</p>
|
</p>
|
||||||
<br />
|
|
||||||
<h6>Other legal requirements</h6>
|
<h3>Other Legal Requirements</h3>
|
||||||
<p>
|
<p className='leading-relaxed'>
|
||||||
The Company may disclose Your Personal Data in the good faith belief
|
The Company may disclose Your Personal Data in the good faith belief
|
||||||
that such action is necessary to:
|
that such action is necessary to:
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul className='list-disc space-y-4 pl-6'>
|
||||||
<li>Comply with a legal obligation</li>
|
<li>Comply with a legal obligation</li>
|
||||||
<li>Protect and defend the rights or property of the Company</li>
|
<li>Protect and defend the rights or property of the Company</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -368,18 +373,17 @@ const Privacy = () => {
|
|||||||
</li>
|
</li>
|
||||||
<li>Protect against legal liability</li>
|
<li>Protect against legal liability</li>
|
||||||
</ul>
|
</ul>
|
||||||
<br />
|
|
||||||
<h4>Security of Your Personal Data</h4>
|
<h2>Additional Information</h2>
|
||||||
<p>
|
<p className='leading-relaxed'>
|
||||||
The security of Your Personal Data is important to Us, but remember that
|
The security of Your Personal Data is important to Us, but remember that
|
||||||
no method of transmission over the Internet, or method of electronic
|
no method of transmission over the Internet, or method of electronic
|
||||||
storage is 100% secure. While We strive to use commercially acceptable
|
storage is 100% secure. While We strive to use commercially acceptable
|
||||||
means to protect Your Personal Data, We cannot guarantee its absolute
|
means to protect Your Personal Data, We cannot guarantee its absolute
|
||||||
security.
|
security.
|
||||||
</p>
|
</p>
|
||||||
<br />
|
<h3>Children's Privacy</h3>
|
||||||
<h3>{"Children's Privacy"}</h3>
|
<p className='leading-relaxed'>
|
||||||
<p>
|
|
||||||
Our Service does not address anyone under the age of 13. We do not
|
Our Service does not address anyone under the age of 13. We do not
|
||||||
knowingly collect personally identifiable information from anyone under
|
knowingly collect personally identifiable information from anyone under
|
||||||
the age of 13. If You are a parent or guardian and You are aware that
|
the age of 13. If You are a parent or guardian and You are aware that
|
||||||
@@ -391,24 +395,24 @@ const Privacy = () => {
|
|||||||
<p>
|
<p>
|
||||||
If We need to rely on consent as a legal basis for processing Your
|
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
|
information and Your country requires consent from a parent, We may
|
||||||
require Your parent&aposs consent before We collect and use that
|
require Your parent's consent before We collect and use that
|
||||||
information.
|
information.
|
||||||
</p>
|
</p>
|
||||||
<br />
|
|
||||||
<h3>Links to Other Websites</h3>
|
<h3>Links to Other Websites</h3>
|
||||||
<p>
|
<p className='leading-relaxed'>
|
||||||
Our Service may contain links to other websites that are not operated by
|
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
|
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
|
third party's site. We strongly advise You to review the Privacy
|
||||||
Policy of every site You visit.
|
Policy of every site You visit.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
We have no control over and assume no responsibility for the content,
|
We have no control over and assume no responsibility for the content,
|
||||||
privacy policies or practices of any third party sites or services.
|
privacy policies or practices of any third party sites or services.
|
||||||
</p>
|
</p>
|
||||||
<br />
|
|
||||||
<h3>Changes to this Privacy Policy</h3>
|
<h3>Changes to this Privacy Policy</h3>
|
||||||
<p>
|
<p className='leading-relaxed'>
|
||||||
We may update Our Privacy Policy from time to time. We will notify You
|
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.
|
of any changes by posting the new Privacy Policy on this page.
|
||||||
</p>
|
</p>
|
||||||
@@ -422,27 +426,18 @@ const Privacy = () => {
|
|||||||
changes. Changes to this Privacy Policy are effective when they are
|
changes. Changes to this Privacy Policy are effective when they are
|
||||||
posted on this page.
|
posted on this page.
|
||||||
</p>
|
</p>
|
||||||
<br />
|
|
||||||
<h3>Contact Us</h3>
|
<h2>Contact Information</h2>
|
||||||
<p>
|
<p className='leading-relaxed'>
|
||||||
If you have any questions about this Privacy Policy, You can contact us
|
If you have any questions about this Privacy Policy, You can contact us
|
||||||
by writing to{' '}
|
by writing to{' '}
|
||||||
{
|
<a
|
||||||
<a href={`mailto:${process.env.NEXT_PUBLIC_BRAND_EMAIL}`}>
|
href={`mailto:${process.env.NEXT_PUBLIC_BRAND_EMAIL}`}
|
||||||
{process.env.NEXT_PUBLIC_BRAND_EMAIL}
|
className='text-purple-600 hover:text-purple-700'
|
||||||
</a>
|
>
|
||||||
}{' '}
|
{process.env.NEXT_PUBLIC_BRAND_EMAIL}
|
||||||
or by visiting{' '}
|
</a>
|
||||||
{
|
.
|
||||||
<Link
|
|
||||||
href={'/privacy'}
|
|
||||||
rel='external nofollow noopener'
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
this
|
|
||||||
</Link>
|
|
||||||
}{' '}
|
|
||||||
page on our website.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -453,7 +448,7 @@ const Privacy = () => {
|
|||||||
<CustomCard
|
<CustomCard
|
||||||
className='max-90vh max-90vw'
|
className='max-90vh max-90vw'
|
||||||
title='Privacy Policy'
|
title='Privacy Policy'
|
||||||
description='Last updated: December 03, 2023'
|
description='Last updated: November 23, 2024'
|
||||||
content={body}
|
content={body}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
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 { FormErrorMessage } from '@components/form/FormErrorMessage';
|
||||||
import { Input } from '@components/Input';
|
import { Input } from '@components/Input';
|
||||||
|
import { LoadingButton } from '@components/LoadingButton';
|
||||||
import { SchemaOrg } 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';
|
||||||
@@ -24,6 +24,7 @@ const Unsubscribe = () => {
|
|||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const ref = useRef<HTMLInputElement | null>(null);
|
const ref = useRef<HTMLInputElement | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const schema = {
|
const schema = {
|
||||||
'@context': 'https://schema.org',
|
'@context': 'https://schema.org',
|
||||||
@@ -37,7 +38,9 @@ const Unsubscribe = () => {
|
|||||||
resolver: zodResolver(UnsubscribeFormSchema),
|
resolver: zodResolver(UnsubscribeFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: ''
|
email: ''
|
||||||
}
|
},
|
||||||
|
mode: 'onSubmit',
|
||||||
|
reValidateMode: 'onSubmit'
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -47,6 +50,7 @@ const Unsubscribe = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function handleSubmit(values: UnsubscribeFormType) {
|
async function handleSubmit(values: UnsubscribeFormType) {
|
||||||
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/unsubscribe', {
|
const response = await fetch('/api/unsubscribe', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -72,6 +76,8 @@ const Unsubscribe = () => {
|
|||||||
setCompleted(true);
|
setCompleted(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(true);
|
setError(true);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +93,7 @@ const Unsubscribe = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mb-5 h-32'>
|
<div className='flex h-32 flex-col justify-between'>
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(handleSubmit)}
|
onSubmit={form.handleSubmit(handleSubmit)}
|
||||||
@@ -99,7 +105,7 @@ const Unsubscribe = () => {
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className='h-6'>
|
<div className='h-6'>
|
||||||
<FormMessage className='text-center' />
|
<FormErrorMessage className='text-center' />
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
@@ -111,8 +117,14 @@ const Unsubscribe = () => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className='align-top'>
|
<div className='mt-2 flex justify-center'>
|
||||||
<Button type='submit'>Submit</Button>
|
<LoadingButton
|
||||||
|
type='submit'
|
||||||
|
loading={isLoading}
|
||||||
|
className='rounded bg-gray-50 px-3 py-1.5 text-sm text-gray-500 transition-colors duration-200 hover:bg-gray-100'
|
||||||
|
>
|
||||||
|
Unsubscribe
|
||||||
|
</LoadingButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
@@ -124,10 +136,11 @@ const Unsubscribe = () => {
|
|||||||
<>
|
<>
|
||||||
<SchemaOrg schema={schema} />
|
<SchemaOrg schema={schema} />
|
||||||
<CustomCard
|
<CustomCard
|
||||||
className='max-90vw w-96'
|
className='w-96'
|
||||||
title='Unsubscribe'
|
title='Stay in the Loop!'
|
||||||
description='You sure you want to leave? :('
|
description="Don't miss out on the latest tech insights and community discussions."
|
||||||
content={renderContent()}
|
content={renderContent()}
|
||||||
|
footer={true}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ export type ButtonProps = {
|
|||||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
|
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ asChild = false, ...props }, ref) => {
|
({ asChild = false, className, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : 'button';
|
const Comp = asChild ? Slot : 'button';
|
||||||
return (
|
return (
|
||||||
<Comp className={cn('btn-grad', 'btn-grad-hover')} ref={ref} {...props} />
|
<Comp
|
||||||
|
className={className ?? cn('btn-grad', 'btn-grad-hover')}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ const Card = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('rounded-lg bg-card text-card-foreground', className)}
|
className={cn(
|
||||||
|
'rounded-lg bg-card text-card-foreground transition-all duration-200',
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ 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);
|
||||||
@@ -35,22 +35,26 @@ export const CustomCard = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='gradient-border shadow-2xl shadow-black'>
|
<div className='mx-auto w-full max-w-screen-lg px-4 sm:px-6 lg:px-8'>
|
||||||
<Card className={`z-10 max-w-[90vw] p-8 ${className}`}>
|
<div className='gradient-border shadow-2xl shadow-black'>
|
||||||
<CardHeader>
|
<Card
|
||||||
<p className='text-xs uppercase text-gray-500'>
|
className={`z-10 w-full transform p-8 transition-all duration-300 ${className}`}
|
||||||
Hackernews + newsletter
|
>
|
||||||
</p>
|
<CardHeader>
|
||||||
<CardTitle>{title}</CardTitle>
|
<p className='text-xs uppercase text-gray-500'>
|
||||||
<CardDescription>{description}</CardDescription>
|
Hackernews + newsletter
|
||||||
</CardHeader>
|
</p>
|
||||||
<CardContent>{content}</CardContent>
|
<CardTitle>{title}</CardTitle>
|
||||||
{footer && (
|
{description && <CardDescription>{description}</CardDescription>}
|
||||||
<CardFooter>
|
</CardHeader>
|
||||||
<Footer />
|
<CardContent>{content}</CardContent>
|
||||||
</CardFooter>
|
{footer && (
|
||||||
)}
|
<CardFooter>
|
||||||
</Card>
|
<Footer />
|
||||||
|
</CardFooter>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import { Button } from './Button';
|
|||||||
interface LinkProps {
|
interface LinkProps {
|
||||||
path: string;
|
path: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CustomLink({ path, text }: LinkProps) {
|
export default function CustomLink({ path, text, className }: LinkProps) {
|
||||||
return (
|
return (
|
||||||
<Button asChild>
|
<Button asChild className={className}>
|
||||||
<Link href={path}>{text}</Link>
|
<Link href={path}>{text}</Link>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,38 +3,50 @@ import Link from 'next/link';
|
|||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import CustomLink from './CustomLink';
|
import CustomLink from './CustomLink';
|
||||||
|
|
||||||
const links = [{ name: 'Subscribe', path: '/' }];
|
|
||||||
|
|
||||||
export const Footer = () => {
|
export const Footer = () => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
return (
|
if (pathname === '/confirmation') {
|
||||||
<div>
|
return;
|
||||||
{pathname === '/' ? (
|
}
|
||||||
<p className='text-center text-xs text-gray-600'>
|
|
||||||
By subscribing, you agree to our{' '}
|
if (pathname === '/unsubscribe') {
|
||||||
<Link
|
return (
|
||||||
className='font-medium text-indigo-600 hover:text-indigo-500'
|
<div className='flex justify-center space-x-4'>
|
||||||
href='/privacy'
|
<CustomLink path='/' text='Subscribe' />
|
||||||
>
|
</div>
|
||||||
Privacy Policy
|
);
|
||||||
</Link>
|
}
|
||||||
.
|
|
||||||
</p>
|
if (pathname === '/privacy') {
|
||||||
) : (
|
return (
|
||||||
<div className='flex justify-center space-x-4'>
|
<div className='relative flex w-full items-center'>
|
||||||
{links.map(
|
<div className='flex w-full justify-center'>
|
||||||
link =>
|
<div className='inline-flex'>
|
||||||
pathname !== link.path &&
|
<CustomLink path='/' text='Subscribe' />
|
||||||
!(pathname === '/confirmation' && link.path === '/') && (
|
</div>
|
||||||
<CustomLink key={link.path} path={link.path} text={link.name} />
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
{pathname === '/privacy' && (
|
|
||||||
<CustomLink path='/unsubscribe' text='Unsubscribe' />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className='absolute right-0'>
|
||||||
</div>
|
<CustomLink
|
||||||
|
path='/unsubscribe'
|
||||||
|
text='Unsubscribe'
|
||||||
|
className='rounded bg-gray-50 px-3 py-1.5 text-sm text-gray-500 transition-colors duration-200 hover:bg-gray-100'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p className='text-center text-xs text-gray-600'>
|
||||||
|
By subscribing, you agree to our{' '}
|
||||||
|
<Link
|
||||||
|
className='font-medium text-indigo-600 hover:text-indigo-500'
|
||||||
|
href='/privacy'
|
||||||
|
>
|
||||||
|
Privacy Policy
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
38
components/LoadingButton.tsx
Normal file
38
components/LoadingButton.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button, ButtonProps } from './Button';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
import { cn } from '@utils/cn';
|
||||||
|
|
||||||
|
interface LoadingButtonProps extends ButtonProps {
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoadingButton = React.forwardRef<HTMLButtonElement, LoadingButtonProps>(
|
||||||
|
({ loading, children, disabled, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<Button {...props} disabled={disabled || loading} ref={ref}>
|
||||||
|
<div className='relative'>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-center',
|
||||||
|
loading ? 'invisible' : 'visible'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{loading && (
|
||||||
|
<span className='absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center gap-2'>
|
||||||
|
<Loader2 className='h-4 w-4 animate-spin' />
|
||||||
|
Loading
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
LoadingButton.displayName = 'LoadingButton';
|
||||||
|
|
||||||
|
export { LoadingButton };
|
||||||
25
components/form/FormErrorMessage.tsx
Normal file
25
components/form/FormErrorMessage.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { useFormField } from '@hooks/useFormField';
|
||||||
|
import { XCircle } from 'lucide-react';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
export const FormErrorMessage = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(() => {
|
||||||
|
const { error, formMessageId } = useFormField();
|
||||||
|
const body = error?.message;
|
||||||
|
|
||||||
|
return (
|
||||||
|
body && (
|
||||||
|
<div
|
||||||
|
id={formMessageId}
|
||||||
|
className='flex items-center justify-center gap-2 text-sm text-red-500 duration-200 animate-in fade-in slide-in-from-top-1'
|
||||||
|
>
|
||||||
|
<XCircle className='h-4 w-4' />
|
||||||
|
<span>{body}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
FormErrorMessage.displayName = 'FormErrorMessage';
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { useFormField } from '@hooks/useFormField';
|
|
||||||
import { cn } from '@utils/cn';
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
export const FormMessage = React.forwardRef<
|
|
||||||
HTMLParagraphElement,
|
|
||||||
React.HTMLAttributes<HTMLParagraphElement>
|
|
||||||
>(({ className, children, ...props }, ref) => {
|
|
||||||
const { error, formMessageId } = useFormField();
|
|
||||||
const body = error ? String(error?.message) : children;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!body ? null : (
|
|
||||||
<p
|
|
||||||
ref={ref}
|
|
||||||
id={formMessageId}
|
|
||||||
className={cn('text-sm font-medium text-destructive', className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{body}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
FormMessage.displayName = 'FormMessage';
|
|
||||||
Reference in New Issue
Block a user