cd78f72f33
Complete rename of all technical identifiers across the codebase: Package names (11 packages): - @planarchy/* → @capakraken/* in all package.json, tsconfig, imports Import statements: 277 files, 548 occurrences replaced Database & Docker: - PostgreSQL user/db: planarchy → capakraken - Docker volumes: planarchy_pgdata → capakraken_pgdata - Connection strings updated in docker-compose, .env, CI CI/CD: - GitHub Actions workflow: all filter commands updated - Test database credentials updated Infrastructure: - Redis channel: planarchy:sse → capakraken:sse - Logger service name: planarchy-api → capakraken-api - Anonymization seed updated - Start/stop/restart scripts updated Test data: - Seed emails: @planarchy.dev → @capakraken.dev - E2E test credentials: all 11 spec files updated - Email defaults: @planarchy.app → @capakraken.app - localStorage keys: planarchy_* → capakraken_* Documentation: 30+ .md files updated Verification: - pnpm install: workspace resolution works - TypeScript: only pre-existing TS2589 (no new errors) - Engine: 310/310 tests pass - Staffing: 37/37 tests pass Co-Authored-By: claude-flow <ruv@ruv.net>
103 lines
3.5 KiB
TypeScript
103 lines
3.5 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
|
const DISMISS_KEY = "capakraken_pwa_dismiss";
|
|
const DISMISS_DURATION_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
|
|
interface BeforeInstallPromptEvent extends Event {
|
|
prompt(): Promise<void>;
|
|
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
|
|
}
|
|
|
|
export function InstallPrompt() {
|
|
const [visible, setVisible] = useState(false);
|
|
const deferredPromptRef = useRef<BeforeInstallPromptEvent | null>(null);
|
|
|
|
useEffect(() => {
|
|
// Check if dismissed recently
|
|
try {
|
|
const dismissed = localStorage.getItem(DISMISS_KEY);
|
|
if (dismissed) {
|
|
const timestamp = parseInt(dismissed, 10);
|
|
if (Date.now() - timestamp < DISMISS_DURATION_MS) return;
|
|
}
|
|
} catch {
|
|
// localStorage unavailable
|
|
}
|
|
|
|
// Check if already installed (standalone mode)
|
|
if (window.matchMedia("(display-mode: standalone)").matches) return;
|
|
|
|
const handler = (e: Event) => {
|
|
e.preventDefault();
|
|
deferredPromptRef.current = e as BeforeInstallPromptEvent;
|
|
setVisible(true);
|
|
};
|
|
|
|
window.addEventListener("beforeinstallprompt", handler);
|
|
return () => window.removeEventListener("beforeinstallprompt", handler);
|
|
}, []);
|
|
|
|
const handleInstall = useCallback(async () => {
|
|
const prompt = deferredPromptRef.current;
|
|
if (!prompt) return;
|
|
|
|
await prompt.prompt();
|
|
const { outcome } = await prompt.userChoice;
|
|
|
|
if (outcome === "accepted") {
|
|
setVisible(false);
|
|
}
|
|
deferredPromptRef.current = null;
|
|
}, []);
|
|
|
|
const handleDismiss = useCallback(() => {
|
|
setVisible(false);
|
|
deferredPromptRef.current = null;
|
|
try {
|
|
localStorage.setItem(DISMISS_KEY, String(Date.now()));
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}, []);
|
|
|
|
if (!visible) return null;
|
|
|
|
return (
|
|
<div className="fixed bottom-20 left-4 right-4 z-50 mx-auto max-w-md animate-in slide-in-from-bottom-4 duration-300 sm:left-auto sm:right-6">
|
|
<div className="flex items-center gap-3 rounded-2xl border border-brand-200/60 bg-white/95 px-4 py-3 shadow-lg backdrop-blur-xl dark:border-brand-900/40 dark:bg-slate-900/95">
|
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-brand-600 text-white shadow-md shadow-brand-600/25">
|
|
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
</svg>
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-medium text-gray-900 dark:text-gray-50">
|
|
Install CapaKraken
|
|
</p>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
Add to home screen for quick access
|
|
</p>
|
|
</div>
|
|
<div className="flex shrink-0 gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={handleDismiss}
|
|
className="rounded-lg px-3 py-1.5 text-xs font-medium text-gray-500 transition-colors hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-slate-800"
|
|
>
|
|
Later
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => void handleInstall()}
|
|
className="rounded-lg bg-brand-600 px-3 py-1.5 text-xs font-medium text-white shadow-sm transition-colors hover:bg-brand-700"
|
|
>
|
|
Install
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|