{excerpt || "(empty)"}
diff --git a/src/client/components/post-composer.tsx b/src/client/components/post-composer.tsx
index 0d4dd0d..f0d09dc 100644
--- a/src/client/components/post-composer.tsx
+++ b/src/client/components/post-composer.tsx
@@ -1,7 +1,8 @@
import { useState, useEffect, useMemo } from "preact/hooks";
import { Send, Save, ArrowLeft, Image, X, Upload, Loader2 } from "lucide-preact";
import { useApp } from "../context";
-import { PLATFORM_LIMITS, PLATFORM_LABELS, mediaLimitError } from "../types";
+import { PLATFORM_LIMITS, PLATFORM_LABELS, mediaError, mediaShapeError, mediaTypeFromUrl } from "../types";
+import type { MediaItem } from "../types";
import type { Platform } from "../types";
import { PostPreview, ChannelTabs } from "./previews";
@@ -25,7 +26,7 @@ interface Props {
}
export function PostComposer({ editId, navigate }: Props) {
- const { channels, labels, posts, createPost, updatePost, uploadImage } = useApp();
+ const { channels, labels, posts, createPost, updatePost, uploadMedia } = useApp();
const existing = editId ? posts.find((p) => p.id === editId) : null;
@@ -38,7 +39,7 @@ export function PostComposer({ editId, navigate }: Props) {
const [selectedChannels, setSelectedChannels] = useState
+ {item.type === "video" ? (
+ // muted + playsinline so the browser paints a first frame
+ // without the tile becoming something that plays audio.
+
+ ) : (
+
+ )}
- )}
+ {/* Attachment */}
+
- )}
+
+ {/* Instagram requires an attachment — this app publishes photo posts, so
+ a video here is a channel that will fail rather than a Reel. */}
+ {media ? (
+
- )}
+ {/* Attachment */}
+
` in each preview card, because the
+// attachment is the one part of a preview that isn't platform-specific — only
+// its frame is, which is what `class` carries. A preview that quietly rendered
+// a video URL into an
would show a broken tile and tell the author their
+// post is broken when it isn't.
+export function PreviewMedia({ media, class: className }: { media?: MediaItem; class: string }) {
+ if (!media) return null;
+ if (media.type === "video") {
+ return ;
+ }
+ return
;
+}
diff --git a/src/client/components/previews/tiktok-preview.tsx b/src/client/components/previews/tiktok-preview.tsx
index f9df3a9..9aa9d26 100644
--- a/src/client/components/previews/tiktok-preview.tsx
+++ b/src/client/components/previews/tiktok-preview.tsx
@@ -1,10 +1,12 @@
import { Heart, MessageCircle, Bookmark, Share, Music2, Image as ImageIcon } from "lucide-preact";
+import type { MediaItem } from "../../types";
+import { PreviewMedia } from "./preview-media";
interface Props {
username: string;
avatarUrl?: string;
content: string;
- imageUrl?: string;
+ media?: MediaItem;
timeLabel?: string;
}
@@ -21,20 +23,20 @@ function toUsername(name: string): string {
const ACTIONS = [Heart, MessageCircle, Bookmark, Share];
-// Photo post only (matches this app's photo-first media model) — rendered as
-// TikTok's full-bleed vertical feed card with the caption + action rail
-// overlaid, rather than the actual short-form video player.
-export function TikTokPreview({ username, avatarUrl, content, imageUrl, timeLabel }: Props) {
+// TikTok's full-bleed vertical feed card, with the caption + action rail
+// overlaid. Carries a photo post or a video: PreviewMedia renders whichever
+// the post actually attached.
+export function TikTokPreview({ username, avatarUrl, content, media, timeLabel }: Props) {
const handle = toUsername(username);
return (
+ {media ? (
+
- )}
+ {/* Attachment */}
+
or a