// Publish a saved demo card to the web (no login) — optionally generating an AI // welcome shown when the QR is scanned. Returns a real cardclaws.com/ // URL and points the card's QR at it. import * as FileSystem from "expo-file-system"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useState } from "react"; import { ActivityIndicator, Alert, Linking, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from "react-native"; import { publishToWeb } from "../src/api/ai"; import { QRCodeView } from "../src/components/card/QRCodeView"; import { wizardInputStyle } from "../src/components/genwizard/Wizard"; import { useLocalCardsStore } from "../src/stores/localCardsStore"; export default function PublishScreen() { const router = useRouter(); const { cardId } = useLocalSearchParams<{ cardId?: string }>(); const card = useLocalCardsStore((s) => (cardId ? s.getById(cardId) : undefined)); const upsert = useLocalCardsStore((s) => s.upsert); const [welcomePrompt, setWelcomePrompt] = useState(""); const [welcomeMessage, setWelcomeMessage] = useState("Great to meet you"); const [payloadKind, setPayloadKind] = useState<"none" | "link" | "code">("none"); const [payloadLabel, setPayloadLabel] = useState(""); const [payloadValue, setPayloadValue] = useState(""); const [now, setNow] = useState(""); const [event, setEvent] = useState(""); const [busy, setBusy] = useState(false); const [result, setResult] = useState(card?.publishedUrl ?? null); if (!card) { return ( Save the card first, then publish. ); } const publish = async () => { setBusy(true); try { // Upload the card's front photo so the web hero shows the real card. let frontImageBase64: string | undefined; let frontImageMime: string | undefined; if (card.imagePath) { try { frontImageBase64 = await FileSystem.readAsStringAsync(card.imagePath, { encoding: FileSystem.EncodingType.Base64, }); frontImageMime = card.imagePath.endsWith(".png") ? "image/png" : "image/jpeg"; } catch { /* no readable photo — publish without it */ } } const { profileUrl } = await publishToWeb({ name: card.name, title: card.title || undefined, frontImageBase64, frontImageMime, links: card.links.filter((l) => l.url.trim()).map((l) => ({ label: l.label, url: l.url })), welcomePrompt: welcomePrompt.trim() || undefined, welcomeMessage: welcomePrompt.trim() ? welcomeMessage : undefined, payload: payloadKind !== "none" && payloadValue.trim() ? { kind: payloadKind, label: payloadLabel.trim(), value: payloadValue.trim() } : undefined, now: now.trim() || undefined, event: event.trim() || undefined, }); // Point the card's QR at the real profile + remember it. upsert({ ...card, url: profileUrl, publishedUrl: profileUrl, updatedAt: Date.now() }); setResult(profileUrl); } catch (e) { const msg = e instanceof Error ? e.message : "Please try again in a moment."; Alert.alert("Couldn't publish", msg); } finally { setBusy(false); } }; return ( Publish “{card.name}” Creates a real, scannable page at cardclaws.com — your card’s QR will point to it. AI welcome scene (optional) {!!welcomePrompt.trim() && ( <> Welcome message )} Event (optional) Now — what you’re up to (optional) Payload drop (optional) {(["none", "link", "code"] as const).map((k) => ( setPayloadKind(k)} style={[styles.kindChip, payloadKind === k && styles.kindOn]} > {k === "none" ? "None" : k === "link" ? "Link" : "Code"} ))} {payloadKind !== "none" && ( <> )} {busy ? ( ) : ( {result ? "Re-publish" : "Publish to web"} )} {busy && !!welcomePrompt.trim() && ( Generating your AI welcome, then publishing… )} {result && ( <> Your live page — show this to scan {result} Linking.openURL(result)}> Open page router.replace("/")}> Done )} ); } const styles = StyleSheet.create({ root: { flex: 1, backgroundColor: "#0a0a0c" }, content: { padding: 20, gap: 12 }, h1: { color: "#f5f5f7", fontSize: 26, fontWeight: "800" }, note: { color: "#9a9aa0", fontSize: 14 }, label: { color: "#9a9aa0", fontSize: 15, marginTop: 8 }, input: { backgroundColor: "#15151a", color: "#f5f5f7", borderRadius: 12, paddingHorizontal: 14, paddingVertical: 12, fontSize: 16, }, cta: { backgroundColor: "#ff3b30", borderRadius: 16, paddingVertical: 16, alignItems: "center", marginTop: 12, }, ctaBusy: { opacity: 0.8 }, ctaText: { color: "#fff", fontWeight: "700", fontSize: 17 }, kindRow: { flexDirection: "row", gap: 8 }, kindChip: { flex: 1, paddingVertical: 10, borderRadius: 10, backgroundColor: "#222228", alignItems: "center", }, kindOn: { backgroundColor: "#ff3b30" }, kindText: { color: "#9a9aa0", fontWeight: "600" }, kindTextOn: { color: "#fff" }, qrWrap: { backgroundColor: "#ffffff", borderRadius: 16, padding: 14, alignSelf: "center", }, url: { color: "#4da3ff", fontSize: 16 }, secondary: { backgroundColor: "#222228", borderRadius: 14, paddingVertical: 14, alignItems: "center", }, secondaryText: { color: "#f5f5f7", fontWeight: "600", fontSize: 16 }, });