// "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 (
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" && ( <>Turn on if this post promotes the client's own business, a product or a third party.
{options.discloseCommercial && (By posting, you agree to TikTok's Music Usage Confirmation {branded && <> and Branded Content Policy>}. {" "}After it's published, the post can take a few minutes to appear on the profile.