Privacy page restyling (#22)

This commit is contained in:
Riccardo Senica
2024-11-23 13:01:48 +01:00
committed by GitHub
parent c300b2501d
commit d8170747c7
12 changed files with 275 additions and 183 deletions

View File

@@ -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 */

View File

@@ -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()}
/> />
</> </>

View File

@@ -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&apos;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&apos;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&apos;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&apos;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&apos;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&apos;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}`}
className='text-purple-600 hover:text-purple-700'
>
{process.env.NEXT_PUBLIC_BRAND_EMAIL} {process.env.NEXT_PUBLIC_BRAND_EMAIL}
</a> </a>
}{' '} .
or by visiting{' '}
{
<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}
/> />
</> </>

View File

@@ -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}
/> />
</> </>
); );

View File

@@ -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}
/>
); );
} }
); );

View File

@@ -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}
/> />
)); ));

View File

@@ -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,14 +35,17 @@ export const CustomCard = ({
} }
return ( 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'> <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> <CardHeader>
<p className='text-xs uppercase text-gray-500'> <p className='text-xs uppercase text-gray-500'>
Hackernews + newsletter Hackernews + newsletter
</p> </p>
<CardTitle>{title}</CardTitle> <CardTitle>{title}</CardTitle>
<CardDescription>{description}</CardDescription> {description && <CardDescription>{description}</CardDescription>}
</CardHeader> </CardHeader>
<CardContent>{content}</CardContent> <CardContent>{content}</CardContent>
{footer && ( {footer && (
@@ -52,5 +55,6 @@ export const CustomCard = ({
)} )}
</Card> </Card>
</div> </div>
</div>
); );
}; };

View File

@@ -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>
); );

View File

@@ -3,14 +3,41 @@ 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();
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 ( return (
<div>
{pathname === '/' ? (
<p className='text-center text-xs text-gray-600'> <p className='text-center text-xs text-gray-600'>
By subscribing, you agree to our{' '} By subscribing, you agree to our{' '}
<Link <Link
@@ -21,20 +48,5 @@ export const Footer = () => {
</Link> </Link>
. .
</p> </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>
); );
}; };

View 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 };

View 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';

View File

@@ -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';