// "Post to" picker shared by New post (Publishing) and Schedule (Content library): // Instagram/Facebook from the Meta link, plus every platform connected for the client, each with its own fields. import { useEffect, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { api } from "./api.js"; import { cx, Field } from "./ui.jsx"; const FORMAT_LABEL = { TEXT: "text", IMAGE: "image", CAROUSEL: "multi-image", REELS: "video", STORY: "story" }; /** Platforms the client can post to: [{ id, name, meta, formats, fields, limits, conn }] */ export function useClientPlatforms(client) { const [state, setState] = useState({ loading: true, list: [] }); useEffect(() => { if (!client) { setState({ loading: false, list: [] }); return; } let live = true; setState((s) => ({ ...s, loading: true })); Promise.all([api(`/api/clients/${client}`), api("/api/platforms"), api(`/api/connections?client=${encodeURIComponent(client)}`)]) .then(([c, p, k]) => { if (!live) return; const info = c.client; const list = []; if (info.igId) list.push({ id: "instagram", name: "Instagram", meta: true, formats: ["IMAGE", "REELS", "CAROUSEL", "STORY"], fields: [], limits: { caption: 2200 } }); if (info.pageId) list.push({ id: "facebook", name: "Facebook", meta: true, formats: ["IMAGE", "REELS", "CAROUSEL", "STORY", "TEXT"], fields: [], limits: {} }); for (const pl of p.platforms) { const conn = k.connections.find((x) => x.platform === pl.id && x.status === "connected"); if (conn) list.push({ ...pl, conn }); } setState({ loading: false, list }); }) .catch(() => live && setState({ loading: false, list: [] })); return () => { live = false; }; }, [client]); return state; } /** Default selection for a client: its Meta platforms. */ export const defaultPlatforms = (list) => list.filter((p) => p.meta).map((p) => p.id); function TargetSelect({ conn, value, onChange, id }) { const [opts, setOpts] = useState(null); useEffect(() => { api(`/api/connections/${conn.id}/targets`).then((r) => setOpts(r.targets)).catch(() => setOpts([])); }, [conn.id]); return ( ); } const TT_PRIVACY = { PUBLIC_TO_EVERYONE: "Everyone", MUTUAL_FOLLOW_FRIENDS: "Friends", FOLLOWER_OF_CREATOR: "Followers", SELF_ONLY: "Only me" }; const mediaSrc = (m) => (/^https?:/.test(m) ? m : `/api/schedule/media?path=${encodeURIComponent(m)}`); const isVideoFile = (m) => /\.(mp4|mov|m4v|webm)$/i.test(String(m).split("?")[0]); const fmtSecs = (s) => `${Math.floor(s / 60)} min${s % 60 ? ` ${Math.round(s % 60)} s` : ""}`; /** TikTok's post screen, following its Content Posting API guidelines (creator info, no default audience, disclosure, consent). */ function TikTokFields({ platform, type, options, setOption, caption, media = [] }) { const [creator, setCreator] = useState(null); const [error, setError] = useState(null); const [videoSecs, setVideoSecs] = useState(null); useEffect(() => { api(`/api/connections/${platform.conn.id}/creator-info`).then((r) => setCreator(r.creator)).catch(setError); }, [platform.conn.id]); const video = type === "REELS"; const firstVideo = media.find(isVideoFile); useEffect(() => { setVideoSecs(null); }, [firstVideo]); const tooLong = Boolean(video && creator?.maxVideoSeconds && videoSecs && videoSecs > creator.maxVideoSeconds); // Problems that must stop the post being saved (read by selectionFor through the _problem option). const problem = error ? `TikTok says this account can't post right now: ${error.message}` : tooLong ? `This video is ${fmtSecs(videoSecs)}; this TikTok account can post videos up to ${fmtSecs(creator.maxVideoSeconds)}. Trim it or untick TikTok.` : undefined; useEffect(() => { if ((options._problem || undefined) !== problem) setOption("_problem", problem); }, [problem]); // eslint-disable-line react-hooks/exhaustive-deps const branded = Boolean(options.discloseCommercial && options.brandedContent); const organic = Boolean(options.discloseCommercial && options.brandOrganic); const check = (key, label, disabled, note) => ( ); const limit = platform.limits?.caption; const used = (options.caption ?? caption ?? "").length; return (
{error &&
{problem} If it says the sign-in expired, reconnect TikTok on the Connections page.
} {media.length > 0 && (
Preview of what will be posted
{media.map((m) => isVideoFile(m) ?
{tooLong &&
{problem}
}
)} {!creator && !error &&
Loading the account's TikTok settings…
} {creator && ( )} {creator && options.mode === "inbox" && (

Quorla uploads the {type === "REELS" ? "video" : "photos"} to the account's TikTok inbox. Someone opens TikTok, chooses who can see it, edits if needed and taps Post there.

)} {creator && options.mode !== "inbox" && ( <>
{!video && ( setOption("title", e.target.value)} /> )}
{check("allowComments", "Allow comments", creator.commentDisabled, "off in this account's TikTok settings")} {video && check("allowDuet", "Allow Duet", creator.duetDisabled, "off in TikTok settings")} {video && check("allowStitch", "Allow Stitch", creator.stitchDisabled, "off in TikTok settings")}
{video && creator.maxVideoSeconds &&
This account can post videos up to {Math.floor(creator.maxVideoSeconds / 60)} min {creator.maxVideoSeconds % 60 ? `${creator.maxVideoSeconds % 60} s` : ""}.
}

Turn on if this post promotes the client's own business, a product or a third party.

{options.discloseCommercial && (
{check("brandOrganic", "Your brand: you're promoting yourself or your own business")} {check("brandedContent", "Branded content: you're promoting another brand or a third party", options.privacy === "SELF_ONLY", "not available for “Only me”")} {(organic || branded) &&
The post will be labelled “{branded ? "Paid partnership" : "Promotional content"}”.
} {!organic && !branded &&
Choose at least one option.
}
)}
)}