Commit graph

42 commits

Author SHA1 Message Date
Paulo Castellano
82eeaa82d9
Merge branch 'main' into 69-feature-aspect-ratio-selection-for-facebook-posts-like-instagram 2026-06-10 13:14:55 -03:00
Paulo Castellano
e25b320ad2 Add aspect ratio selection for Facebook posts
Facebook feed posts now have the same aspect ratio selector as Instagram
(1:1, 4:5, 16:9, Original), letting users control how their image is cropped
before publishing instead of leaving it to Facebook's auto-crop.

The Facebook Graph API has no crop/aspect-ratio parameter for organic Page
photo posts (image crops are ads-only) — Facebook displays the bytes it
receives at their native ratio. So, exactly like Instagram, the crop is done
server-side before upload.

- Extract the Instagram crop logic into a shared CropsImageForAspectRatio
  concern (download -> crop -> store -> URL), with a per-publisher
  cropFailureException hook. InstagramPublisher refactored to use it
  (behaviour identical; existing tests unchanged).
- FacebookPublisher reads meta.aspect_ratio and crops single- and multi-image
  feed posts (facebook-crops/ prefix). Reels/Stories are unaffected.
- FacebookSettings: aspect ratio picker on the Feed variant (default Original
  -> no crop). ScheduleTab wires meta; FacebookPreview reflects the chosen
  ratio. Adds posts.form.facebook.aspect.* in en/es/pt-BR.

Closes #69
2026-06-05 18:01:45 -03:00
Paulo Castellano
50477b97f0 fix(tiktok): align UX strings and render processing notice for app review
Match the literal copy the TikTok app reviewer looks for during the
Content Posting API audit, per the Mixpost playbook (the only public
account of how to pass).

- Privacy dropdown placeholder: "Select who can view this post"
- Tooltip on interaction toggles disabled by creator: "Turned off in
  your TikTok account settings"
- Render the "may take a few minutes" notice that was translated but
  never displayed (processing_hint)

Mirror both i18n changes in pt-BR and es. Add a feature test that locks
the literal strings so they cannot drift before the next review cycle.
2026-05-29 10:16:04 -03:00
Paulo Castellano
8dfa664fa6 fix(facebook): restrict Page Stories to video-only
Facebook Stories do not work reliably with images via the Graph API.
Align validation and publisher with video-only (like Reel), while
Instagram Story continues to accept image or video.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 21:21:50 -03:00
Paulo Castellano
560393db2a feat: regenerate AI post images with brand palette in editor
Let users adjust AI-generated slides in place via async job and Echo, while applying workspace brand, background, and text colors to image prompts. Autofill swaps site text/background colors for the image palette, and regeneration is blocked on finalized posts with safer job cleanup.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 16:27:55 -03:00
Paulo Castellano
df35d1b671 feat(posts): add validation message for past date selection in PickTimePopover
- Introduced a new translation key for future date selection prompts in English, Spanish, and Portuguese.
- Implemented logic to disable the confirm button and display a warning message when a past date is selected in the PickTimePopover component.
- Updated date formatting utility to handle UTC dates for HTML datetime-local inputs.
2026-05-19 16:26:40 -03:00
Paulo Castellano
99d1770c6c fix(post): edit redirect loop + frontend validations
- PostController@edit was redirecting Failed→show while show was
  redirecting Failed→edit, producing ERR_TOO_MANY_REDIRECTS. Failed
  posts now render in show.
- New universal `hasContentOrMedia` rule in Edit.vue blocks publishing
  when both text and media are empty (closes the hole where empty posts
  could reach the publish button).
- Unified `PLATFORM_VARIANTS` to include Facebook, Instagram and
  LinkedIn variants. togglePlatform snaps to a compatible variant when
  reselecting a platform whose current content_type is incompatible
  with the attached media (fixes the case where Reel+image left the
  tile permanently blocked).
- platformIssues suppresses the issue on deselected tiles when a
  compatible variant exists, so the tile remains clickable and the
  snap can recover state.
- Use ContentType enum in place of string literals.
2026-05-19 14:18:13 -03:00
Paulo Castellano
d8f836a569 chore(i18n): drop orphan cannot_edit_published key + gitignore compiled JSONs
- Remove cannot_edit_published from en/es/pt-BR posts.php — its sole
  caller (App/PostController) was deleted in the previous commit
  along with PostAction::AlreadyPublished.
