Skip to content

Commit c17ec77

Browse files
ui: mobile hamburger menu, nav rename, honest provenance copy
- Replace the flaky native <details> mobile menu with a real client-side MobileMenu (button + state); reliable toggle on mobile, closes on link tap, outside tap, and Escape; accessible aria-expanded/controls. Uses a hamburger icon that becomes an X when open. - Rename the nav item 'Check image' to 'Check AI / Deepfake' (EN + HI). - Remove the 'Runs entirely in your browser' badge and reword the hero subtitle and privacy note on /check-image: they now say the first check runs on-device and nothing is sent anywhere unless the user chooses the OpenAI deeper check. - Fix a stale .env.example comment that wrongly named Azure; the database is Neon. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2ff787e commit c17ec77

6 files changed

Lines changed: 105 additions & 29 deletions

File tree

asmita/.env.example

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
# =============================================================================
55

66
# --- Database ---------------------------------------------------------------
7-
# Local: a Postgres server on your machine.
8-
# Production: Azure Database for PostgreSQL Flexible Server (free tier).
9-
# Azure connection strings require SSL. Example shape:
10-
# DATABASE_URL=postgresql://USER:PASS@HOST.postgres.database.azure.com:5432/asmita?sslmode=require
11-
# DIRECT_URL should match (used by Prisma migrate).
7+
# Local: a Postgres server on your machine, OR leave DATABASE_URL unset to use
8+
# the in-memory store (store.ts) with no Postgres required.
9+
# Production: Neon (serverless Postgres). Connection strings require SSL and use
10+
# a pooled host for DATABASE_URL and a direct host for DIRECT_URL. Example shape:
11+
# DATABASE_URL=postgresql://USER:PASS@ep-xxxx-pooler.REGION.aws.neon.tech/neondb?sslmode=require
12+
# DIRECT_URL=postgresql://USER:PASS@ep-xxxx.REGION.aws.neon.tech/neondb?sslmode=require
13+
# (DIRECT_URL, without -pooler, is used by Prisma migrate.)
1214
# SHADOW_DATABASE_URL only needed if running `prisma migrate dev` against
1315
# a remote DB; usually leave unset in production.
1416
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/asmita

