Mobile "Publish to web" + anonymous demo publish (AI welcome, no auth)
Turns a no-login demo card into a real, scannable web profile with an optional
AI welcome — completing the owner side of the welcome feature without auth.
Backend:
- POST /v1/demo/publish (public, rate-limited 10/h/IP): creates a fresh
password-less owner (so the profile shows the right name + fits the free-tier
1-card limit), builds a card (contact title + profile links), optionally
generates an AI welcome, and publishes. Returns { handle, profileUrl }.
- demo_service + handlers/demo; 2 tests (publish→public profile w/ welcome,
name required). Gate green (120 tests). Verified live: publish → Astro renders
the welcome hero + image (from MinIO) + We-Met form, 18KB page.
Mobile:
- publish.tsx: enter an optional AI welcome scene + message, Publish → shows the
live cardclaws URL (Open/Done) and points the card's QR at it.
- demo.tsx: "Publish to web · AI welcome" button (saves, then opens publish).
- api.publishToWeb; LocalCard.publishedUrl.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
63fdc4913b
commit
fd7a55e809
@@ -178,15 +178,31 @@ export default function CardEditorScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
// Persist the current edits to the local gallery (shared by Save + Publish).
|
||||
const persistCard = async () => {
|
||||
const existing = getById(id);
|
||||
const imagePath = imageUri ? await persistImage(imageUri, id) : (existing?.imagePath ?? "");
|
||||
const videoPath = videoUri ? await persistVideo(videoUri, id) : undefined;
|
||||
upsert({
|
||||
id,
|
||||
name,
|
||||
title,
|
||||
url,
|
||||
imagePath,
|
||||
videoPath,
|
||||
links,
|
||||
publishedUrl: existing?.publishedUrl,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
if (imageUri) setImageUri(imagePath);
|
||||
if (videoPath) setVideoUri(videoPath);
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
if (!imageUri && !videoUri) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
const imagePath = imageUri ? await persistImage(imageUri, id) : (getById(id)?.imagePath ?? "");
|
||||
const videoPath = videoUri ? await persistVideo(videoUri, id) : undefined;
|
||||
upsert({ id, name, title, url, imagePath, videoPath, links, updatedAt: Date.now() });
|
||||
if (imageUri) setImageUri(imagePath);
|
||||
if (videoPath) setVideoUri(videoPath);
|
||||
await persistCard();
|
||||
setOnBack(false);
|
||||
setShowing(true);
|
||||
} finally {
|
||||
@@ -194,6 +210,16 @@ export default function CardEditorScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
const goPublish = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
await persistCard();
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
router.push(`/publish?cardId=${id}`);
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
const existing = getById(id);
|
||||
if (existing?.imagePath) await deleteImage(existing.imagePath);
|
||||
@@ -292,6 +318,15 @@ export default function CardEditorScreen() {
|
||||
{saving ? <ActivityIndicator color="#fff" /> : <Text style={styles.ctaText}>Save card</Text>}
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
style={[styles.publishBtn, saving && styles.ctaDisabled]}
|
||||
disabled={saving}
|
||||
onPress={goPublish}
|
||||
>
|
||||
<MaterialCommunityIcons name="web" size={20} color="#f5f5f7" />
|
||||
<Text style={styles.photoText}>Publish to web · AI welcome</Text>
|
||||
</Pressable>
|
||||
|
||||
{cardId && (
|
||||
<Pressable onPress={onDelete}>
|
||||
<Text style={styles.deleteText}>Delete card</Text>
|
||||
@@ -365,6 +400,15 @@ const styles = StyleSheet.create({
|
||||
marginTop: 8,
|
||||
},
|
||||
ctaDisabled: { opacity: 0.4 },
|
||||
publishBtn: {
|
||||
flexDirection: "row",
|
||||
gap: 8,
|
||||
backgroundColor: "#222228",
|
||||
borderRadius: 16,
|
||||
paddingVertical: 15,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
ctaText: { color: "#fff", fontWeight: "700", fontSize: 17 },
|
||||
deleteText: { color: "#ff453a", textAlign: "center", paddingVertical: 14, fontWeight: "600" },
|
||||
viewerRoot: { flex: 1, backgroundColor: "#0a0a0c" },
|
||||
|
||||
Reference in New Issue
Block a user