forked from Rosetta/landing
Move legal pages to React Router
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Anchor, Text } from '@mantine/core';
|
||||
import { Link } from 'react-router-dom';
|
||||
import classes from './Footer.module.css';
|
||||
|
||||
const LEGAL_NAME = 'ROSETTA CLOUD SERVICES LIMITED';
|
||||
@@ -15,10 +16,10 @@ export function Footer() {
|
||||
<Anchor href="/support/" size="sm" c="dimmed">
|
||||
Support
|
||||
</Anchor>
|
||||
<Anchor href="/privacy/" size="sm" c="dimmed">
|
||||
<Anchor component={Link} to="/privacy" size="sm" c="dimmed">
|
||||
Privacy Policy
|
||||
</Anchor>
|
||||
<Anchor href="/terms/" size="sm" c="dimmed">
|
||||
<Anchor component={Link} to="/terms" size="sm" c="dimmed">
|
||||
Terms of Use
|
||||
</Anchor>
|
||||
</nav>
|
||||
|
||||
163
src/components/LegalPage/LegalPage.module.css
Normal file
163
src/components/LegalPage/LegalPage.module.css
Normal file
@@ -0,0 +1,163 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background:
|
||||
radial-gradient(circle at 50% 0%, light-dark(rgba(34, 139, 230, 0.12), rgba(34, 139, 230, 0.16)), transparent 34rem),
|
||||
light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
|
||||
}
|
||||
|
||||
.shell {
|
||||
padding-top: var(--mantine-spacing-lg);
|
||||
padding-bottom: calc(var(--mantine-spacing-xl) * 2);
|
||||
}
|
||||
|
||||
.header {
|
||||
min-height: 58px;
|
||||
padding-bottom: var(--mantine-spacing-md);
|
||||
border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--mantine-spacing-xs);
|
||||
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white));
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.brand img {
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
}
|
||||
|
||||
.main {
|
||||
width: min(100%, 820px);
|
||||
margin: 0 auto;
|
||||
padding-top: 76px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
padding: 0 var(--mantine-spacing-md) calc(var(--mantine-spacing-xl) * 1.5);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-top: var(--mantine-spacing-sm);
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
|
||||
font-family: Outfit, var(--mantine-font-family);
|
||||
font-size: clamp(40px, 7vw, 62px);
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 1.08;
|
||||
}
|
||||
|
||||
.document {
|
||||
padding: 44px 48px 52px;
|
||||
background: light-dark(rgba(255, 255, 255, 0.9), rgba(20, 21, 23, 0.9));
|
||||
border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin: 0;
|
||||
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
|
||||
font-size: var(--mantine-font-size-lg);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.text,
|
||||
.list {
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1));
|
||||
font-size: var(--mantine-font-size-md);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0 0 var(--mantine-spacing-md);
|
||||
}
|
||||
|
||||
.text:last-child,
|
||||
.address:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-bottom: var(--mantine-spacing-md);
|
||||
padding-left: var(--mantine-spacing-xs);
|
||||
}
|
||||
|
||||
.list li::marker {
|
||||
color: var(--mantine-color-blue-5);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-top: 42px;
|
||||
padding-top: 36px;
|
||||
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
margin: 0 0 var(--mantine-spacing-md);
|
||||
color: light-dark(var(--mantine-color-dark-9), var(--mantine-color-white));
|
||||
font-family: Outfit, var(--mantine-font-family);
|
||||
font-size: clamp(22px, 3vw, 28px);
|
||||
font-weight: 500;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.document a {
|
||||
color: light-dark(var(--mantine-color-blue-7), var(--mantine-color-blue-3));
|
||||
text-underline-offset: 3px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.address {
|
||||
margin: var(--mantine-spacing-md) 0;
|
||||
border-left: 3px solid var(--mantine-color-blue-5);
|
||||
background: light-dark(var(--mantine-color-blue-0), var(--mantine-color-dark-6));
|
||||
}
|
||||
|
||||
.address p {
|
||||
margin: 0;
|
||||
color: light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0));
|
||||
line-height: 1.65;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 72px;
|
||||
padding-top: var(--mantine-spacing-lg);
|
||||
border-top: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
|
||||
}
|
||||
|
||||
@media (max-width: 48em) {
|
||||
.main {
|
||||
padding-top: 52px;
|
||||
}
|
||||
|
||||
.document {
|
||||
padding: 32px 24px 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 36em) {
|
||||
.shell {
|
||||
padding-inline: var(--mantine-spacing-md);
|
||||
}
|
||||
|
||||
.header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.intro {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.document {
|
||||
margin-inline: calc(var(--mantine-spacing-xs) * -1);
|
||||
padding-inline: 20px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
165
src/components/LegalPage/LegalPage.tsx
Normal file
165
src/components/LegalPage/LegalPage.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
Anchor,
|
||||
Box,
|
||||
Container,
|
||||
Group,
|
||||
List,
|
||||
Paper,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { RosettaLogo } from '../RosettaLogo/RosettaLogo';
|
||||
import classes from './LegalPage.module.css';
|
||||
|
||||
type LegalRoute = 'privacy' | 'terms';
|
||||
|
||||
interface LegalPageProps {
|
||||
active: LegalRoute;
|
||||
title: string;
|
||||
updated: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface LegalSectionProps {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function LegalPage({ active, title, updated, children }: LegalPageProps) {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<Box className={classes.page}>
|
||||
<Container size="lg" className={classes.shell}>
|
||||
<Group component="header" justify="space-between" className={classes.header}>
|
||||
<Anchor
|
||||
component={Link}
|
||||
to="/"
|
||||
className={classes.brand}
|
||||
aria-label="Rosetta home"
|
||||
>
|
||||
<RosettaLogo size={34} />
|
||||
<Text component="span" fw={700} fz="lg">
|
||||
Rosetta
|
||||
</Text>
|
||||
</Anchor>
|
||||
|
||||
<Group component="nav" gap="lg" aria-label="Legal pages">
|
||||
<Anchor href="/support/" c="dimmed" size="sm">
|
||||
Support
|
||||
</Anchor>
|
||||
<Anchor
|
||||
component={Link}
|
||||
to="/privacy"
|
||||
c={active === 'privacy' ? 'blue' : 'dimmed'}
|
||||
fw={active === 'privacy' ? 600 : 400}
|
||||
size="sm"
|
||||
aria-current={active === 'privacy' ? 'page' : undefined}
|
||||
>
|
||||
Privacy
|
||||
</Anchor>
|
||||
<Anchor
|
||||
component={Link}
|
||||
to="/terms"
|
||||
c={active === 'terms' ? 'blue' : 'dimmed'}
|
||||
fw={active === 'terms' ? 600 : 400}
|
||||
size="sm"
|
||||
aria-current={active === 'terms' ? 'page' : undefined}
|
||||
>
|
||||
Terms
|
||||
</Anchor>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Box component="main" className={classes.main}>
|
||||
<Box className={classes.intro}>
|
||||
<Text tt="uppercase" fw={700} fz="xs" c="blue" lts="0.08em">
|
||||
Rosetta legal
|
||||
</Text>
|
||||
<Title order={1} className={classes.title}>
|
||||
{title}
|
||||
</Title>
|
||||
<Text c="dimmed" mt="md">
|
||||
Last updated: {updated}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Paper
|
||||
component="article"
|
||||
radius="lg"
|
||||
withBorder
|
||||
shadow="sm"
|
||||
className={classes.document}
|
||||
>
|
||||
{children}
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
<Group component="footer" justify="space-between" className={classes.footer}>
|
||||
<Text c="dimmed" fz="xs">
|
||||
© {currentYear} ROSETTA CLOUD SERVICES LIMITED
|
||||
</Text>
|
||||
<Group component="nav" gap="md" aria-label="Footer links">
|
||||
<Anchor component={Link} to="/" c="dimmed" size="xs">
|
||||
Rosetta home
|
||||
</Anchor>
|
||||
<Anchor href="/support/" c="dimmed" size="xs">
|
||||
Support
|
||||
</Anchor>
|
||||
<Anchor
|
||||
component={Link}
|
||||
to={active === 'privacy' ? '/terms' : '/privacy'}
|
||||
c="dimmed"
|
||||
size="xs"
|
||||
>
|
||||
{active === 'privacy' ? 'Terms of Use' : 'Privacy Policy'}
|
||||
</Anchor>
|
||||
</Group>
|
||||
</Group>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function LegalSection({ title, children }: LegalSectionProps) {
|
||||
return (
|
||||
<Box component="section" className={classes.section}>
|
||||
<Title order={2} className={classes.sectionTitle}>
|
||||
{title}
|
||||
</Title>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function LegalText({
|
||||
children,
|
||||
lead = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
lead?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Text component="p" className={lead ? classes.lead : classes.text}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export function LegalList({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<List spacing="xs" className={classes.list}>
|
||||
{children}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
export function LegalAddress({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<Paper radius="md" p="md" className={classes.address}>
|
||||
<Text component="p">{children}</Text>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -5,28 +5,31 @@ interface SEOProps {
|
||||
description?: string;
|
||||
ogImage?: string;
|
||||
keywords?: string;
|
||||
canonical?: string;
|
||||
}
|
||||
|
||||
export function SEO({
|
||||
title = 'Rosetta - Secure Messaging for Everyone',
|
||||
description = 'End-to-end encrypted messaging app with military-grade security. Download for Windows, Linux, and Android.',
|
||||
ogImage = '/og-image.png',
|
||||
keywords = 'secure messaging, encrypted chat, private messenger'
|
||||
ogImage = '/og.png',
|
||||
keywords = 'secure messaging, encrypted chat, private messenger',
|
||||
canonical = 'https://rosetta.im/'
|
||||
}: SEOProps) {
|
||||
useEffect(() => {
|
||||
// Update title
|
||||
document.title = title;
|
||||
|
||||
// Update meta tags
|
||||
updateMetaTag('name', 'description', description);
|
||||
updateMetaTag('name', 'keywords', keywords);
|
||||
updateMetaTag('property', 'og:title', title);
|
||||
updateMetaTag('property', 'og:description', description);
|
||||
updateMetaTag('property', 'og:image', ogImage);
|
||||
updateMetaTag('property', 'og:url', canonical);
|
||||
updateMetaTag('name', 'twitter:title', title);
|
||||
updateMetaTag('name', 'twitter:description', description);
|
||||
updateMetaTag('name', 'twitter:image', ogImage);
|
||||
}, [title, description, ogImage, keywords]);
|
||||
updateMetaTag('name', 'twitter:url', canonical);
|
||||
updateCanonical(canonical);
|
||||
}, [title, description, ogImage, keywords, canonical]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -42,3 +45,15 @@ function updateMetaTag(attr: string, name: string, content: string) {
|
||||
|
||||
element.setAttribute('content', content);
|
||||
}
|
||||
|
||||
function updateCanonical(href: string) {
|
||||
let element = document.querySelector<HTMLLinkElement>('link[rel="canonical"]');
|
||||
|
||||
if (!element) {
|
||||
element = document.createElement('link');
|
||||
element.rel = 'canonical';
|
||||
document.head.appendChild(element);
|
||||
}
|
||||
|
||||
element.href = href;
|
||||
}
|
||||
|
||||
12
src/components/ScrollToTop/ScrollToTop.tsx
Normal file
12
src/components/ScrollToTop/ScrollToTop.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export function ScrollToTop() {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'instant' });
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user