asmita/src/app/(public)/check-image/page.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,6 @@ export default async function CheckImagePage() {
3030
<p className="muted mx-auto mt-7 max-w-lg text-base leading-[1.7] md:text-lg md:leading-[1.7]">
3131
{t(locale, "check.hero.sub")}
3232
</p>
33-
<div className="mt-6 flex justify-center">
34-
<span className="cf-badge">
35-
<span className="cf-badge-text">🔒 {t(locale, "check.privacy.badge")}</span>
36-
</span>
37-
</div>
3833
</div>
3934
</section>
4035

asmita/src/components/layout/Header.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Link from "next/link";
22
import { LanguageToggle } from "@/components/layout/LanguageToggle";
3+
import { MobileMenu } from "@/components/layout/MobileMenu";
34
import { t, type Locale } from "@/lib/i18n";
45

56
export function Header({ locale }: { locale: Locale }) {
@@ -28,19 +29,7 @@ export function Header({ locale }: { locale: Locale }) {
2829
</nav>
2930
<div className="flex items-center gap-3 md:hidden">
3031
<LanguageToggle initialLocale={locale} />
31-
<details className="relative">
32-
<summary className="btn btn-secondary list-none">{t(locale, "nav.menu")}</summary>
33-
<nav className="panel absolute right-0 top-14 z-30 grid w-56 gap-3 p-4 text-sm font-bold text-[var(--muted)]">
34-
<Link href="/how-it-works">{t(locale, "nav.howItWorks")}</Link>
35-
<Link href="/check-image">{t(locale, "nav.checkImage")}</Link>
36-
<Link href="/resources">{t(locale, "nav.resources")}</Link>
37-
<Link href="/faq">{t(locale, "nav.faq")}</Link>
38-
<Link href="/privacy">{t(locale, "nav.privacy")}</Link>
39-
<Link href="/start" className="btn btn-primary">
40-
{t(locale, "nav.start")}
41-
</Link>
42-
</nav>
43-
</details>
32+
<MobileMenu locale={locale} />
4433
</div>
4534
</div>
4635
</header>
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"use client";
2+
3+
import { useEffect, useRef, useState } from "react";
4+
import Link from "next/link";
5+
import { t, type Locale } from "@/lib/i18n";
6+
7+
const links = [
8+
{ href: "/how-it-works", key: "nav.howItWorks" },
9+
{ href: "/check-image", key: "nav.checkImage" },
10+
{ href: "/resources", key: "nav.resources" },
11+
{ href: "/faq", key: "nav.faq" },
12+
{ href: "/privacy", key: "nav.privacy" },
13+
] as const;
14+
15+
export function MobileMenu({ locale }: { locale: Locale }) {
16+
const [open, setOpen] = useState(false);
17+
const wrapRef = useRef<HTMLDivElement>(null);
18+
19+
// Close on outside tap or Escape. (Each link also closes the menu on click.)
20+
useEffect(() => {
21+
if (!open) return;
22+
function onPointer(e: PointerEvent) {
23+
if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) {
24+
setOpen(false);
25+
}
26+
}
27+
function onKey(e: KeyboardEvent) {
28+
if (e.key === "Escape") setOpen(false);
29+
}
30+
document.addEventListener("pointerdown", onPointer);
31+
document.addEventListener("keydown", onKey);
32+
return () => {
33+
document.removeEventListener("pointerdown", onPointer);
34+
document.removeEventListener("keydown", onKey);
35+
};
36+
}, [open]);
37+
38+
return (
39+
<div className="relative" ref={wrapRef}>
40+
<button
41+
type="button"
42+
className="btn btn-secondary px-2.5"
43+
aria-expanded={open}
44+
aria-controls="mobile-menu"
45+
aria-label={t(locale, "nav.menu")}
46+
onClick={() => setOpen((v) => !v)}
47+
>
48+
<svg
49+
width="22"
50+
height="22"
51+
viewBox="0 0 24 24"
52+
fill="none"
53+
stroke="currentColor"
54+
strokeWidth="2"
55+
strokeLinecap="round"
56+
strokeLinejoin="round"
57+
aria-hidden
58+
>
59+
{open ? (
60+
<>
61+
<path d="M18 6 6 18" />
62+
<path d="m6 6 12 12" />
63+
</>
64+
) : (
65+
<>
66+
<path d="M3 6h18" />
67+
<path d="M3 12h18" />
68+
<path d="M3 18h18" />
69+
</>
70+
)}
71+
</svg>
72+
</button>
73+
{open && (
74+
<nav
75+
id="mobile-menu"
76+
className="panel absolute right-0 top-14 z-30 grid w-56 gap-3 p-4 text-sm font-bold text-[var(--muted)]"
77+
>
78+
{links.map((link) => (
79+
<Link key={link.href} href={link.href} onClick={() => setOpen(false)}>
80+
{t(locale, link.key)}
81+
</Link>
82+
))}
83+
<Link href="/start" className="btn btn-primary" onClick={() => setOpen(false)}>
84+
{t(locale, "nav.start")}
85+
</Link>
86+
</nav>
87+
)}
88+
</div>
89+
);
90+
}

asmita/src/i18n/en.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -750,14 +750,14 @@
750750
"reg.age.title": "Before we begin",
751751
"reg.age.sub": "Please confirm your age to continue.",
752752
"reg.progress.age": "Age",
753-
"nav.checkImage": "Check image",
753+
"nav.checkImage": "Check AI / Deepfake",
754754
"check.pill": "AI image check",
755755
"check.langNote": "Also available in",
756756
"check.hero.title.1": "Check if an image was",
757757
"check.hero.title.2": "made by AI",
758-
"check.hero.sub": "Read the hidden credentials a file carries to see whether it was generated or edited by an AI tool. The image is read on your own device and is never uploaded.",
758+
"check.hero.sub": "See whether an image was generated or edited by an AI tool. The first check runs on your own device. Nothing is sent anywhere unless you choose the deeper check.",
759759
"check.privacy.badge": "Runs entirely in your browser",
760-
"check.privacy.note": "This check happens on your device. The file you choose is never sent to Asmita or anyone else.",
760+
"check.privacy.note": "The first check runs on your device. Your image is only sent anywhere if you choose the deeper check, and then only to verify it.",
761761
"check.age.title": "Before you continue",
762762
"check.age.body": "This tool is only for images of adults. If the person in the image is under 18, please do not upload it here.",
763763
"check.age.confirm": "The person in this image is 18 or older.",

asmita/src/i18n/hi.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -750,14 +750,14 @@
750750
"privacy.s05.i2b.bold": "PDQ hash codes -",
751751
"privacy.s05.i2b.detail": "notices में शामिल करने के लिए ताकि platforms matching content को identify करके proactively block कर सकें। Code सिर्फ़ उन platforms के साथ share होता है जिन्हें आप notice address करते हैं।",
752752
"start.minor.link": "18 साल से कम उम्र के हैं? Support resources ढूँढिए।",
753-
"nav.checkImage": "इमेज जाँचें",
753+
"nav.checkImage": "AI / Deepfake जाँच",
754754
"check.pill": "AI इमेज जाँच",
755755
"check.langNote": "यह भी उपलब्ध है",
756756
"check.hero.title.1": "जानिए कि कोई इमेज",
757757
"check.hero.title.2": "AI से बनी है या नहीं",
758-
"check.hero.sub": "फ़ाइल में छिपे credentials पढ़कर देखिए कि उसे किसी AI टूल से बनाया या एडिट किया गया है या नहीं। इमेज आपके अपने डिवाइस पर ही पढ़ी जाती है, कहीं अपलोड नहीं होती",
758+
"check.hero.sub": "देखिए कि कोई इमेज किसी AI टूल से बनाई या एडिट की गई है या नहीं। पहली जाँच आपके अपने डिवाइस पर होती है। जब तक आप गहरी जाँच नहीं चुनते, कुछ भी कहीं नहीं भेजा जाता",
759759
"check.privacy.badge": "पूरी तरह आपके ब्राउज़र में चलता है",
760-
"check.privacy.note": "यह जाँच आपके डिवाइस पर होती है। आप जो फ़ाइल चुनते हैं, वह Asmita या किसी और को कभी नहीं भेजी जाती",
760+
"check.privacy.note": "पहली जाँच आपके डिवाइस पर होती है। आपकी इमेज तभी कहीं भेजी जाती है जब आप गहरी जाँच चुनते हैं, और तब भी सिर्फ़ उसे जाँचने के लिए",
761761
"check.age.title": "आगे बढ़ने से पहले",
762762
"check.age.body": "यह टूल सिर्फ़ वयस्कों की इमेज के लिए है। अगर इमेज में व्यक्ति 18 साल से कम उम्र का है, तो कृपया उसे यहाँ अपलोड न करें।",
763763
"check.age.confirm": "इस इमेज में व्यक्ति 18 साल या उससे बड़ा है।",

0 commit comments

Comments
 (0)