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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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