feat: Implement real-time post status updates and configurable social platform enabling/disabling.

This commit is contained in:
Paulo Castellano 2026-01-16 12:36:52 -03:00
parent 7c80d717b1
commit 7867fcce02
32 changed files with 480 additions and 37 deletions

View file

@ -28,6 +28,7 @@ ## Foundational Context
- vue (VUE) - v3
- @laravel/vite-plugin-wayfinder (WAYFINDER) - v0
- eslint (ESLINT) - v9
- laravel-echo (ECHO) - v2
- prettier (PRETTIER) - v3
## Conventions

View file

@ -28,6 +28,7 @@ ## Foundational Context
- vue (VUE) - v3
- @laravel/vite-plugin-wayfinder (WAYFINDER) - v0
- eslint (ESLINT) - v9
- laravel-echo (ECHO) - v2
- prettier (PRETTIER) - v3
## Conventions

View file

@ -0,0 +1,30 @@
<?php
namespace App\Broadcasting;
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Log;
class PostChannel
{
public function join(User $user, Post $post): array|bool
{
Log::info('PostChannel::join called', [
'user_id' => $user->id,
'user_email' => $user->email,
'post_id' => $post->id,
'workspace_id' => $post->workspace_id,
]);
$result = $post->workspace->hasMember($user);
Log::info('PostChannel::join result', [
'result' => $result,
'workspace_owner_id' => $post->workspace->user_id,
'is_owner' => $post->workspace->user_id === $user->id,
]);
return $result;
}
}

View file

@ -91,4 +91,19 @@ public function supportsTextOnly(): bool
self::Threads => true,
};
}
public function isEnabled(): bool
{
return config("trypost.platforms.{$this->value}.enabled", true);
}
/**
* Get all enabled platforms.
*
* @return array<self>
*/
public static function enabled(): array
{
return array_filter(self::cases(), fn (self $platform) => $platform->isEnabled());
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace App\Events;
use App\Models\PostPlatform;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class PostPlatformStatusUpdated implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public PostPlatform $postPlatform) {}
public function broadcastOn(): array
{
return [
new PrivateChannel('posts.'.$this->postPlatform->post_id),
];
}
public function broadcastWith(): array
{
$post = $this->postPlatform->post->fresh();
return [
'post_platform' => [
'id' => $this->postPlatform->id,
'status' => $this->postPlatform->status,
'platform_url' => $this->postPlatform->platform_url,
'error_message' => $this->postPlatform->error_message,
'published_at' => $this->postPlatform->published_at?->toISOString(),
],
'post' => [
'id' => $post->id,
'status' => $post->status->value,
'published_at' => $post->published_at?->toISOString(),
],
];
}
}

View file

