Skip to content
Open
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
Binary file added app/apple-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 14 additions & 3 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { CalendarToggleProvider } from '@/components/calendar-toggle-context'
import { MapLoadingProvider } from '@/components/map-loading-context';
import ConditionalLottie from '@/components/conditional-lottie';
import { MapProvider as MapContextProvider } from '@/components/map/map-context'
import { IOSInstallPrompt } from '@/components/pwa/ios-install-prompt'

const fontSans = FontSans({
subsets: ['latin'],
Expand All @@ -28,14 +29,22 @@ const fontPoppins = Poppins({
weight: ['400', '500', '600', '700']
})

const title = ''
const title = 'QCX - AI-powered Search'
const description =
'language to Maps'
'A minimalistic AI-powered search tool that uses advanced models for deep analysis and geospatial data.'

export const metadata: Metadata = {
metadataBase: new URL('https://www.qcx.world'),
title,
description,
appleWebApp: {
capable: true,
statusBarStyle: 'default',
title: 'QCX',
},
formatDetection: {
telephone: false,
},
openGraph: {
Comment on lines 31 to 48

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

title is currently a plain string ('QCX - AI-powered Search'), while the manifest uses a different name ('QCX - AI-powered Search & Analysis') and appleWebApp.title uses 'QCX'. This mismatch can create inconsistent install/UI labels across platforms and metadata previews.

Consider centralizing these values (e.g., a shared APP_NAME/APP_DESCRIPTION constant) so metadata, manifest(), and appleWebApp.title stay aligned.

Suggestion

Unify the app naming/description by extracting constants shared by both app/layout.tsx and app/manifest.ts (e.g., lib/app-metadata.ts). Example:

// lib/app-metadata.ts
export const APP_TITLE = 'QCX'
export const APP_TAGLINE = 'AI-powered Search & Analysis'
export const APP_DESCRIPTION = 'A minimalistic AI-powered search tool that uses advanced models for deep analysis and geospatial data.'

Then in layout/manifest:

const title = `${APP_TITLE} - ${APP_TAGLINE}`

Reply with "@CharlieHelps yes please" if you’d like me to add a commit that consolidates these strings and updates both files accordingly.

title,
description
Expand All @@ -52,7 +61,8 @@ export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
minimumScale: 1,
maximumScale: 1
maximumScale: 1,
themeColor: '#171717'
}

export default function RootLayout({
Expand Down Expand Up @@ -84,6 +94,7 @@ export default function RootLayout({
<Header />
<ConditionalLottie />
{children}
<IOSInstallPrompt />
<Sidebar />
<Footer />
<Toaster />
Expand Down
42 changes: 42 additions & 0 deletions app/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { MetadataRoute } from 'next'

export default function manifest(): MetadataRoute.Manifest {
return {
name: 'QCX - AI-powered Search & Analysis',
short_name: 'QCX',
description: 'A minimalistic AI-powered search tool that uses advanced models for deep analysis and geospatial data.',
start_url: '/',
display: 'standalone',
background_color: '#171717',
theme_color: '#171717',
icons: [
{
src: '/icon-192.png',
sizes: '192x192',
type: 'image/png',
purpose: 'any',
},
{
src: '/icon-512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any',
},
{
src: '/icon-1024-maskable.png',
sizes: '1024x1024',
type: 'image/png',
purpose: 'maskable',
},
],
Comment on lines +12 to +31

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manifest defines a maskable icon only at 1024x1024 and the other icons are purpose: 'any'. Many platforms (notably Android) strongly prefer a maskable 512x512 icon for adaptive launcher shapes, and having maskable only at 1024 may not be used as expected by all install surfaces.

Suggestion

Add a 512x512 maskable variant (and keep any variants). For example, add public/icon-512-maskable.png and include it:

icons: [
  { src: '/icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
  { src: '/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
  { src: '/icon-512-maskable.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
  { src: '/icon-1024-maskable.png', sizes: '1024x1024', type: 'image/png', purpose: 'maskable' },
],

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

screenshots: [
{
src: '/images/opengraph-image.png',
sizes: '1200x630',
type: 'image/png',
label: 'QCX Home Screen',
},
],
categories: ['search', 'ai', 'productivity'],
}
}
56 changes: 56 additions & 0 deletions components/pwa/ios-install-prompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client'

import { useEffect, useState } from 'react'
import { X, Share } from 'lucide-react'
import { Button } from '@/components/ui/button'

export function IOSInstallPrompt() {
const [showPrompt, setShowPrompt] = useState(false)

useEffect(() => {
// Check if it's iOS
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream

// Check if it's already in standalone mode (installed)
const isStandalone = window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as any).standalone

Comment on lines +10 to +17

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The iOS detection and standalone checks rely on navigator.userAgent and (window as any) casts. This works, but it’s brittle (UA spoofing, iPadOS desktop UA changes) and the any casts reduce safety. You can make this more robust by using feature-based checks and avoiding any (e.g., use navigator.standalone via typed narrowing, and use navigator.userAgentData when available).

Suggestion

Consider replacing UA parsing and any casts with a more robust, typed approach:

useEffect(() => {
  const ua = navigator.userAgent
  const isIOS =
    /iPad|iPhone|iPod/.test(ua) ||
    // iPadOS sometimes reports as Mac; detect touch support
    (ua.includes('Mac') && 'ontouchend' in document)

  const isStandalone =
    window.matchMedia('(display-mode: standalone)').matches ||
    // iOS Safari (installed) exposes `navigator.standalone`
    ('standalone' in navigator && (navigator as Navigator & { standalone?: boolean }).standalone)

  const dismissed = sessionStorage.getItem('ios-pwa-prompt-dismissed') === 'true'
  if (isIOS && !isStandalone && !dismissed) setShowPrompt(true)
}, [])

This keeps behavior but reduces brittleness and removes (window as any).

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

// Check if the prompt was already dismissed this session
const isDismissed = sessionStorage.getItem('ios-pwa-prompt-dismissed')

if (isIOS && !isStandalone && !isDismissed) {
setShowPrompt(true)
}
}, [])

const handleDismiss = () => {
setShowPrompt(false)
sessionStorage.setItem('ios-pwa-prompt-dismissed', 'true')
}

if (!showPrompt) return null

return (
<div className="fixed bottom-4 left-4 right-4 z-[100] md:max-w-sm md:left-auto">
<div className="bg-background border rounded-lg shadow-lg p-4 relative animate-in fade-in slide-in-from-bottom-4 duration-300">
<button
onClick={handleDismiss}
className="absolute top-2 right-2 text-muted-foreground hover:text-foreground"
>
<X className="h-4 w-4" />
</button>
Comment on lines +36 to +41

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The close control is implemented as a plain <button> with only an icon. For accessibility, it should have an explicit accessible name (e.g., aria-label). Without it, screen readers may announce it ambiguously.

Suggestion

Add an accessible label (and optionally set type="button" explicitly):

<button
  type="button"
  aria-label="Dismiss install prompt"
  onClick={handleDismiss}
  className="absolute top-2 right-2 text-muted-foreground hover:text-foreground"
>
  <X className="h-4 w-4" />
</button>

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

<div className="flex items-center gap-3">
<div className="bg-primary/10 p-2 rounded-md">
<Share className="h-6 w-6 text-primary" />
</div>
<div>
<h3 className="font-semibold text-sm">Install QCX</h3>
<p className="text-xs text-muted-foreground mt-1">
Tap <span className="inline-block bg-muted px-1 rounded mx-1">Share</span> then <span className="font-semibold">Add to Home Screen</span> to install as an app.
</p>
</div>
</div>
</div>
</div>
)
}
Binary file added public/icon-1024-maskable.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.