Add contact, privacy, and terms pages with localization support

- Implemented contact page with form submission functionality and email integration.
- Created privacy and terms pages with structured content and localization.
- Updated English and French localization files to include new strings for contact, privacy, and terms.
- Enhanced About page with detailed sections on mission, technology, security, and tools offered.
This commit is contained in:
Your Name
2026-03-09 13:24:50 +02:00
parent b9900106b2
commit ce5c85ec7d
10 changed files with 672 additions and 118 deletions

View File

@@ -1,50 +1,103 @@
import { useTranslation } from 'react-i18next';
import { Helmet } from 'react-helmet-async';
import { Link } from 'react-router-dom';
import { Target, Cpu, Shield, Lock, Wrench } from 'lucide-react';
import { FILE_RETENTION_MINUTES } from '@/config/toolLimits';
export default function AboutPage() {
const { t } = useTranslation();
const toolCategories = t('pages.about.toolCategories', { returnObjects: true }) as string[];
return (
<>
<Helmet>
<title>{t('common.about')} {t('common.appName')}</title>
<meta name="description" content="About our free online file conversion tools." />
<title>{t('pages.about.title')} {t('common.appName')}</title>
<meta name="description" content={t('pages.about.metaDescription')} />
<link rel="canonical" href={`${window.location.origin}/about`} />
</Helmet>
<div className="prose mx-auto max-w-2xl dark:prose-invert">
<h1>{t('common.about')}</h1>
<div className="mx-auto max-w-3xl">
<h1 className="mb-8 text-3xl font-bold text-slate-900 dark:text-white">
{t('pages.about.title')}
</h1>
<p>
We provide free, fast, and secure online tools for converting, compressing,
and processing files PDFs, images, videos, and text.
</p>
{/* Mission */}
<section className="mb-10">
<div className="flex items-center gap-3 mb-3">
<Target className="h-6 w-6 text-primary-600 dark:text-primary-400" />
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
{t('pages.about.missionTitle')}
</h2>
</div>
<p className="text-slate-600 dark:text-slate-400 leading-relaxed">
{t('pages.about.missionText')}
</p>
</section>
<h2>Why use our tools?</h2>
<ul>
<li><strong>100% Free</strong> No hidden charges, no sign-up required.</li>
<li><strong>Private & Secure</strong> Files are auto-deleted within {FILE_RETENTION_MINUTES} minutes.</li>
<li><strong>Fast Processing</strong> Server-side processing for reliable results.</li>
<li><strong>Works Everywhere</strong> Desktop, tablet, or mobile.</li>
</ul>
{/* Technology */}
<section className="mb-10">
<div className="flex items-center gap-3 mb-3">
<Cpu className="h-6 w-6 text-primary-600 dark:text-primary-400" />
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
{t('pages.about.technologyTitle')}
</h2>
</div>
<p className="text-slate-600 dark:text-slate-400 leading-relaxed">
{t('pages.about.technologyText')}
</p>
</section>
<h2>Available Tools</h2>
<ul>
<li>PDF conversion tools (PDFWord)</li>
<li>PDF optimization and utility tools (compress, merge, split, rotate, page numbers)</li>
<li>PDF security tools (watermark, protect, unlock)</li>
<li>PDF/image conversion tools (PDFImages, ImagesPDF)</li>
<li>Image processing tools (convert, resize)</li>
<li>Video to GIF tool</li>
<li>Text tools (word counter, cleaner)</li>
<li>PDF to flowchart extraction tool</li>
</ul>
{/* Security */}
<section className="mb-10">
<div className="flex items-center gap-3 mb-3">
<Shield className="h-6 w-6 text-primary-600 dark:text-primary-400" />
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
{t('pages.about.securityTitle')}
</h2>
</div>
<p className="text-slate-600 dark:text-slate-400 leading-relaxed">
{t('pages.about.securityText')}
</p>
</section>
<h2>Contact</h2>
<p>
Have feedback or feature requests? Reach out at{' '}
<a href="mailto:support@example.com">support@example.com</a>.
</p>
{/* File Privacy */}
<section className="mb-10">
<div className="flex items-center gap-3 mb-3">
<Lock className="h-6 w-6 text-primary-600 dark:text-primary-400" />
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
{t('pages.about.privacyTitle')}
</h2>
</div>
<p className="text-slate-600 dark:text-slate-400 leading-relaxed">
{t('pages.about.privacyText', { minutes: FILE_RETENTION_MINUTES })}
</p>
</section>
{/* What We Offer */}
<section className="mb-10">
<div className="flex items-center gap-3 mb-3">
<Wrench className="h-6 w-6 text-primary-600 dark:text-primary-400" />
<h2 className="text-xl font-semibold text-slate-900 dark:text-white">
{t('pages.about.toolsTitle')}
</h2>
</div>
{Array.isArray(toolCategories) && (
<ul className="list-disc space-y-2 pl-5 text-slate-600 dark:text-slate-400">
{toolCategories.map((cat, idx) => (
<li key={idx}>{cat}</li>
))}
</ul>
)}
</section>
{/* CTA */}
<div className="rounded-xl border border-slate-200 bg-white p-6 text-center dark:border-slate-700 dark:bg-slate-800">
<p className="mb-4 text-slate-600 dark:text-slate-400">
<Link to="/contact" className="font-medium text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300">
{t('common.contact')}
</Link>
</p>
</div>
</div>
</>
);

View File

@@ -0,0 +1,179 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Helmet } from 'react-helmet-async';
import { Mail, Send, CheckCircle } from 'lucide-react';
const CONTACT_EMAIL = 'support@saas-pdf.com';
type Category = 'general' | 'bug' | 'feature';
export default function ContactPage() {
const { t } = useTranslation();
const [category, setCategory] = useState<Category>('general');
const [submitted, setSubmitted] = useState(false);
const placeholderKey = `pages.contact.${category}Placeholder` as const;
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = e.currentTarget;
const data = new FormData(form);
const subject = data.get('subject') as string;
const body = data.get('message') as string;
const name = data.get('name') as string;
// Open user's email client with pre-filled fields
const mailto = `mailto:${CONTACT_EMAIL}?subject=${encodeURIComponent(`[${category}] ${subject}`)}&body=${encodeURIComponent(`From: ${name}\n\n${body}`)}`;
window.location.href = mailto;
setSubmitted(true);
}
if (submitted) {
return (
<>
<Helmet>
<title>{t('pages.contact.title')} {t('common.appName')}</title>
</Helmet>
<div className="mx-auto flex max-w-md flex-col items-center gap-4 py-20 text-center">
<CheckCircle className="h-16 w-16 text-green-500" />
<h2 className="text-2xl font-bold text-slate-800 dark:text-slate-100">
{t('pages.contact.successMessage')}
</h2>
<button
onClick={() => setSubmitted(false)}
className="mt-4 rounded-lg bg-primary-600 px-6 py-2 text-white transition-colors hover:bg-primary-700"
>
{t('pages.contact.title')}
</button>
</div>
</>
);
}
return (
<>
<Helmet>
<title>{t('pages.contact.title')} {t('common.appName')}</title>
<meta name="description" content={t('pages.contact.metaDescription')} />
<link rel="canonical" href={`${window.location.origin}/contact`} />
</Helmet>
<div className="mx-auto max-w-2xl">
<div className="mb-8 text-center">
<h1 className="text-3xl font-bold text-slate-800 dark:text-slate-100">
{t('pages.contact.title')}
</h1>
<p className="mt-2 text-slate-600 dark:text-slate-400">
{t('pages.contact.subtitle')}
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6 rounded-xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-700 dark:bg-slate-800">
<h2 className="text-lg font-semibold text-slate-700 dark:text-slate-200">
{t('pages.contact.formTitle')}
</h2>
{/* Category */}
<div>
<label className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
{t('pages.contact.categoryLabel')}
</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value as Category)}
className="w-full rounded-lg border border-slate-300 bg-white px-4 py-2 text-slate-700 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200"
>
<option value="general">{t('pages.contact.categories.general')}</option>
<option value="bug">{t('pages.contact.categories.bug')}</option>
<option value="feature">{t('pages.contact.categories.feature')}</option>
</select>
</div>
{/* Name */}
<div>
<label htmlFor="name" className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
{t('common.name')}
</label>
<input
id="name"
name="name"
type="text"
required
placeholder={t('pages.contact.namePlaceholder')}
className="w-full rounded-lg border border-slate-300 bg-white px-4 py-2 text-slate-700 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200"
/>
</div>
{/* Email */}
<div>
<label htmlFor="email" className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
{t('common.email')}
</label>
<input
id="email"
name="email"
type="email"
required
placeholder={t('pages.contact.emailPlaceholder')}
className="w-full rounded-lg border border-slate-300 bg-white px-4 py-2 text-slate-700 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200"
/>
</div>
{/* Subject */}
<div>
<label htmlFor="subject" className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
{t('common.subject')}
</label>
<input
id="subject"
name="subject"
type="text"
required
placeholder={t('pages.contact.subjectPlaceholder')}
className="w-full rounded-lg border border-slate-300 bg-white px-4 py-2 text-slate-700 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200"
/>
</div>
{/* Message */}
<div>
<label htmlFor="message" className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300">
{t('common.message')}
</label>
<textarea
id="message"
name="message"
rows={6}
required
placeholder={t(placeholderKey)}
className="w-full rounded-lg border border-slate-300 bg-white px-4 py-2 text-slate-700 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-200 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200"
/>
</div>
{/* Submit */}
<button
type="submit"
className="flex w-full items-center justify-center gap-2 rounded-lg bg-primary-600 px-6 py-3 font-medium text-white transition-colors hover:bg-primary-700"
>
<Send className="h-4 w-4" />
{t('common.send')}
</button>
</form>
{/* Direct email fallback */}
<div className="mt-6 text-center text-sm text-slate-500 dark:text-slate-400">
<p>
{t('pages.contact.directEmail')}{' '}
<a
href={`mailto:${CONTACT_EMAIL}`}
className="inline-flex items-center gap-1 font-medium text-primary-600 hover:underline dark:text-primary-400"
>
<Mail className="h-4 w-4" />
{CONTACT_EMAIL}
</a>
</p>
<p className="mt-1">{t('pages.contact.responseTime')}</p>
</div>
</div>
</>
);
}

