Privacy page restyling (#22)
This commit is contained in:
@@ -54,6 +54,22 @@
|
||||
h6 {
|
||||
@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 */
|
||||
|
||||
21
app/page.tsx
21
app/page.tsx
@@ -1,16 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@components/Button';
|
||||
import { CardDescription } from '@components/Card';
|
||||
import { CustomCard } from '@components/CustomCard';
|
||||
import { ErrorMessage } from '@components/ErrorMessage';
|
||||
import { FormControl } from '@components/form/FormControl';
|
||||
import { FormMessage } from '@components/form/FormMessage';
|
||||
import { FormErrorMessage } from '@components/form/FormErrorMessage';
|
||||
import { Input } from '@components/Input';
|
||||
import { SchemaOrg } from '@components/SchemaOrg';
|
||||
import { FormField } from '@contexts/FormField/FormFieldProvider';
|
||||
import { FormItem } from '@contexts/FormItem/FormItemProvider';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { LoadingButton } from '@components/LoadingButton';
|
||||
import {
|
||||
ResponseType,
|
||||
SubscribeFormSchema,
|
||||
@@ -23,6 +23,7 @@ export const Home = () => {
|
||||
const [completed, setCompleted] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [error, setError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
@@ -36,10 +37,13 @@ export const Home = () => {
|
||||
resolver: zodResolver(SubscribeFormSchema),
|
||||
defaultValues: {
|
||||
email: ''
|
||||
}
|
||||
},
|
||||
mode: 'onSubmit',
|
||||
reValidateMode: 'onSubmit'
|
||||
});
|
||||
|
||||
async function handleSubmit(values: SubscribeFormType) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch('/api/subscribe', {
|
||||
method: 'POST',
|
||||
@@ -65,6 +69,8 @@ export const Home = () => {
|
||||
setCompleted(true);
|
||||
} catch (error) {
|
||||
setError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,12 +98,13 @@ export const Home = () => {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className='h-6'>
|
||||
<FormMessage className='text-center' />
|
||||
<FormErrorMessage className='text-center' />
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder='example@example.com'
|
||||
className='text-center'
|
||||
disabled={isLoading}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -105,7 +112,9 @@ export const Home = () => {
|
||||
)}
|
||||
/>
|
||||
<div className='align-top'>
|
||||
<Button type='submit'>Submit</Button>
|
||||
<LoadingButton type='submit' loading={isLoading}>
|
||||
Submit
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
@@ -123,7 +132,7 @@ export const Home = () => {
|
||||
<CustomCard
|
||||
className='max-90vw w-96'
|
||||
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()}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { CustomCard } from '@components/CustomCard';
|
||||
import { SchemaOrg } from '@components/SchemaOrg';
|
||||
import Link from 'next/link';
|
||||
|
||||
const Privacy = () => {
|
||||
const schema = {
|
||||
@@ -14,8 +13,8 @@ const Privacy = () => {
|
||||
};
|
||||
|
||||
const body = (
|
||||
<div className='my-2 max-h-[60vh] overflow-auto'>
|
||||
<p>
|
||||
<div className='privacy-content my-2 max-h-[60vh] space-y-1 overflow-auto'>
|
||||
<p className='leading-relaxed'>
|
||||
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
|
||||
@@ -24,29 +23,23 @@ const Privacy = () => {
|
||||
<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{' '}
|
||||
<Link
|
||||
href='https://www.termsfeed.com/privacy-policy-generator/'
|
||||
target='_blank'
|
||||
>
|
||||
Privacy Policy Generator
|
||||
</Link>
|
||||
.
|
||||
accordance with this Privacy Policy.
|
||||
</p>
|
||||
<br />
|
||||
|
||||
<h2>Interpretation and Definitions</h2>
|
||||
<h3>Interpretation</h3>
|
||||
<p>
|
||||
<p className='leading-relaxed'>
|
||||
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>
|
||||
<br />
|
||||
<h4>Definitions</h4>
|
||||
<p>For the purposes of this Privacy Policy:</p>
|
||||
<ul>
|
||||
|
||||
<h3>Definitions</h3>
|
||||
<p className='leading-relaxed'>
|
||||
For the purposes of this Privacy Policy:
|
||||
</p>
|
||||
<ul className='list-disc space-y-4 pl-6'>
|
||||
<li>
|
||||
<p>
|
||||
<strong>Account</strong> means a unique account created for You to
|
||||
@@ -124,17 +117,18 @@ const Privacy = () => {
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
<h3>Collecting and Using Your Personal Data</h3>
|
||||
<h4>Types of Data Collected</h4>
|
||||
<h6>Personal Data</h6>
|
||||
<p>
|
||||
|
||||
<h2>Data Collection and Usage</h2>
|
||||
<h3>Types of Data Collected</h3>
|
||||
|
||||
<h4>Personal Data</h4>
|
||||
<p className='leading-relaxed'>
|
||||
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>
|
||||
<ul className='list-disc space-y-4 pl-6'>
|
||||
<li>
|
||||
<p>Email address</p>
|
||||
</li>
|
||||
@@ -142,11 +136,13 @@ const Privacy = () => {
|
||||
<p>Usage Data</p>
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
<h6>Usage Data</h6>
|
||||
<p>Usage Data is collected automatically when using the Service.</p>
|
||||
|
||||
<h4>Usage Data</h4>
|
||||
<p className='leading-relaxed'>
|
||||
Usage Data is collected automatically when using the Service.
|
||||
</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
|
||||
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
|
||||
@@ -165,10 +161,12 @@ const Privacy = () => {
|
||||
visit our Service or when You access the Service by or through a mobile
|
||||
device.
|
||||
</p>
|
||||
<br />
|
||||
<h4>Use of Your Personal Data</h4>
|
||||
<p>The Company may use Personal Data for the following purposes:</p>
|
||||
<ul>
|
||||
|
||||
<h2>Use of Your Personal Data</h2>
|
||||
<p className='leading-relaxed'>
|
||||
The Company may use Personal Data for the following purposes:
|
||||
</p>
|
||||
<ul className='list-disc space-y-4 pl-6'>
|
||||
<li>
|
||||
<p>
|
||||
<strong>To provide and maintain our Service</strong>, including to
|
||||
@@ -195,7 +193,7 @@ const Privacy = () => {
|
||||
<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
|
||||
such as a mobile application's 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
|
||||
@@ -239,7 +237,7 @@ const Privacy = () => {
|
||||
</li>
|
||||
</ul>
|
||||
<p>We may share Your personal information in the following situations:</p>
|
||||
<ul>
|
||||
<ul className='list-disc space-y-4 pl-6'>
|
||||
<li>
|
||||
<strong>With Service Providers:</strong> We may share Your personal
|
||||
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.
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
<h4>Retention of Your Personal Data</h4>
|
||||
<p>
|
||||
|
||||
<h2>Data Handling and Security</h2>
|
||||
|
||||
<h3>Retention of Your Personal Data</h3>
|
||||
<p className='leading-relaxed'>
|
||||
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
|
||||
@@ -291,18 +291,20 @@ const Privacy = () => {
|
||||
functionality of Our Service, or We are legally obligated to retain this
|
||||
data for longer time periods.
|
||||
</p>
|
||||
<br />
|
||||
<h4>Transfer of Your Personal Data</h4>
|
||||
<p>
|
||||
|
||||
<h3>Transfer of Your Personal Data</h3>
|
||||
<p className='leading-relaxed'>
|
||||
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
|
||||
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>
|
||||
|
||||
<h3>Security of Your Personal Data</h3>
|
||||
<p className='leading-relaxed'>
|
||||
Your consent to this Privacy Policy followed by Your submission of such
|
||||
information represents Your agreement to that transfer.
|
||||
</p>
|
||||
@@ -313,9 +315,11 @@ const Privacy = () => {
|
||||
a country unless there are adequate controls in place including the
|
||||
security of Your data and other personal information.
|
||||
</p>
|
||||
<br />
|
||||
<h4>Delete Your Personal Data</h4>
|
||||
<p>
|
||||
|
||||
<h4 className='text-lg font-medium text-gray-800'>
|
||||
Delete Your Personal Data
|
||||
</h4>
|
||||
<p className='leading-relaxed'>
|
||||
You have the right to delete or request that We assist in deleting the
|
||||
Personal Data that We have collected about You.
|
||||
</p>
|
||||
@@ -334,29 +338,30 @@ const Privacy = () => {
|
||||
Please note, however, that We may need to retain certain information
|
||||
when we have a legal obligation or lawful basis to do so.
|
||||
</p>
|
||||
<br />
|
||||
<h4>Disclosure of Your Personal Data</h4>
|
||||
<h6>Business Transactions</h6>
|
||||
<p>
|
||||
|
||||
<h2>Legal Disclosures</h2>
|
||||
|
||||
<h3>Business Transactions</h3>
|
||||
<p className='leading-relaxed'>
|
||||
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>
|
||||
<br />
|
||||
<h6>Law enforcement</h6>
|
||||
<p>
|
||||
|
||||
<h3>Law Enforcement</h3>
|
||||
<p className='leading-relaxed'>
|
||||
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>
|
||||
<br />
|
||||
<h6>Other legal requirements</h6>
|
||||
<p>
|
||||
|
||||
<h3>Other Legal Requirements</h3>
|
||||
<p className='leading-relaxed'>
|
||||
The Company may disclose Your Personal Data in the good faith belief
|
||||
that such action is necessary to:
|
||||
</p>
|
||||
<ul>
|
||||
<ul className='list-disc space-y-4 pl-6'>
|
||||
<li>Comply with a legal obligation</li>
|
||||
<li>Protect and defend the rights or property of the Company</li>
|
||||
<li>
|
||||
@@ -368,18 +373,17 @@ const Privacy = () => {
|
||||
</li>
|
||||
<li>Protect against legal liability</li>
|
||||
</ul>
|
||||
<br />
|
||||
<h4>Security of Your Personal Data</h4>
|
||||
<p>
|
||||
|
||||
<h2>Additional Information</h2>
|
||||
<p className='leading-relaxed'>
|
||||
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>
|
||||
<br />
|
||||
<h3>{"Children's Privacy"}</h3>
|
||||
<p>
|
||||
<h3>Children's Privacy</h3>
|
||||
<p className='leading-relaxed'>
|
||||
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
|
||||
@@ -391,24 +395,24 @@ const Privacy = () => {
|
||||
<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
|
||||
require Your parent's consent before We collect and use that
|
||||
information.
|
||||
</p>
|
||||
<br />
|
||||
|
||||
<h3>Links to Other Websites</h3>
|
||||
<p>
|
||||
<p className='leading-relaxed'>
|
||||
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
|
||||
third party's 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>
|
||||
<br />
|
||||
|
||||
<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
|
||||
of any changes by posting the new Privacy Policy on this page.
|
||||
</p>
|
||||
@@ -422,27 +426,18 @@ const Privacy = () => {
|
||||
changes. Changes to this Privacy Policy are effective when they are
|
||||
posted on this page.
|
||||
</p>
|
||||
<br />
|
||||
<h3>Contact Us</h3>
|
||||
<p>
|
||||
|
||||
<h2>Contact Information</h2>
|
||||
<p className='leading-relaxed'>
|
||||
If you have any questions about this Privacy Policy, You can contact us
|
||||
by writing to{' '}
|
||||
{
|
||||
<a href={`mailto:${process.env.NEXT_PUBLIC_BRAND_EMAIL}`}>
|
||||
<a
|
||||
href={`mailto:${process.env.NEXT_PUBLIC_BRAND_EMAIL}`}
|
||||
className='text-purple-600 hover:text-purple-700'
|
||||
>
|
||||
{process.env.NEXT_PUBLIC_BRAND_EMAIL}
|
||||
</a>
|
||||
}{' '}
|
||||
or by visiting{' '}
|
||||
{
|
||||
<Link
|
||||
href={'/privacy'}
|
||||
rel='external nofollow noopener'
|
||||
target='_blank'
|
||||
>
|
||||
this
|
||||
</Link>
|
||||
}{' '}
|
||||
page on our website.
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -453,7 +448,7 @@ const Privacy = () => {
|
||||
<CustomCard
|
||||
className='max-90vh max-90vw'
|
||||
title='Privacy Policy'
|
||||
description='Last updated: December 03, 2023'
|
||||
description='Last updated: November 23, 2024'
|
||||
content={body}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@components/Button';
|
||||
import { CardDescription } from '@components/Card';
|
||||
import { CustomCard } from '@components/CustomCard';
|
||||
import { ErrorMessage } from '@components/ErrorMessage';
|
||||
import { FormControl } from '@components/form/FormControl';
|
||||
import { FormMessage } from '@components/form/FormMessage';
|
||||
import { FormErrorMessage } from '@components/form/FormErrorMessage';
|
||||
import { Input } from '@components/Input';
|
||||
import { LoadingButton } from '@components/LoadingButton';
|
||||
import { SchemaOrg } from '@components/SchemaOrg';
|
||||
import { FormField } from '@contexts/FormField/FormFieldProvider';
|
||||
import { FormItem } from '@contexts/FormItem/FormItemProvider';
|
||||
@@ -24,6 +24,7 @@ const Unsubscribe = () => {
|
||||
const [message, setMessage] = useState('');
|
||||
const [error, setError] = useState(false);
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
@@ -37,7 +38,9 @@ const Unsubscribe = () => {
|
||||
resolver: zodResolver(UnsubscribeFormSchema),
|
||||
defaultValues: {
|
||||
email: ''
|
||||
}
|
||||
},
|
||||
mode: 'onSubmit',
|
||||
reValidateMode: 'onSubmit'
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -47,6 +50,7 @@ const Unsubscribe = () => {
|
||||
}, []);
|
||||
|
||||
async function handleSubmit(values: UnsubscribeFormType) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch('/api/unsubscribe', {
|
||||
method: 'POST',
|
||||
@@ -72,6 +76,8 @@ const Unsubscribe = () => {
|
||||
setCompleted(true);
|
||||
} catch (error) {
|
||||
setError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +93,7 @@ const Unsubscribe = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mb-5 h-32'>
|
||||
<div className='flex h-32 flex-col justify-between'>
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
@@ -99,7 +105,7 @@ const Unsubscribe = () => {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className='h-6'>
|
||||
<FormMessage className='text-center' />
|
||||
<FormErrorMessage className='text-center' />
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -111,8 +117,14 @@ const Unsubscribe = () => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className='align-top'>
|
||||
<Button type='submit'>Submit</Button>
|
||||
<div className='mt-2 flex justify-center'>
|
||||
<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>
|
||||
</form>
|
||||
</FormProvider>
|
||||
@@ -124,10 +136,11 @@ const Unsubscribe = () => {
|
||||
<>
|
||||
<SchemaOrg schema={schema} />
|
||||
<CustomCard
|
||||
className='max-90vw w-96'
|
||||
title='Unsubscribe'
|
||||
description='You sure you want to leave? :('
|
||||
className='w-96'
|
||||
title='Stay in the Loop!'
|
||||
description="Don't miss out on the latest tech insights and community discussions."
|
||||
content={renderContent()}
|
||||
footer={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -7,10 +7,14 @@ export type ButtonProps = {
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ asChild = false, ...props }, ref) => {
|
||||
({ asChild = false, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
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) => (
|
||||
<div
|
||||
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}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -21,7 +21,7 @@ export const CustomCard = ({
|
||||
title,
|
||||
description,
|
||||
content,
|
||||
className,
|
||||
className = '',
|
||||
footer = true
|
||||
}: CardProps) => {
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
@@ -35,14 +35,17 @@ export const CustomCard = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mx-auto w-full max-w-screen-lg px-4 sm:px-6 lg:px-8'>
|
||||
<div className='gradient-border shadow-2xl shadow-black'>
|
||||
<Card className={`z-10 max-w-[90vw] p-8 ${className}`}>
|
||||
<Card
|
||||
className={`z-10 w-full transform p-8 transition-all duration-300 ${className}`}
|
||||
>
|
||||
<CardHeader>
|
||||
<p className='text-xs uppercase text-gray-500'>
|
||||
Hackernews + newsletter
|
||||
</p>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
{description && <CardDescription>{description}</CardDescription>}
|
||||
</CardHeader>
|
||||
<CardContent>{content}</CardContent>
|
||||
{footer && (
|
||||
@@ -52,5 +55,6 @@ export const CustomCard = ({
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,11 +5,12 @@ import { Button } from './Button';
|
||||
interface LinkProps {
|
||||
path: string;
|
||||
text: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function CustomLink({ path, text }: LinkProps) {
|
||||
export default function CustomLink({ path, text, className }: LinkProps) {
|
||||
return (
|
||||
<Button asChild>
|
||||
<Button asChild className={className}>
|
||||
<Link href={path}>{text}</Link>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -3,14 +3,41 @@ import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import CustomLink from './CustomLink';
|
||||
|
||||
const links = [{ name: 'Subscribe', path: '/' }];
|
||||
|
||||
export const Footer = () => {
|
||||
const pathname = usePathname();
|
||||
|
||||
if (pathname === '/confirmation') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === '/unsubscribe') {
|
||||
return (
|
||||
<div className='flex justify-center space-x-4'>
|
||||
<CustomLink path='/' text='Subscribe' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (pathname === '/privacy') {
|
||||
return (
|
||||
<div className='relative flex w-full items-center'>
|
||||
<div className='flex w-full justify-center'>
|
||||
<div className='inline-flex'>
|
||||
<CustomLink path='/' text='Subscribe' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='absolute right-0'>
|
||||
<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 (
|
||||
<div>
|
||||
{pathname === '/' ? (
|
||||
<p className='text-center text-xs text-gray-600'>
|
||||
By subscribing, you agree to our{' '}
|
||||
<Link
|
||||
@@ -21,20 +48,5 @@ export const Footer = () => {
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
) : (
|
||||
<div className='flex justify-center space-x-4'>
|
||||
{links.map(
|
||||
link =>
|
||||
pathname !== link.path &&
|
||||
!(pathname === '/confirmation' && link.path === '/') && (
|
||||
<CustomLink key={link.path} path={link.path} text={link.name} />
|
||||
)
|
||||
)}
|
||||
{pathname === '/privacy' && (
|
||||
<CustomLink path='/unsubscribe' text='Unsubscribe' />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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