@ -22,11 +22,11 @@ class FacebookController extends SocialController
'pages_show_list',
'pages_read_engagement',
'pages_manage_posts',
'pages_read_user_content',
];
public function connect(Request $request, Workspace $workspace): Response
{
$this->ensurePlatformEnabled();
$this->authorize('manageAccounts', $workspace);
if ($workspace->hasConnectedPlatform($this->platform->value)) {

View file

@ -14,7 +14,7 @@
class InstagramController extends SocialController
{
protected string $driver = 'facebook';
protected string $driver = 'instagram';
protected SocialPlatform $platform = SocialPlatform::Instagram;
@ -27,6 +27,7 @@ class InstagramController extends SocialController
public function connect(Request $request, Workspace $workspace): Response
{
$this->ensurePlatformEnabled();
$this->authorize('manageAccounts', $workspace);
if ($workspace->hasConnectedPlatform($this->platform->value)) {

View file

@ -27,6 +27,7 @@ class LinkedInController extends SocialController
public function connect(Request $request, Workspace $workspace): Response
{
$this->ensurePlatformEnabled();
$this->authorize('manageAccounts', $workspace);
if ($workspace->hasConnectedPlatform($this->platform->value)) {

View file

@ -31,6 +31,7 @@ class LinkedInPageController extends SocialController
public function connect(Request $request, Workspace $workspace): SymfonyResponse
{
$this->ensurePlatformEnabled();
$this->authorize('manageAccounts', $workspace);
if ($workspace->hasConnectedPlatform($this->platform->value)) {

View file

@ -16,13 +16,22 @@
class SocialController extends Controller
{
protected SocialPlatform $platform;
protected function ensurePlatformEnabled(): void
{
if (isset($this->platform) && ! $this->platform->isEnabled()) {
abort(403, 'This platform is currently unavailable.');
}
}
public function index(Workspace $workspace): Response
{
$this->authorize('view', $workspace);
$connectedAccounts = $workspace->socialAccounts;
$platforms = collect(SocialPlatform::cases())->map(function ($platform) use ($connectedAccounts) {
$platforms = collect(SocialPlatform::enabled())->map(function ($platform) use ($connectedAccounts) {
$connected = $connectedAccounts->firstWhere('platform', $platform);
return [
@ -32,7 +41,7 @@ public function index(Workspace $workspace): Response
'connected' => $connected !== null,
'account' => $connected,
];
});
})->values();
return Inertia::render('accounts/Index', [
'workspace' => $workspace,

View file

@ -24,6 +24,7 @@ class ThreadsController extends SocialController
public function connect(Request $request, Workspace $workspace): Response
{
$this->ensurePlatformEnabled();
$this->authorize('manageAccounts', $workspace);
if ($workspace->hasConnectedPlatform($this->platform->value)) {

View file

@ -24,6 +24,7 @@ class TikTokController extends SocialController
public function connect(Request $request, Workspace $workspace): Response
{
$this->ensurePlatformEnabled();
$this->authorize('manageAccounts', $workspace);
if ($workspace->hasConnectedPlatform($this->platform->value)) {

View file

@ -24,6 +24,7 @@ class XController extends SocialController
public function connect(Request $request, Workspace $workspace): Response
{
$this->ensurePlatformEnabled();
$this->authorize('manageAccounts', $workspace);
if ($workspace->hasConnectedPlatform($this->platform->value)) {

View file

@ -24,6 +24,7 @@ class YouTubeController extends SocialController
public function connect(Request $request, Workspace $workspace): Response
{
$this->ensurePlatformEnabled();
$this->authorize('manageAccounts', $workspace);
if ($workspace->hasConnectedPlatform($this->platform->value)) {

View file

@ -3,6 +3,7 @@
namespace App\Jobs;
use App\Enums\SocialPlatform;
use App\Events\PostPlatformStatusUpdated;
use App\Models\PostPlatform;
use App\Services\Social\FacebookPublisher;
use App\Services\Social\InstagramPublisher;
@ -29,6 +30,7 @@ public function __construct(public PostPlatform $postPlatform) {}
public function handle(): void
{
$this->postPlatform->markAsPublishing();
$this->broadcastStatus();
try {
$publisher = $this->getPublisher();
@ -47,6 +49,14 @@ public function handle(): void
// Always check and update post status after each platform finishes
$this->updatePostStatus();
// Broadcast final status
$this->broadcastStatus();
}
private function broadcastStatus(): void
{
PostPlatformStatusUpdated::dispatch($this->postPlatform->fresh());
}
private function getPublisher(): LinkedInPublisher|LinkedInPagePublisher|XPublisher|TikTokPublisher|YouTubePublisher|FacebookPublisher|InstagramPublisher|ThreadsPublisher

View file

@ -2,6 +2,7 @@
namespace App\Providers;
use App\Socialite\InstagramExtendSocialite;
use App\Socialite\LinkedInPageExtendSocialite;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Date;
@ -34,6 +35,7 @@ public function boot(): void
protected function configureSocialite(): void
{
Event::listen(SocialiteWasCalled::class, InstagramExtendSocialite::class);
Event::listen(SocialiteWasCalled::class, LinkedInExtendSocialite::class);
Event::listen(SocialiteWasCalled::class, LinkedInPageExtendSocialite::class);
Event::listen(SocialiteWasCalled::class, TikTokExtendSocialite::class);

View file

@ -73,7 +73,7 @@ private function publishTextPost(string $userId, string $accessToken, string $co
private function publishImagePost(string $userId, string $accessToken, string $content, $media): array
{
Log::info('Threads publishing image post', ['user_id' => $userId]);
Log::info('Threads publishing image post', ['user_id' => $userId, 'image_url' => $media->url]);
// Step 1: Create container
$containerResponse = Http::post("{$this->baseUrl}/{$userId}/threads", [
@ -93,7 +93,12 @@ private function publishImagePost(string $userId, string $accessToken, string $c
$containerId = $containerResponse->json()['id'];
// Step 2: Publish
Log::info('Threads image container created', ['container_id' => $containerId]);
// Step 2: Wait for image processing
$this->waitForMediaProcessing($containerId, $accessToken);
// Step 3: Publish
return $this->publishContainer($userId, $accessToken, $containerId);
}
@ -164,10 +169,8 @@ private function publishCarousel(string $userId, string $accessToken, string $co
$childId = $containerResponse->json()['id'];
// Wait for video processing if needed
if ($isVideo) {
$this->waitForMediaProcessing($childId, $accessToken);
}
// Wait for media processing (both images and videos)
$this->waitForMediaProcessing($childId, $accessToken);
$childContainers[] = $childId;
}
@ -234,32 +237,45 @@ private function waitForMediaProcessing(string $containerId, string $accessToken
{
for ($i = 0; $i < $maxAttempts; $i++) {
$statusResponse = Http::get("{$this->baseUrl}/{$containerId}", [
'fields' => 'status',
'fields' => 'status,error_message',
'access_token' => $accessToken,
]);
if ($statusResponse->failed()) {
Log::warning('Threads status check failed', [
'container_id' => $containerId,
'attempt' => $i,
'body' => $statusResponse->body(),
]);
sleep(3);
continue;
}
$status = $statusResponse->json()['status'] ?? 'UNKNOWN';
$data = $statusResponse->json();
$status = $data['status'] ?? 'UNKNOWN';
Log::info('Threads media processing status', ['status' => $status, 'attempt' => $i]);
Log::info('Threads media processing status', [
'container_id' => $containerId,
'status' => $status,
'attempt' => $i,
'response' => $data,
]);
if ($status === 'FINISHED') {
return;
}
if ($status === 'ERROR') {
throw new \Exception('Threads media processing failed');
$errorMessage = $data['error_message'] ?? 'Unknown error';
throw new \Exception('Threads media processing failed: '.$errorMessage);
}
sleep(3);
}
Log::warning('Threads media processing timeout, proceeding anyway');
Log::warning('Threads media processing timeout', ['container_id' => $containerId]);
throw new \Exception('Threads media processing timeout after '.$maxAttempts.' attempts');
}
private function refreshToken(SocialAccount $account): void

View file

@ -0,0 +1,13 @@
<?php
namespace App\Socialite;
use SocialiteProviders\Manager\SocialiteWasCalled;
class InstagramExtendSocialite
{
public function handle(SocialiteWasCalled $socialiteWasCalled): void
{
$socialiteWasCalled->extendSocialite('instagram', \Laravel\Socialite\Two\FacebookProvider::class);
}
}

View file

@ -34,7 +34,8 @@
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"pestphp/pest": "^4.3",
"pestphp/pest-plugin-laravel": "^4.0"
"pestphp/pest-plugin-laravel": "^4.0",
"spatie/laravel-horizon-watcher": "^1.1"
},
"autoload": {
"files": [

191
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c6c2e7e35c2c6d5737296f159edc9a95",
"content-hash": "6993d4c308ced358f1dca8da903c9c22",
"packages": [
{
"name": "aws/aws-crt-php",
@ -12063,6 +12063,195 @@
],
"time": "2025-02-07T05:00:38+00:00"
},
{
"name": "spatie/file-system-watcher",
"version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/file-system-watcher.git",
"reference": "2fe96de88af6660b3ab7658a8f1d41b10ad0aff6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/file-system-watcher/zipball/2fe96de88af6660b3ab7658a8f1d41b10ad0aff6",
"reference": "2fe96de88af6660b3ab7658a8f1d41b10ad0aff6",
"shasum": ""
},
"require": {
"php": "^8.3",
"symfony/process": "^7.0|^8.0"
},
"require-dev": {
"pestphp/pest": "^4.0",
"spatie/ray": "^1.22",
"spatie/temporary-directory": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\Watcher\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"role": "Developer"
}
],
"description": "Watch changes in the file system using PHP",
"homepage": "https://github.com/spatie/file-system-watcher",
"keywords": [
"file-system-watcher",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/file-system-watcher/issues",
"source": "https://github.com/spatie/file-system-watcher/tree/1.2.1"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2025-11-26T10:41:42+00:00"
},
{
"name": "spatie/laravel-horizon-watcher",
"version": "1.1.4",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-horizon-watcher.git",
"reference": "141b94e8eea18f465c4419e8eea54936308a8788"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-horizon-watcher/zipball/141b94e8eea18f465c4419e8eea54936308a8788",
"reference": "141b94e8eea18f465c4419e8eea54936308a8788",
"shasum": ""
},
"require": {
"illuminate/contracts": "^9.0|^10.0|^11.0|^12.0",
"php": "^8.1",
"spatie/file-system-watcher": "^1.1.1",
"spatie/laravel-package-tools": "^1.13.6"
},
"require-dev": {
"laravel/pint": "^1.2",
"nunomaduro/collision": "^6.3.1|^8.0",
"orchestra/testbench": "^7.11|^8.0|^9.0|^10.0",
"pestphp/pest": "^1.22.1|^2.34|^3.7",
"pestphp/pest-plugin-laravel": "^1.3|^2.3|^3.1",
"phpunit/phpunit": "^9.5.26|^10.5|^11.5.3",
"spatie/laravel-ray": "^1.31"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"HorizonWatcher": "Spatie\\HorizonWatcher\\Facades\\HorizonWatcher"
},
"providers": [
"Spatie\\HorizonWatcher\\HorizonWatcherServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Spatie\\HorizonWatcher\\": "src",
"Spatie\\HorizonWatcher\\Database\\Factories\\": "database/factories"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"role": "Developer"
}
],
"description": "Automatically restart Horizon when local PHP files change",
"homepage": "https://github.com/spatie/laravel-horizon-watcher",
"keywords": [
"laravel",
"laravel-horizon-watcher",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/laravel-horizon-watcher/issues",
"source": "https://github.com/spatie/laravel-horizon-watcher/tree/1.1.4"
},
"time": "2025-07-17T15:43:23+00:00"
},
{
"name": "spatie/laravel-package-tools",
"version": "1.92.7",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-package-tools.git",
"reference": "f09a799850b1ed765103a4f0b4355006360c49a5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/f09a799850b1ed765103a4f0b4355006360c49a5",
"reference": "f09a799850b1ed765103a4f0b4355006360c49a5",
"shasum": ""
},
"require": {
"illuminate/contracts": "^9.28|^10.0|^11.0|^12.0",
"php": "^8.0"
},
"require-dev": {
"mockery/mockery": "^1.5",
"orchestra/testbench": "^7.7|^8.0|^9.0|^10.0",
"pestphp/pest": "^1.23|^2.1|^3.1",
"phpunit/php-code-coverage": "^9.0|^10.0|^11.0",
"phpunit/phpunit": "^9.5.24|^10.5|^11.5",
"spatie/pest-plugin-test-time": "^1.1|^2.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\LaravelPackageTools\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"role": "Developer"
}
],
"description": "Tools for creating Laravel packages",
"homepage": "https://github.com/spatie/laravel-package-tools",
"keywords": [
"laravel-package-tools",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/laravel-package-tools/issues",
"source": "https://github.com/spatie/laravel-package-tools/tree/1.92.7"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2025-07-17T15:46:43+00:00"
},
{
"name": "staabm/side-effects-detector",
"version": "1.0.5",

View file

@ -67,13 +67,20 @@
'redirect' => env('GOOGLE_CLIENT_REDIRECT'),
],
// Facebook (used for Facebook Pages and Instagram)
// Facebook Pages
'facebook' => [
'client_id' => env('FACEBOOK_CLIENT_ID'),
'client_secret' => env('FACEBOOK_CLIENT_SECRET'),
'redirect' => env('FACEBOOK_CLIENT_REDIRECT'),
],
// Instagram
'instagram' => [
'client_id' => env('INSTAGRAM_CLIENT_ID'),
'client_secret' => env('INSTAGRAM_CLIENT_SECRET'),
'redirect' => env('INSTAGRAM_CLIENT_REDIRECT'),
],
// Threads
'threads' => [
'client_id' => env('THREADS_CLIENT_ID'),

43
config/trypost.php Normal file
View file

@ -0,0 +1,43 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Social Platforms
|--------------------------------------------------------------------------
|
| Configure which social platforms are enabled in the application.
| Set to false to temporarily disable a platform (e.g., when credentials
| are revoked, expired, or pending approval).
|
*/
'platforms' => [
'linkedin' => [
'enabled' => env('TRYPOST_LINKEDIN_ENABLED', true),
],
'linkedin-page' => [
'enabled' => env('TRYPOST_LINKEDIN_PAGE_ENABLED', true),
],
'x' => [
'enabled' => env('TRYPOST_X_ENABLED', true),
],
'tiktok' => [
'enabled' => env('TRYPOST_TIKTOK_ENABLED', true),
],
'youtube' => [
'enabled' => env('TRYPOST_YOUTUBE_ENABLED', true),
],
'facebook' => [
'enabled' => env('TRYPOST_FACEBOOK_ENABLED', true),
],
'instagram' => [
'enabled' => env('TRYPOST_INSTAGRAM_ENABLED', true),
],
'threads' => [
'enabled' => env('TRYPOST_THREADS_ENABLED', true),
],
],
];

31
package-lock.json generated
View file

@ -29,6 +29,7 @@
"@types/node": "^22.13.5",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/eslint-config-typescript": "^14.3.0",
"chokidar": "^5.0.0",
"concurrently": "^9.0.1",
"eslint": "^9.17.0",
"eslint-config-prettier": "^10.0.1",
@ -2889,6 +2890,22 @@
"node": ">=8"
}
},
"node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@ -6005,6 +6022,20 @@
],
"license": "MIT"
},
"node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",

View file

@ -18,6 +18,7 @@
"@types/node": "^22.13.5",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/eslint-config-typescript": "^14.3.0",
"chokidar": "^5.0.0",
"concurrently": "^9.0.1",
"eslint": "^9.17.0",
"eslint-config-prettier": "^10.0.1",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,3 +1,13 @@
<svg width="166" height="166" viewBox="0 0 166 166" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M162.041 38.7592C162.099 38.9767 162.129 39.201 162.13 39.4264V74.4524C162.13 74.9019 162.011 75.3435 161.786 75.7325C161.561 76.1216 161.237 76.4442 160.847 76.6678L131.462 93.5935V127.141C131.462 128.054 130.977 128.897 130.186 129.357L68.8474 164.683C68.707 164.763 68.5538 164.814 68.4007 164.868C68.3432 164.887 68.289 164.922 68.2284 164.938C67.7996 165.051 67.3489 165.051 66.9201 164.938C66.8499 164.919 66.7861 164.881 66.7191 164.855C66.5787 164.804 66.4319 164.76 66.2979 164.683L4.97219 129.357C4.58261 129.133 4.2589 128.81 4.0337 128.421C3.8085 128.032 3.68976 127.591 3.68945 127.141L3.68945 22.0634C3.68945 21.8336 3.72136 21.6101 3.7788 21.393C3.79794 21.3196 3.84262 21.2526 3.86814 21.1791C3.91601 21.0451 3.96068 20.9078 4.03088 20.7833C4.07874 20.7003 4.14894 20.6333 4.20638 20.5566C4.27977 20.4545 4.34678 20.3491 4.43293 20.2598C4.50632 20.1863 4.60205 20.1321 4.68501 20.0682C4.77755 19.9916 4.86051 19.9086 4.96581 19.848L35.6334 2.18492C36.0217 1.96139 36.4618 1.84375 36.9098 1.84375C37.3578 1.84375 37.7979 1.96139 38.1862 2.18492L68.8506 19.848H68.857C68.9591 19.9118 69.0452 19.9916 69.1378 20.065C69.2207 20.1289 69.3133 20.1863 69.3867 20.2566C69.476 20.3491 69.5398 20.4545 69.6164 20.5566C69.6707 20.6333 69.7441 20.7003 69.7887 20.7833C69.8621 20.911 69.9036 21.0451 69.9546 21.1791C69.9802 21.2526 70.0248 21.3196 70.044 21.3962C70.1027 21.6138 70.1328 21.8381 70.1333 22.0634V87.6941L95.686 72.9743V39.4232C95.686 39.1997 95.7179 38.9731 95.7753 38.7592C95.7977 38.6826 95.8391 38.6155 95.8647 38.5421C95.9157 38.408 95.9604 38.2708 96.0306 38.1463C96.0785 38.0633 96.1487 37.9962 96.2029 37.9196C96.2795 37.8175 96.3433 37.7121 96.4326 37.6227C96.506 37.5493 96.5986 37.495 96.6815 37.4312C96.7773 37.3546 96.8602 37.2716 96.9623 37.2109L127.633 19.5479C128.021 19.324 128.461 19.2062 128.91 19.2062C129.358 19.2062 129.798 19.324 130.186 19.5479L160.85 37.2109C160.959 37.2748 161.042 37.3546 161.137 37.428C161.217 37.4918 161.31 37.5493 161.383 37.6195C161.473 37.7121 161.536 37.8175 161.613 37.9196C161.67 37.9962 161.741 38.0633 161.785 38.1463C161.859 38.2708 161.9 38.408 161.951 38.5421C161.98 38.6155 162.021 38.6826 162.041 38.7592ZM157.018 72.9743V43.8477L146.287 50.028L131.462 58.5675V87.6941L157.021 72.9743H157.018ZM126.354 125.663V96.5176L111.771 104.85L70.1301 128.626V158.046L126.354 125.663ZM8.80126 26.4848V125.663L65.0183 158.043V128.629L35.6494 112L35.6398 111.994L35.6271 111.988C35.5281 111.93 35.4452 111.847 35.3526 111.777C35.2729 111.713 35.1803 111.662 35.1101 111.592L35.1038 111.582C35.0208 111.502 34.9634 111.403 34.8932 111.314C34.8293 111.228 34.7528 111.154 34.7017 111.065L34.6985 111.055C34.6411 110.96 34.606 110.845 34.5645 110.736C34.523 110.64 34.4688 110.551 34.4432 110.449C34.4113 110.328 34.4049 110.197 34.3922 110.072C34.3794 109.976 34.3539 109.881 34.3539 109.785V109.778V41.2045L19.5322 32.6619L8.80126 26.4848ZM36.913 7.35007L11.3635 22.0634L36.9066 36.7768L62.4529 22.0602L36.9066 7.35007H36.913ZM50.1999 99.1736L65.0215 90.6374V26.4848L54.2906 32.6651L39.4657 41.2045V105.357L50.1999 99.1736ZM128.91 24.713L103.363 39.4264L128.91 54.1397L154.453 39.4232L128.91 24.713ZM126.354 58.5675L111.529 50.028L100.798 43.8477V72.9743L115.619 81.5106L126.354 87.6941V58.5675ZM67.5711 124.205L105.042 102.803L123.772 92.109L98.2451 77.4053L68.8538 94.3341L42.0663 109.762L67.5711 124.205Z" fill="#FF2D20"/>
<svg width="175" height="164" viewBox="0 0 175 164" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2011_294)">
<path d="M174.58 43.75V163.46H54.8701L174.58 43.75Z" fill="#3660E0"/>
<path d="M174.58 0H119.71L87.29 32.42L174.58 119.71V0Z" fill="#855ED4"/>
<path d="M0 0V119.71L87.29 32.42L119.71 0H0Z" fill="#FFCC33"/>
<path d="M0 43.75V163.46H54.87L87.29 131.04L0 43.75Z" fill="#F39200"/>
</g>
<defs>
<clipPath id="clip0_2011_294">
<rect width="174.58" height="163.46" rx="28" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 544 B

View file

@ -1,5 +1,4 @@
import '../css/app.css';
import './echo';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';

View file

@ -1,14 +0,0 @@
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});

View file

@ -1,6 +1,8 @@
<script setup lang="ts">
import { Head, Link } from '@inertiajs/vue3';
import { useEcho } from '@laravel/echo-vue';
import { Clock, CheckCircle, AlertCircle, Loader2, FileText, ExternalLink, Pencil } from 'lucide-vue-next';
import { ref, computed } from 'vue';
import dayjs from '@/dayjs';
import AppLayout from '@/layouts/AppLayout.vue';
@ -65,6 +67,29 @@ interface Props {
const props = defineProps<Props>();
// Reactive state for real-time updates
const post = ref(props.post);
// Listen for status updates via WebSocket
useEcho(
`posts.${props.post.id}`,
'PostPlatformStatusUpdated',
(e: { post_platform: { id: string; status: string; platform_url: string | null; error_message: string | null; published_at: string | null }; post: { id: string; status: string; published_at: string | null } }) => {
// Update post platform status
const platformIndex = post.value.post_platforms.findIndex(pp => pp.id === e.post_platform.id);
if (platformIndex !== -1) {
post.value.post_platforms[platformIndex].status = e.post_platform.status;
post.value.post_platforms[platformIndex].platform_url = e.post_platform.platform_url;
post.value.post_platforms[platformIndex].error_message = e.post_platform.error_message;
post.value.post_platforms[platformIndex].published_at = e.post_platform.published_at;
}
// Update post status
post.value.status = e.post.status;
post.value.published_at = e.post.published_at;
},
);
const breadcrumbs: BreadcrumbItemType[] = [
{ title: 'Workspaces', href: '/workspaces' },
{ title: props.workspace.name, href: `/workspaces/${props.workspace.id}` },
@ -117,8 +142,8 @@ const formatDateTime = (date: string | null): string => {
return dayjs.utc(date).tz(props.workspace.timezone).format('MMM D, YYYY [at] h:mm A');
};
const canEdit = ['draft', 'scheduled'].includes(props.post.status);
const enabledPlatforms = props.post.post_platforms.filter(pp => pp.enabled);
const canEdit = computed(() => ['draft', 'scheduled'].includes(post.value.status));
const enabledPlatforms = computed(() => post.value.post_platforms.filter(pp => pp.enabled));
</script>
<template>

View file

@ -1,7 +1,10 @@
<?php
use App\Broadcasting\PostChannel;
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});
Broadcast::channel('posts.{post}', PostChannel::class);