- Add lang/php_*.json to .gitignore. They are build artifacts emitted
  by laravel-vue-i18n/vite from the lang/{locale}/*.php sources and
  shouldn't have been tracked (regenerated on every npm run build /
  composer dev). Untracking now also means the orphan key disappears
  from the compiled bundles on the next build without a manual edit.
2026-05-19 13:12:13 -03:00
Paulo Castellano
3f6032c152 fix(facebook): empty-message rejection + state consistency + no re-publish on terminal
Production incident: a customer's Facebook Page post failed with 'The post
is empty. Please enter a message to share.' (error code 197) and ended up
with a contradictory DB state (status=published + error_message=set).

Three independent bugs were uncovered:

A. FacebookPublisher sends 'message'/'description' as null when the user
   posts media without text. Graph API requires the key be omitted, not
   null. Fixed in publishSingleImagePost, publishMultiImagePost,
   publishVideoPost, publishReel.

B. markAsPublished/markAsFailed leak stale fields across transitions
   (a published row could retain error_message from a prior failure,
   vice-versa). Both transitions now explicitly clear the opposite
   side's fields.

C. status='failed' was editable in the UI and the backend, so users
   were re-clicking Publish, generating duplicate failure emails and
   the contradictory state from bug B. The frontend isReadOnly check
   and the UpdatePost backend guard now treat Published/PartiallyPublished/
   Failed/Publishing as terminal. To retry, the user duplicates the post.

11 new tests guarantee these can't regress silently: FB payload shape
per content type, PostPlatform field-clearing on transitions, and the
terminal-status block at the controller level.
2026-05-19 12:47:18 -03:00
Paulo Castellano
d336f79059 feat(social): reschedule publish on PlatformUnavailable instead of failing
Before: a scheduled post hitting a platform outage was marked Failed —
user had to manually retry. Now the job reschedules itself for 10
minutes later and the PostPlatform shows status "Retrying". Loops
indefinitely until the platform accepts the post.

- Adds PostPlatformStatus::Retrying (existing string column, no migration)
- PublishToSocialPlatform: PlatformUnavailable catch now calls
  rescheduleForRetry() which (a) updates the row to Retrying with
  retry_count + next_attempt_at in error_context, and (b) dispatches
  itself with a 10-minute delay. updatePostStatus() naturally leaves
  the parent Post in Publishing because Retrying is neither Published
  nor Failed.
- Same treatment for the retry-refresh edge case (publisher throws
  TokenExpired, refresh subsequently fails with PlatformUnavailable).
- i18n + frontend status config updated (en, pt-BR, es) for both
  posts.status.retrying and posts.edit.status.retrying.
- Tests: 3 new tests covering the dispatched job, the edge case path,
  and retry_count increment across attempts.
2026-05-19 10:03:30 -03:00
Paulo Castellano
a96136925c fix(posts): drop redundant 'publishing' toast on publish
Show.vue already renders a full-screen overlay with spinner + the same
'post is being published' messaging while post.status === 'publishing'.
The flash toast was saying the same thing transiently — duplicate UX
that also contributed to the visual noise as Echo events triggered
partial reloads.

- Remove session()->flash() for the Publishing action in PostController
- Drop the now-orphan 'flash.publishing' key from en/pt-BR/es

Scheduled-action flash kept (Show.vue has no equivalent overlay for it).
2026-05-15 12:23:52 -03:00
Paulo Castellano
44d891ef08 fix(pinterest): restore board picker + require board_id in validation
The post editor lost the Pinterest board picker during a UI rewrite,
causing scheduled posts to fail in production with 'Pinterest board_id
is required'. This restores the picker and locks the contract with
validation + tests so the regression cannot happen silently again.

Backend:
- PostController: pinterestBoards is now Record<account_id, Board[]>
  (mirrors the TikTok creator-info pattern); supports multi-account.
- UpdatePostRequest: 'platforms.*.meta.board_id' rule + after-validator
  rejects Publishing/Scheduling Pinterest posts without board_id.

Frontend:
- PinterestSettings.vue: Combobox board picker with empty-state warning;
  emits update:meta with board_id.
- ScheduleTab / PostEditorSidebar / Edit pass pinterestBoards down by
  social_account_id.

Tests (6 new):
- UpdatePostRequestTest: rejects publishing/scheduling without board_id
  across pin/carousel/video pin; allows draft without board_id;
  pinterest error doesn't block sibling platforms in multi-platform.
- PinterestPublisherTest: publisher throws for carousel + video pin
  when no board_id (existing image-pin case kept).

1542 tests passing.
2026-05-15 11:51:20 -03:00
Paulo Castellano
af96cb0a0e feat(posts): multi-select label filter on the posts list
Adds a combobox-style filter to the posts index toolbar so users can
narrow All / Scheduled / Posted / Drafts views by one or more labels.

- `PostController::index` accepts `?labels[]=<id>` and applies
  `whereHas('labels', whereIn(...))` (OR semantics across selected labels).
  Workspace labels are exposed to the page (sorted by name) and the
  selected set comes back under `filters.labels`.
- New `LabelFilter.vue` component reuses the existing Popover + Command
  pattern (matching `FontPicker` in the Brand settings page). Trigger
  renders the selected `LabelBadge`s inline (mirroring how each post row
  already displays its labels): 1-3 shown directly, 4+ shown as the
  first three plus a "+N" overflow indicator. Clear button has a
  tooltip and `cursor-pointer`, and stops `click`/`pointerdown`/
  `mousedown` so it doesn't reopen the Popover.
- Existing search debounce is shared with the new label watcher via a
  single `buildFilterUrl` helper. URL is updated with `preserveState +
  replace` so the back stack stays clean.
- i18n in en / pt-BR / es: `filter_by_label`, `label_search_placeholder`,
  `no_labels`, `clear_label_filter`.

Tests: 4 new index tests covering the labels prop exposure, single-label
filter, multi-label OR filter, and blank-id sanitization. Full suite:
1509 passed, 2 skipped, 0 failed.
2026-05-14 09:57:55 -03:00
Paulo Castellano
620d23187e fix(social): handle TokenExpired status fail-fast and notify user
Three related fixes for the failure mode where a scheduled post errors out
as 'An unknown X error occurred.' when a social account's refresh_token
was already invalidated by the provider:

1. **PublishToSocialPlatform**: fail-fast when account status is
   `TokenExpired`. Previously the job tried to publish, the publisher
   internally tried to refresh, the provider rejected the rotated
   refresh_token, and the failure surfaced as a generic 'unknown' error
   instead of a clear 'reconnect your account' signal.

2. **XPublisher::refreshToken**: when the OAuth endpoint rejects the
   refresh_token (typically because it was rotated/revoked at X), log the
   raw response and throw `TokenExpiredException` instead of falling
   through to `XPublishException::fromApiResponse` which expects the
   tweet-API response shape (`type`/`title`/`detail`) and treats
   OAuth-style responses (`error`/`error_description`) as 'Unknown'.

3. **SocialAccount::markAsTokenExpired**: dispatch an in-app + email
   notification (`Type::AccountDisconnected`) when an account
   transitions from `Connected` → `TokenExpired`, mirroring the
   existing pattern in `markAsDisconnected`. Wrapped in a lock to
   prevent duplicate notifications on concurrent transitions. Accepts an
   optional `notify: false` so the batch verifier
   (`VerifyWorkspaceConnections`) can suppress per-account
   notifications and rely on its summary email.
2026-05-12 18:46:06 -03:00
Paulo Castellano
b9636c6f49 feat(posts): lock scheduled posts behind explicit unschedule + extract PostEditorHeader
Scheduled posts were silently editable: auto-save fired on every keystroke
with status='scheduled', and the backend either silently failed (per-platform
content-length, our new rule) or accepted invalid changes that blew up at
publish time. Editing a queued post is also a race with the publisher job.

UX change: scheduled state now reads-only the editor and exposes one
explicit escape — Unschedule to edit. The post drops to draft, user edits,
re-schedules. Matches Buffer/Hootsuite/Later/Postiz.

Implementation:
- New isLocked computed (= isReadOnly || isScheduled) gates save(),
  triggerAutosave(), debouncedSave(), togglePlatform(). submit() stays on
  isReadOnly so unschedulePost('draft') can still transition out.
- Editor body wraps in pointer-events-none + opacity-60 when isScheduled,
  visually dimming inputs without per-input :disabled plumbing.
- Header swaps to a violet "This post is scheduled / Unschedule to edit"
  banner replacing the entire normal action bar. Delete is hidden in
  this state — only escape is Unschedule, then act on the draft.

Refactor: extracted the entire header into PostEditorHeader.vue (matches
the PostEditorComposer / PostEditorSidebar family). Single <header> root
with v-if/v-else branches by status. ~85 lines out of Edit.vue.

Cleanup: dropped orphan posts.edit.scheduled_for translation key from
en/pt-BR/es — replaced by the new scheduled_overlay_* keys.
2026-05-11 20:57:17 -03:00
Paulo Castellano
45b5b41923 feat: add thread title support to multi-language files and post edit interface 2026-05-11 20:47:53 -03:00
Paulo Castellano
953be22b5b fix(posts): block scheduling when content exceeds any platform's char limit
Threads posts over 500 chars were saved + scheduled successfully and only
failed inside the publish job. The frontend already showed the 537|500 badge
but `canSchedule` ignored content length, so Schedule and Post Now stayed
enabled. Backend `UpdatePostRequest` only capped at 63206 (Facebook's max),
not per-platform.

- Add `Platform::contentOverflow()` as the single source of truth and reuse it
  from `HasSocialHttpClient::validateContentLength` (publish-time).
- New `ContentFitsPlatformLimits` rule applied to the `content` field on
  `App\\UpdatePostRequest`, `Api\\UpdatePostRequest`, and `Api\\StorePostRequest`
  via `Rule::when(...)` so drafts are not blocked.
- Rule dedupes per platform (two Threads accounts -> one error) and reports
  the platform label, hard cap, and overage via i18n.
- Edit.vue feeds `contentLengthOverflows` into `canSchedule` and lists each
  offending platform in `postActionTooltip` using the existing
  `getPlatformLabel` resolver.
2026-05-11 19:39:41 -03:00
Paulo Castellano
0682b6503c perf(tiktok): load creator_info synchronously and cache it
Two related fixes that together eliminate the 'Loading your TikTok
account settings…' flicker users were seeing on every keystroke /
variant click in the post editor:

1. PostController::edit no longer wraps tiktokCreatorInfos in
   Inertia::defer. The map is computed during the initial render and
   shipped as a regular prop. Without defer, the prop never resets to
   null between Inertia visits, so the loading line never reappears.

2. TikTokCreatorInfo::fetch is now wrapped in a 5-minute Cache::remember
   keyed by social_account_id. Autosaves (which round-trip through
   PostController::update → back() → edit() again) used to issue a
   fresh TikTok API call for every connected account on every save —
   now the cache short-circuits them. Creator info changes very rarely
   (only when the user updates privacy settings on TikTok itself), so
   five minutes of staleness is acceptable; the worst case is a
   slightly out-of-date privacy-options list that corrects on next
   page load.

Frontend cleanup: dropped the creatorInfoLoading prop, the inline
loading <p>, and the now-orphaned posts.form.tiktok.creator_info_loading
i18n key in en/pt-BR/es. ScheduleTab no longer passes the prop.
2026-05-09 15:01:22 -03:00
Paulo Castellano
c5e667f92a refactor(tiktok): use i18n for content type description and validator error
- ContentType::description() now reads from posts.content_types.{value}.description
  instead of hardcoded English. Adds the missing tiktok_photo entries in en/pt-BR/es
  and syncs three Pinterest descriptions that had drifted between the i18n file and
  the previously-hardcoded enum strings (the enum strings were the user-visible source).
- UpdatePostRequest::withValidator surfaces the privacy_required validation error
  via posts.form.tiktok.privacy_required (added in en/pt-BR/es) instead of an
  English string.
- Drops a verbose 3-line comment in withValidator that explained obvious code.
2026-05-09 13:56:00 -03:00
Paulo Castellano
2c08da7787 feat(tiktok): photo carousel support + UX Content Sharing API compliance
## Photo carousel support

- Adds `ContentType::TikTokPhoto` enum case (max 35 photos, 1:1 aspect,
  supportsImage true, supportsVideo false) and JS mirror in content-type.ts.
- Variant pill picker (Video / Photo carousel) at the top of TikTokSettings,
  mirroring the Instagram pattern. Wired through ScheduleTab to the parent
  editor's existing update:platformContentType emit.
- i18n keys for variant_label / variant.video / variant.photo in en/pt-BR/es.
- Publisher: split buildPostInfo into buildVideoPostInfo (uses `title`,
  TikTok cap 2200 chars) and buildPhotoPostInfo (uses `description`, cap
  4000 chars; omits Duet/Stitch/AIGC since they don't apply). Removed the
  no-longer-needed queryCreatorInfo() call from publishVideo/publishPhotos
  — its only previous consumer (silent privacy_level fallback) is gone.

## UX Content Sharing API compliance

Per TikTok review feedback citing
https://developers.tiktok.com/doc/content-sharing-guidelines#required_ux_implementation_in_your_app

Point 1 — already satisfied (creator_info fetch + nickname display).

Point 2/4 — Music Usage Confirmation declaration is now always visible
in TikTokSettings; text changes between "Music Usage Confirmation" and
"Branded Content Policy and Music Usage Confirmation" based on toggle
state. Previously the entire `<p>` block was conditional on a brand
toggle being selected, hiding the baseline declaration.

Point 2b — privacy_level may not have a default. UI was already correct;
backend hardened: UpdatePostRequest now requires meta.privacy_level for
tiktok platforms when status is publishing/scheduled (via withValidator);
TikTokPublisher::resolveRequiredPrivacyLevel throws TikTokPublishException
(ContentPolicy category) when missing instead of silently falling back to
the creator's preferred level.

Point 2c — interaction settings now condition on content type:
- Photo posts hide Duet/Stitch (they don't apply per TikTok docs).
- Photo posts hide AIGC (also video-only).
- Video posts hide Auto Add Music (photos-only feature).
- Max-duration warning hidden when not a video post.
Source of truth is the user-selected contentType prop, not inferred
from media — ensures the UI reacts immediately to the variant pill.

Point 3a — publish button stays disabled when Disclose toggle is on
without a sub-selection (already the case via tiktokComplianceValid).
The disabled tooltip now uses the verbatim TikTok-required text "You
need to indicate if your content promotes yourself, a third party, or
both." instead of the generic "Some platform settings are incomplete..."
when the only blocker is TikTok disclosure incompleteness.

Point 3b — SELF_ONLY (Only me) privacy option is no longer filtered out
when Branded Content is checked. It is rendered disabled with a hover
tooltip "Branded content visibility cannot be set to private." plus a
persistent amber warning paragraph below the dropdown. When the user
toggles Branded Content while privacy is SELF_ONLY, the privacy clears
and a vue-sonner warning toast surfaces the change.

## Cross-cutting

- New `resources/js/enums/platform.ts` mirrors the PHP Platform enum,
  used in Edit.vue (tiktokComplianceValid + tiktokDisclosureIncomplete)
  and ScheduleTab.vue (all selected*Platforms computeds) to replace
  string literal comparisons against `'tiktok'` / `'facebook'` / etc.
- PostPlatformFactory tiktok() state defaults meta.privacy_level to
  SELF_ONLY so existing test fixtures keep passing under the new
  publisher/FormRequest requirements.

## Tests

- New tests/Unit/Enums/PostPlatform/TikTokPhotoContentTypeTest.php
  covering the new enum case (4 tests).
- TikTokPublisherTest: added "video uses title not description" and
  "throws when meta.privacy_level missing" regression tests; renamed
  two existing tests that depended on the removed silent fallback.
- New tests/Feature/UpdatePostRequestTest.php with 3 tests covering
  the FormRequest's privacy_level enforcement (publish-rejected,
  publish-passes, draft-allowed).

Full Pest suite: 1490 passed, 2 skipped (pre-existing).
2026-05-09 12:47:49 -03:00
Paulo Castellano
ff1cc63d9b refactor: introduce VideoPreview component and standardize media handling across post previews 2026-05-08 18:30:11 -03:00
Paulo Castellano
148a2f432f feat: AI image generation pipeline and post creation overhaul
Core changes:
- Replace Unsplash slide pipeline with gpt-image-2 via Laravel AI SDK.
  New AiImageClient builds prompts from a Blade template seeded by the
  workspace's ImageStyle enum, content language, brand color (mapped to a
  human-readable name via HexColorName helper) and brand description.
- Drop Template B from TemplateImageGenerator: every slide now renders as
  Template A (full-bleed photo + bottom gradient + white/grey overlay).
  Removes renderTemplateB, roundCorners, blendHex, ensureContrast and the
  closing-slide pipeline.
- StreamPostCreation creates the Post directly and dispatches
  PostCreationReady with post_id; the wizard kills its preview step and
  redirects straight to the post editor on completion. Finalize endpoint
  removed.
- New Workspace.image_style enum field with an 8-option visual picker
  shared by /workspaces/create and /settings/workspace/brand via a single
  BrandForm component (autofill is a prop). 8 sample webp thumbs ship
  under public/images/branding/image-styles/.
- Media items gain optional source ('ai'|'unsplash'|'giphy') and
  source_meta (recipe needed to regenerate AI images later); the gallery
  picker tags Unsplash/Giphy attachments.
- Brand-color autofill: new CssColorFrequencyExtractor parses every
  hex/rgb/hsl value in the homepage CSS, clusters perceptually similar
  shades in CIE LAB (Delta E 76 < 12), filters neutrals and returns the
  most frequent cluster. Solves Tailwind/utility-CSS sites where no
  semantic --primary variable is exposed.
- Credits: gpt-image-2 metered at 15 credits/image (low quality default).
- Layout: AuthSplitLayout right column is sticky/h-svh so the form
  textarea growth no longer stretches the marketing slider.
- i18n cleanup: localized labels follow the no-em-dash convention.
2026-05-08 13:38:30 -03:00
Paulo Castellano
7733d573da refactor: in-page page headers, indies tabs, and post details polish
- Replace global AppHeader slots with in-page headers on Calendar, Edit,
  and Show pages — full-width canvas with the page-specific controls
  living in their own ink-bordered bar.
- Remove sidebar collapse mechanism: drop Cmd+B keyboard shortcut, force
  defaultOpen=true (ignore persisted cookie), make toggleSidebar a no-op
  on desktop. Sidebar is permanent on desktop, sheet behavior preserved
  on mobile.
- Redesign Tabs to match the design system: each TabsTrigger renders as
  a standalone sticker button (border-2 + rounded-md + shadow-xs), with
  amber-200 active state mirroring the "POST" badge from post-templates.
- Show page layout: cap preview Card to max-w-xl, preserve natural
  aspect for single-image posts (object-contain, max-h-[480px]), drop
  redundant stats grid, swap header sides (Back button left, status +
  date right with date first).
- Assets: bump GalleryBrowser search inputs to h-12 for comfortable
  browsing across uploads, Unsplash, and Giphy tabs.
- Fix PostPlatformMetrics loading-forever bug: hoist useHttp out of the
  onMounted callback into setup, and read the response array directly
  (backend returns the array at root, not under .data).
- i18n: rename posts.show.back_to_posts → posts.show.back; remove unused
  posts.show.summary.* keys.
2026-05-06 16:22:55 -03:00
Paulo Castellano
acdc78f8bd refactor: standardize UI components, modernize localization structure, and remove legacy appearance management middleware. 2026-05-06 14:26:18 -03:00
Paulo Castellano
6f3bdb2caa feat: add duplicate post functionality and migrate LinkedIn analytics to the /rest/ API. 2026-05-03 22:37:51 -03:00
Paulo Castellano
0f385e1b59 refactor: reorganize billing language keys and implement AccountPolicy for subscription management 2026-05-03 17:26:55 -03:00
Paulo Castellano
b47f2488d0 refactor: replace hashtags functionality with reusable signatures feature 2026-05-03 15:23:30 -03:00
Paulo Castellano
d39124e90b refactor: reorganize settings UI, migrate post templates to a file-based registry, and remove legacy video generation features 2026-05-03 13:44:13 -03:00
Paulo Castellano
e6efa8410e refactor: optimize platform enum and enhance AI content generator prompts with localization updates 2026-05-03 10:17:01 -03:00
Paulo Castellano
028fe1fefd feat: add image title and body fields to AI post creation and register PostTemplateSeeder in local environments 2026-05-03 10:06:02 -03:00
Paulo Castellano
1e1519876d feat: replace legacy AI assistant with modular post content generation, review, and template management system 2026-05-03 09:36:50 -03:00
Paulo Castellano
531bb23315 feat: implement platform-specific media validation and content length constraints in PostEditor 2026-05-02 15:32:55 -03:00
Paulo Castellano
02f45e31e7 feat: implement post editor sidebar with tabbed navigation and add support for LinkedIn and Pinterest platform settings 2026-05-02 14:50:36 -03:00
Paulo Castellano
f3605717c7 refactor: unify social analytics, reorganize workspace settings, and implement content validation rules 2026-05-02 12:22:42 -03:00
Paulo Castellano
dafdd5da43 feat: media gallery picker, custom emoji picker, preview tabs, real platform logos
- gallery: extract /assets tabs (uploads, Unsplash, Giphy) into shared
  GalleryBrowser used by both /assets and a new MediaPickerDialog inside the
  post editor; add JSON search endpoint for workspace assets with tests
- emoji: replace broken emoji-picker-element web component with a custom
  EmojiPicker (full Unicode set, search, categories, recently-used,
  light/dark, i18n)
- preview tab: platform selector pills, variant tabs (data-driven from
  content_types map) so the user can switch Feed/Reel/Story etc. and have
  it autosave through the same handler ScheduleTab uses
- platform logos: shared usePlatformLogo composable (logo + label + content
  types); replaces inline maps across 5 components, fixes
  instagram-facebook falling back to default.png
- tooltips: hover details (display_name · @username + platform label) on
  platform avatars across editor, posts list and calendar
- settings cards: show ` · @username` in the title bar so multiple accounts
  on the same network are distinguishable
- routes: drop the throttle:6,1 group middleware on social connect routes
  (was 429ing legitimate OAuth retries) and rely on the default limiter
2026-05-01 14:53:49 -03:00
Paulo Castellano
35646bbaf6 chore: working 2026-04-23 13:23:24 -03:00
Paulo Castellano
b3b59b4d13 refactor: remove onboarding flow, implement brand analysis services, and replace setup middleware with account readiness checks 2026-04-16 23:05:51 -03:00
Paulo Castellano
cb91529964 chore: working 2026-04-02 17:57:06 -03:00
Paulo Castellano
66d0731090 fix: YouTube requires content text — frontend validation now shows error when content is empty 2026-04-01 15:12:56 -03:00
Paulo Castellano
843b3991ec chore: posthog, ui, features and more 2026-03-30 21:18:07 -03:00
Paulo Castellano
dacdc80fa0 fix: add pending status i18n for PostPlatform in all 3 languages 2026-03-30 16:55:14 -03:00
Paulo Castellano
56b8c92e72 refactor: settings redesign, Spanish translations, language system, strict_types
Settings pages:
- Redesign layout to match Sendkit (max-w-4xl, space-y-12, Separator sections)
- Merge Members page into Workspace settings with Table, invite Dialog, ConfirmDeleteModal
- Add workspace logo upload/delete routes and controller methods
- Translate all hardcoded strings in Workspace.vue modals

Language system:
- Drop languages table, replace language_id FK with locale string column on users
- Create config/languages.php for available languages and default locale
- Add Spanish (es) translations (13 files)
- Simplify HandleInertiaRequests, ProfileController, RegisteredUserController

Code quality:
- Add declare(strict_types=1) to all PHP files
- Fix MastodonPublisher using wrong attribute (filename -> original_filename)
- Fix HasMediaTest for new has_photo/photo_url accessors
- Fix PublishToSocialPlatformTest type error revealed by strict_types
- Remove orphaned Language model from AppServiceProvider morph map
- Update User TypeScript interface (has_photo, photo_url, locale)
- Eager load media relation on workspaces to prevent N+1
- Add 8 new tests for workspace logo upload/delete
- Update workspace settings test to assert members/invitations props

All 710 tests passing.
2026-03-30 00:20:43 -03:00