Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "qryptchat-web",
"private": true,
"version": "0.9.1",
"version": "0.9.2",
"type": "module",
"bin": {
"qc": "./bin/qc.js",
Expand Down
4 changes: 2 additions & 2 deletions src/app/client-layout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import ActiveCallInterface from '@/lib/components/voice-call/ActiveCallInterface
// token, since passkey/CoinPay/phone sessions have no cookie.
installApiAuth({ refresh: (token) => useAuthStore.getState().refreshSession(token) });

export default function ClientLayout({ children, footerBottom }) {
export default function ClientLayout({ children }) {
const pathname = usePathname();
const currentTheme = useThemeStore((s) => s.currentTheme);
const { t: _t } = useI18n();
Expand Down Expand Up @@ -60,7 +60,7 @@ export default function ClientLayout({ children, footerBottom }) {
<div className="app" suppressHydrationWarning>
<Navbar />
<main className="main-content">{children}</main>
{shouldShowFooter && <Footer bottom={footerBottom} />}
{shouldShowFooter && <Footer />}
{mounted && (
<>
<PWAToastManager />
Expand Down
9 changes: 5 additions & 4 deletions src/app/layout.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import './globals.css';
import ClientLayout from './client-layout.jsx';
import Script from "next/script";
import { Footer as PfsFooter } from '@profullstack/footer/react';
import PitFooter from '@/lib/components/PitFooter.jsx';

const SITE_URL = (process.env.NEXT_PUBLIC_APP_URL ?? process.env.NEXT_PUBLIC_BASE_URL ?? 'https://qrypt.chat').replace(/\/$/, '');

// Re-render hourly so @profullstack/footer picks up its @latest template.
// Re-render hourly so the pit.world footer stays fresh.
export const revalidate = 3600;

export const metadata = {
Expand Down Expand Up @@ -146,8 +146,9 @@ export default function RootLayout({ children }) {
/>
</head>
<body suppressHydrationWarning>
<ClientLayout footerBottom={<PfsFooter site="https://qrypt.chat/" />}>{children}</ClientLayout>
<Script data-site="38c4083a-a35e-435d-8a0e-3510c465f419" src="https://crawlproof.com/stats.js" strategy="afterInteractive" />
<ClientLayout>{children}</ClientLayout>
<Script data-site="38c4083a-a35e-435d-8a0e-3510c465f419" src="https://crawlproof.com/stats.js" strategy="afterInteractive" />
<PitFooter />
</body>
</html>
);
Expand Down
118 changes: 30 additions & 88 deletions src/lib/components/Footer.jsx
Original file line number Diff line number Diff line change
@@ -1,102 +1,44 @@
'use client';

import { useI18n } from '@/lib/hooks/useI18n.js';

const socialLinks = [
{ name: 'Discord', url: 'https://discord.gg/w5nHdzpQ29' },
{ name: 'GitHub', url: 'https://github.com/profullstack' },
{ name: 'X (Twitter)', url: 'https://x.com/profullstackinc' },
{ name: 'Bluesky', url: 'https://bsky.app/profile/chovyfu.bsky.social' },
const links = [
{ label: 'Security', href: '/security' },
{ label: 'About', href: '/about' },
{ label: 'Blog', href: '/blog' },
{ label: 'FAQ', href: '/faq' },
{ label: 'Contact', href: '/contact' },
{ label: 'Warrant Canary', href: '/warrant-canary' },
{ label: 'Privacy', href: '/privacy' },
{ label: 'Terms', href: '/terms' },
{ label: 'GitHub', href: 'https://github.com/profullstack/qryptchat-web' },
];


export default function Footer({ bottom }) {
const { t } = useI18n();
export default function Footer() {
const onionUrl = process.env.NEXT_PUBLIC_ONION_URL;
const isOnionSite = typeof window !== 'undefined' && window.location.hostname.endsWith('.onion');

return (
<footer className="footer">
<div className="footer-content">
<div className="brand-section">
<div className="brand-header">
<div className="logo">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
<span className="brand-name">QryptChat</span>
</div>
</div>
<p className="brand-tagline">{t('app.tagline')}</p>
{!isOnionSite && onionUrl && (
<div className="onion-link">
<span>馃 Available on Tor:</span>
<a href={onionUrl} className="onion-url" target="_blank" rel="noopener noreferrer">
{onionUrl}
</a>
</div>
)}
</div>

<div className="footer-links">
<div className="link-section">
<h4>Product</h4>
<a href="/security">Security</a>
<a href="/encryption-test">Encryption Test</a>
<a href="/premium">Premium</a>
</div>
<div className="link-section">
<h4>Company</h4>
<a href="/about">About</a>
<a href="/blog">Blog</a>
<a href="/faq">FAQ</a>
<a href="/contact">Contact</a>
<a href="/warrant-canary">Warrant Canary</a>
</div>
<div className="link-section">
<h4>Legal</h4>
<a href="/privacy">Privacy Policy</a>
<a href="/terms">Terms of Service</a>
</div>
</div>

<div className="social-section">
<h4>Connect</h4>
<div className="social-links">
{socialLinks.map((link) => (
<a key={link.name} href={link.url} target="_blank" rel="noopener noreferrer" className="social-link" aria-label={link.name}>
{link.name}
</a>
))}
</div>
</div>
</div>

{/* Copyright + Profullstack webring: @profullstack/footer, server-rendered in app/layout.jsx */}
{bottom && <div className="footer-bottom">{bottom}</div>}
<nav className="footer-links" aria-label="Footer">
{links.map((link) => (
<a key={link.href} href={link.href}>{link.label}</a>
))}
</nav>
<p className="footer-copy">
&copy; {new Date().getFullYear()} <a href="https://profullstack.com/">Profullstack, Inc.</a>
{!isOnionSite && onionUrl && (
<>
{' 路 '}
<a href={onionUrl} target="_blank" rel="noopener noreferrer">Tor .onion</a>
</>
)}
</p>

<style>{`
.footer { background: var(--color-bg-secondary); border-top: 1px solid var(--color-border-primary); padding: var(--space-12) 0 var(--space-6); }
.footer-content { max-width: 1200px; margin: 0 auto; padding: 0 var(--space-6); display: grid; grid-template-columns: 2fr 2fr 1fr; gap: var(--space-8); }
.brand-section .logo { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2); }
.brand-name { font-weight: 700; font-size: 1.125rem; color: var(--color-text-primary); }
.brand-tagline { color: var(--color-text-secondary); font-size: 0.875rem; margin-bottom: var(--space-3); }
.onion-link { font-size: 0.75rem; color: var(--color-text-secondary); }
.onion-url { color: var(--color-brand-primary); word-break: break-all; }
.footer-links { display: flex; gap: var(--space-8); }
.link-section { display: flex; flex-direction: column; gap: var(--space-2); }
.link-section h4 { font-weight: 600; font-size: 0.875rem; color: var(--color-text-primary); margin-bottom: var(--space-1); }
.link-section a { color: var(--color-text-secondary); text-decoration: none; font-size: 0.875rem; transition: color .2s; }
.link-section a:hover { color: var(--color-brand-primary); }
.social-section h4 { font-weight: 600; font-size: 0.875rem; color: var(--color-text-primary); margin-bottom: var(--space-3); }
.social-links { display: flex; flex-direction: column; gap: var(--space-2); }
.social-link { color: var(--color-text-secondary); text-decoration: none; font-size: 0.875rem; transition: color .2s; }
.social-link:hover { color: var(--color-brand-primary); }
.footer-bottom { max-width: 1200px; margin: var(--space-8) auto 0; }
@media (max-width: 768px) {
.footer-content { grid-template-columns: 1fr; }
.footer-links { flex-direction: column; }
}
.footer { background: var(--color-bg-secondary); border-top: 1px solid var(--color-border-primary); padding: var(--space-6) var(--space-6); display: flex; flex-direction: column; align-items: center; gap: var(--space-2); font-size: 0.875rem; color: var(--color-text-secondary); text-align: center; }
.footer-links { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-2) var(--space-5); }
.footer a { color: var(--color-text-secondary); text-decoration: none; transition: color .2s; }
.footer a:hover { color: var(--color-brand-primary); }
.footer-copy { margin: 0; }
`}</style>
</footer>
);
Expand Down
19 changes: 19 additions & 0 deletions src/lib/components/PitFooter.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// The pit.world network footer, fetched on the server and rendered as the last
// thing in <body>. Cached for an hour; any failure (timeout, non-200, odd body)
// renders nothing so the page never breaks.
const PIT_FOOTER_URL = 'https://pit.world/snippets/footer.html?site=qrypt.chat';

export default async function PitFooter() {
try {
const res = await fetch(PIT_FOOTER_URL, {
next: { revalidate: 3600 },
signal: AbortSignal.timeout(3000),
});
if (!res.ok) return null;
const html = await res.text();
if (!html.includes('data-pit-footer')) return null;
return <div dangerouslySetInnerHTML={{ __html: html }} />;
} catch {
return null;
}
}
Loading