View File

@@ -3,58 +3,55 @@ import { Helmet } from 'react-helmet-async';
import { FILE_RETENTION_MINUTES } from '@/config/toolLimits';
const LAST_UPDATED = '2026-03-06';
const CONTACT_EMAIL = 'support@saas-pdf.com';
export default function PrivacyPage() {
const { t } = useTranslation();
const fileItems = t('pages.privacy.fileHandlingItems', { minutes: FILE_RETENTION_MINUTES, returnObjects: true }) as string[];
const thirdPartyItems = t('pages.privacy.thirdPartyItems', { returnObjects: true }) as string[];
return (
<>
<Helmet>
<title>{t('common.privacy')} {t('common.appName')}</title>
<meta name="description" content="Privacy policy for our online tools." />
<title>{t('pages.privacy.title')} {t('common.appName')}</title>
<meta name="description" content={t('pages.privacy.metaDescription')} />
<link rel="canonical" href={`${window.location.origin}/privacy`} />
</Helmet>
<div className="prose mx-auto max-w-2xl dark:prose-invert">
<h1>{t('common.privacy')}</h1>
<p><em>Last updated: {LAST_UPDATED}</em></p>
<h1>{t('pages.privacy.title')}</h1>
<p><em>{t('pages.privacy.lastUpdated', { date: LAST_UPDATED })}</em></p>
<h2>1. Data Collection</h2>
<h2>{t('pages.privacy.dataCollectionTitle')}</h2>
<p>{t('pages.privacy.dataCollectionText')}</p>
<h2>{t('pages.privacy.fileHandlingTitle')}</h2>
{Array.isArray(fileItems) && (
<ul>
{fileItems.map((item, idx) => <li key={idx}>{item}</li>)}
</ul>
)}
<h2>{t('pages.privacy.cookiesTitle')}</h2>
<p>{t('pages.privacy.cookiesText')}</p>
<h2>{t('pages.privacy.thirdPartyTitle')}</h2>
{Array.isArray(thirdPartyItems) && (
<ul>
{thirdPartyItems.map((item, idx) => <li key={idx}>{item}</li>)}
</ul>
)}
<h2>{t('pages.privacy.securityTitle')}</h2>
<p>{t('pages.privacy.securityText')}</p>
<h2>{t('pages.privacy.rightsTitle')}</h2>
<p>{t('pages.privacy.rightsText', { minutes: FILE_RETENTION_MINUTES })}</p>
<h2>{t('pages.privacy.contactTitle')}</h2>
<p>
We only collect files you intentionally upload for processing. We do not
require registration, and we do not store personal information.
</p>
<h2>2. File Processing & Storage</h2>
<ul>
<li>Uploaded files are processed on our secure servers.</li>
<li>All uploaded and output files are <strong>automatically deleted within {FILE_RETENTION_MINUTES} minutes</strong>.</li>
<li>Files are stored in encrypted cloud storage during processing.</li>
<li>We do not access, read, or share the content of your files.</li>
</ul>
<h2>3. Cookies & Analytics</h2>
<p>
We use essential cookies to remember your language preference. We may use
Google Analytics and Google AdSense, which may place their own cookies.
You can manage cookie preferences in your browser settings.
</p>
<h2>4. Third-Party Services</h2>
<ul>
<li><strong>Google AdSense</strong> for displaying advertisements.</li>
<li><strong>AWS S3</strong> for temporary file storage.</li>
</ul>
<h2>5. Security</h2>
<p>
We employ industry-standard security measures including HTTPS encryption,
file validation, rate limiting, and automatic file cleanup.
</p>
<h2>6. Contact</h2>
<p>
Questions about this policy? Contact us at{' '}
<a href="mailto:support@example.com">support@example.com</a>.
{t('pages.privacy.contactText')}{' '}
<a href={`mailto:${CONTACT_EMAIL}`}>{CONTACT_EMAIL}</a>.
</p>
</div>
</>

View File

@@ -3,65 +3,58 @@ import { Helmet } from 'react-helmet-async';
import { FILE_RETENTION_MINUTES } from '@/config/toolLimits';
const LAST_UPDATED = '2026-03-06';
const CONTACT_EMAIL = 'support@saas-pdf.com';
export default function TermsPage() {
const { t } = useTranslation();
const useItems = t('pages.terms.useItems', { returnObjects: true }) as string[];
const fileItems = t('pages.terms.fileItems', { minutes: FILE_RETENTION_MINUTES, returnObjects: true }) as string[];
return (
<>
<Helmet>
<title>{t('common.terms')} {t('common.appName')}</title>
<meta name="description" content="Terms of service for our online tools." />
<title>{t('pages.terms.title')} {t('common.appName')}</title>
<meta name="description" content={t('pages.terms.metaDescription')} />
<link rel="canonical" href={`${window.location.origin}/terms`} />
</Helmet>
<div className="prose mx-auto max-w-2xl dark:prose-invert">
<h1>{t('common.terms')}</h1>
<p><em>Last updated: {LAST_UPDATED}</em></p>
<h1>{t('pages.terms.title')}</h1>
<p><em>{t('pages.terms.lastUpdated', { date: LAST_UPDATED })}</em></p>
<h2>1. Acceptance of Terms</h2>
<h2>{t('pages.terms.acceptanceTitle')}</h2>
<p>{t('pages.terms.acceptanceText')}</p>
<h2>{t('pages.terms.serviceTitle')}</h2>
<p>{t('pages.terms.serviceText')}</p>
<h2>{t('pages.terms.useTitle')}</h2>
{Array.isArray(useItems) && (
<ul>
{useItems.map((item, idx) => <li key={idx}>{item}</li>)}
</ul>
)}
<h2>{t('pages.terms.fileTitle')}</h2>
{Array.isArray(fileItems) && (
<ul>
{fileItems.map((item, idx) => <li key={idx}>{item}</li>)}
</ul>
)}
<h2>{t('pages.terms.liabilityTitle')}</h2>
<p>{t('pages.terms.liabilityText')}</p>
<h2>{t('pages.terms.ipTitle')}</h2>
<p>{t('pages.terms.ipText')}</p>
<h2>{t('pages.terms.changesTitle')}</h2>
<p>{t('pages.terms.changesText')}</p>
<h2>{t('pages.terms.contactTitle')}</h2>
<p>
By accessing and using SaaS-PDF, you agree to be bound by these Terms of
Service. If you do not agree, please discontinue use immediately.
</p>
<h2>2. Service Description</h2>
<p>
SaaS-PDF provides free online tools for file conversion, compression,
and transformation. The service is provided &ldquo;as is&rdquo; without
warranties of any kind.
</p>
<h2>3. Acceptable Use</h2>
<ul>
<li>You may only upload files that you have the right to process.</li>
<li>You must not upload malicious, illegal, or copyrighted content without authorization.</li>
<li>Automated or excessive use of the service is prohibited.</li>
</ul>
<h2>4. File Handling</h2>
<ul>
<li>All uploaded and processed files are automatically deleted within {FILE_RETENTION_MINUTES} minutes.</li>
<li>We are not responsible for any data loss during processing.</li>
<li>You are responsible for maintaining your own file backups.</li>
</ul>
<h2>5. Limitation of Liability</h2>
<p>
SaaS-PDF shall not be liable for any direct, indirect, incidental, or
consequential damages resulting from the use or inability to use the
service.
</p>
<h2>6. Changes to Terms</h2>
<p>
We reserve the right to modify these terms at any time. Continued use of
the service after changes constitutes acceptance of the updated terms.
</p>
<h2>7. Contact</h2>
<p>
Questions about these terms? Contact us at{' '}
<a href="mailto:support@example.com">support@example.com</a>.
{t('pages.terms.contactText')}{' '}
<a href={`mailto:${CONTACT_EMAIL}`}>{CONTACT_EMAIL}</a>.
</p>
</div>
</>