From ffb4368202364d731310b2c0de45f6a9bdc51f3c Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Mon, 17 Aug 2026 20:50:24 -0300 Subject: [PATCH 01/76] Replace welcome steps with a chat and pitch reach after connect The four welcome pages become one scripted chat. After a network connects, the latest post is deferred from the platform API and a reach pitch is shown before Stripe. POSTs, checkout, and the connect-required gate stay the same. Co-authored-by: Cursor --- app/Actions/Welcome/FetchLatestSocialPost.php | 572 ++++++++++++++ app/Enums/SocialAccount/Platform.php | 17 + .../Controllers/App/WelcomeController.php | 79 +- lang/ar/welcome.php | 11 +- lang/de/welcome.php | 11 +- lang/el/welcome.php | 11 +- lang/en/welcome.php | 11 +- lang/es/welcome.php | 11 +- lang/fr/welcome.php | 11 +- lang/it/welcome.php | 11 +- lang/ja/welcome.php | 11 +- lang/ko/welcome.php | 11 +- lang/nl/welcome.php | 11 +- lang/pl/welcome.php | 11 +- lang/pt-BR/welcome.php | 11 +- lang/ru/welcome.php | 11 +- lang/tr/welcome.php | 11 +- lang/uk/welcome.php | 11 +- lang/zh/welcome.php | 11 +- .../accounts/NetworkConnectGrid.vue | 49 +- resources/js/components/welcome/GoalChips.vue | 148 ++++ .../js/components/welcome/PersonaChips.vue | 124 +++ .../js/components/welcome/PlatformChips.vue | 58 ++ .../js/components/welcome/ReferralChips.vue | 182 +++++ resources/js/layouts/WelcomeLayout.vue | 46 +- resources/js/pages/welcome/Chat.vue | 723 ++++++++++++++++++ resources/js/pages/welcome/Connect.vue | 73 -- resources/js/pages/welcome/Goals.vue | 194 ----- resources/js/pages/welcome/Persona.vue | 169 ---- resources/js/pages/welcome/ReferralSource.vue | 232 ------ tests/Browser/WelcomeConnectTest.php | 151 +++- .../Welcome/FetchLatestSocialPostTest.php | 516 +++++++++++++ .../Feature/Welcome/WelcomeControllerTest.php | 157 +++- tests/Unit/Enums/PlatformTest.php | 23 + 34 files changed, 2958 insertions(+), 731 deletions(-) create mode 100644 app/Actions/Welcome/FetchLatestSocialPost.php create mode 100644 resources/js/components/welcome/GoalChips.vue create mode 100644 resources/js/components/welcome/PersonaChips.vue create mode 100644 resources/js/components/welcome/PlatformChips.vue create mode 100644 resources/js/components/welcome/ReferralChips.vue create mode 100644 resources/js/pages/welcome/Chat.vue delete mode 100644 resources/js/pages/welcome/Connect.vue delete mode 100644 resources/js/pages/welcome/Goals.vue delete mode 100644 resources/js/pages/welcome/Persona.vue delete mode 100644 resources/js/pages/welcome/ReferralSource.vue create mode 100644 tests/Feature/Welcome/FetchLatestSocialPostTest.php diff --git a/app/Actions/Welcome/FetchLatestSocialPost.php b/app/Actions/Welcome/FetchLatestSocialPost.php new file mode 100644 index 000000000..8d94aef07 --- /dev/null +++ b/app/Actions/Welcome/FetchLatestSocialPost.php @@ -0,0 +1,572 @@ +, each_views: int, extra_views: int}}|null + */ + public function handle(SocialAccount $account): ?array + { + if ($account->status !== Status::Connected) { + return null; + } + + if (! $account->platform->supportsImpressionAnalytics()) { + return null; + } + + try { + $post = match ($account->platform) { + Platform::Instagram, Platform::InstagramFacebook => $this->instagram($account), + Platform::Facebook => $this->facebook($account), + Platform::X => $this->x($account), + Platform::Threads => $this->threads($account), + Platform::TikTok => $this->tiktok($account), + Platform::YouTube => $this->youtube($account), + Platform::Pinterest => $this->pinterest($account), + Platform::LinkedInPage => $this->linkedinPage($account), + default => null, + }; + + return $post === null ? null : $this->withReach($account, $post); + } catch (Throwable $e) { + report($e); + + return null; + } + } + + /** + * @return array{id: string, caption: string|null, media_url: string|null, permalink: string|null, published_at: string|null, impressions: int|null}|null + */ + private function instagram(SocialAccount $account): ?array + { + $base = $account->platform->instagramGraphBaseUrl(); + $response = $this->http() + ->get("{$base}/{$account->platform_user_id}/media", [ + 'fields' => 'id,caption,media_type,media_url,thumbnail_url,permalink,timestamp', + 'limit' => 1, + 'access_token' => $account->access_token, + ]); + + if ($response->failed()) { + $this->logFailure('Instagram', $response->body()); + + return null; + } + + $item = data_get($response->json(), 'data.0'); + + if (! is_array($item)) { + return null; + } + + $id = (string) data_get($item, 'id'); + $mediaUrl = data_get($item, 'media_type') === 'VIDEO' + ? data_get($item, 'thumbnail_url') + : data_get($item, 'media_url'); + + return $this->post( + $id, + data_get($item, 'caption'), + is_string($mediaUrl) ? $mediaUrl : null, + data_get($item, 'permalink'), + data_get($item, 'timestamp'), + $this->graphInsights($account->platform->instagramGraphBaseUrl(), $id, $account->access_token, ['views', 'reach']), + ); + } + + /** + * @return array{id: string, caption: string|null, media_url: string|null, permalink: string|null, published_at: string|null, impressions: int|null}|null + */ + private function facebook(SocialAccount $account): ?array + { + $base = (string) config('trypost.platforms.facebook.graph_api'); + $response = $this->http() + ->get("{$base}/{$account->platform_user_id}/posts", [ + 'fields' => 'id,message,full_picture,permalink_url,created_time', + 'limit' => 1, + 'access_token' => $account->access_token, + ]); + + if ($response->failed()) { + $this->logFailure('Facebook', $response->body()); + + return null; + } + + $item = data_get($response->json(), 'data.0'); + + if (! is_array($item)) { + return null; + } + + $id = (string) data_get($item, 'id'); + + return $this->post( + $id, + data_get($item, 'message'), + data_get($item, 'full_picture'), + data_get($item, 'permalink_url'), + data_get($item, 'created_time'), + $this->graphInsights((string) config('trypost.platforms.facebook.graph_api'), $id, $account->access_token, ['post_impressions']), + ); + } + + /** + * @return array{id: string, caption: string|null, media_url: string|null, permalink: string|null, published_at: string|null, impressions: int|null}|null + */ + private function x(SocialAccount $account): ?array + { + $base = (string) config('trypost.platforms.x.api'); + $response = $this->http() + ->withToken($account->access_token) + ->get("{$base}/users/{$account->platform_user_id}/tweets", [ + 'max_results' => 5, + 'tweet.fields' => 'created_at,text,attachments,public_metrics', + 'expansions' => 'attachments.media_keys', + 'media.fields' => 'url,preview_image_url', + ]); + + if ($response->failed()) { + $this->logFailure('X', $response->body()); + + return null; + } + + $item = data_get($response->json(), 'data.0'); + + if (! is_array($item)) { + return null; + } + + $mediaKey = data_get($item, 'attachments.media_keys.0'); + $mediaUrl = null; + + if (is_string($mediaKey)) { + $media = collect(data_get($response->json(), 'includes.media', [])) + ->first(fn (mixed $row): bool => is_array($row) && data_get($row, 'media_key') === $mediaKey); + + $mediaUrl = is_array($media) + ? (data_get($media, 'preview_image_url') ?? data_get($media, 'url')) + : null; + } + + $id = (string) data_get($item, 'id'); + + $impressions = data_get($item, 'public_metrics.impression_count'); + + return $this->post( + $id, + data_get($item, 'text'), + is_string($mediaUrl) ? $mediaUrl : null, + "https://x.com/i/web/status/{$id}", + data_get($item, 'created_at'), + is_numeric($impressions) ? (int) $impressions : null, + ); + } + + /** + * @return array{id: string, caption: string|null, media_url: string|null, permalink: string|null, published_at: string|null, impressions: int|null}|null + */ + private function threads(SocialAccount $account): ?array + { + $base = (string) config('trypost.platforms.threads.graph_api'); + $response = $this->http() + ->get("{$base}/{$account->platform_user_id}/threads", [ + 'fields' => 'id,text,media_type,permalink,timestamp,media_url,thumbnail_url', + 'limit' => 1, + 'access_token' => $account->access_token, + ]); + + if ($response->failed()) { + $this->logFailure('Threads', $response->body()); + + return null; + } + + $item = data_get($response->json(), 'data.0'); + + if (! is_array($item)) { + return null; + } + + $id = (string) data_get($item, 'id'); + $mediaUrl = data_get($item, 'thumbnail_url') ?? data_get($item, 'media_url'); + + return $this->post( + $id, + data_get($item, 'text'), + is_string($mediaUrl) ? $mediaUrl : null, + data_get($item, 'permalink'), + data_get($item, 'timestamp'), + $this->graphInsights((string) config('trypost.platforms.threads.graph_api'), $id, $account->access_token, ['views']), + ); + } + + /** + * @return array{id: string, caption: string|null, media_url: string|null, permalink: string|null, published_at: string|null, impressions: int|null}|null + */ + private function tiktok(SocialAccount $account): ?array + { + $base = (string) config('trypost.platforms.tiktok.api'); + $response = $this->http() + ->asJson() + ->withToken($account->access_token) + ->post("{$base}/video/list/?fields=id,title,cover_image_url,share_url,create_time", [ + 'max_count' => 1, + ]); + + if ($response->failed()) { + $this->logFailure('TikTok', $response->body()); + + return null; + } + + $item = data_get($response->json(), 'data.videos.0'); + + if (! is_array($item)) { + return null; + } + + $id = (string) data_get($item, 'id'); + $created = data_get($item, 'create_time'); + + return $this->post( + $id, + data_get($item, 'title'), + data_get($item, 'cover_image_url'), + data_get($item, 'share_url'), + is_numeric($created) ? now()->setTimestamp((int) $created)->toIso8601String() : null, + $this->tiktokViews($account, $id), + ); + } + + /** + * @return array{id: string, caption: string|null, media_url: string|null, permalink: string|null, published_at: string|null, impressions: int|null}|null + */ + private function youtube(SocialAccount $account): ?array + { + $base = (string) config('trypost.platforms.youtube.data_api'); + $response = $this->http() + ->withToken($account->access_token) + ->get("{$base}/search", [ + 'part' => 'snippet', + 'forMine' => 'true', + 'type' => 'video', + 'order' => 'date', + 'maxResults' => 1, + ]); + + if ($response->failed()) { + $this->logFailure('YouTube', $response->body()); + + return null; + } + + $item = data_get($response->json(), 'items.0'); + + if (! is_array($item)) { + return null; + } + + $videoId = (string) data_get($item, 'id.videoId'); + + if ($videoId === '') { + return null; + } + + return $this->post( + $videoId, + data_get($item, 'snippet.title'), + data_get($item, 'snippet.thumbnails.high.url') ?? data_get($item, 'snippet.thumbnails.default.url'), + "https://www.youtube.com/watch?v={$videoId}", + data_get($item, 'snippet.publishedAt'), + $this->youtubeViews($account, $videoId), + ); + } + + /** + * @return array{id: string, caption: string|null, media_url: string|null, permalink: string|null, published_at: string|null, impressions: int|null}|null + */ + private function pinterest(SocialAccount $account): ?array + { + $base = (string) config('trypost.platforms.pinterest.api'); + $response = $this->http() + ->withToken($account->access_token) + ->get("{$base}/pins", [ + 'page_size' => 1, + ]); + + if ($response->failed()) { + $this->logFailure('Pinterest', $response->body()); + + return null; + } + + $item = data_get($response->json(), 'items.0'); + + if (! is_array($item)) { + return null; + } + + $id = (string) data_get($item, 'id'); + + return $this->post( + $id, + data_get($item, 'description') ?? data_get($item, 'title'), + data_get($item, 'media.images.400x300.url') ?? data_get($item, 'media.images.150x150.url'), + null, + data_get($item, 'created_at'), + $this->pinterestImpressions($account, $id), + ); + } + + /** + * @return array{id: string, caption: string|null, media_url: string|null, permalink: string|null, published_at: string|null, impressions: int|null}|null + */ + private function linkedinPage(SocialAccount $account): ?array + { + $base = (string) config('trypost.platforms.linkedin-page.api').'/rest'; + $author = rawurlencode('urn:li:organization:'.$account->platform_user_id); + $response = $this->http() + ->withToken($account->access_token) + ->withHeaders([ + 'Linkedin-Version' => '202601', + 'X-Restli-Protocol-Version' => '2.0.0', + ]) + ->get("{$base}/posts?q=author&author={$author}&count=1&sortBy=LAST_MODIFIED"); + + if ($response->failed()) { + $this->logFailure('LinkedIn Page', $response->body()); + + return null; + } + + $item = data_get($response->json(), 'elements.0'); + + if (! is_array($item)) { + return null; + } + + $id = (string) data_get($item, 'id'); + $createdAt = data_get($item, 'createdAt'); + + return $this->post( + $id, + data_get($item, 'commentary'), + null, + $id !== '' ? "https://www.linkedin.com/feed/update/{$id}" : null, + is_numeric($createdAt) + ? now()->setTimestamp((int) ((int) $createdAt / 1000))->toIso8601String() + : null, + ); + } + + private function http(): PendingRequest + { + return Http::timeout(8)->connectTimeout(3); + } + + /** + * @return array{id: string, caption: string|null, media_url: string|null, permalink: string|null, published_at: string|null, impressions: int|null}|null + */ + private function post( + string $id, + mixed $caption, + mixed $mediaUrl, + mixed $permalink, + mixed $publishedAt, + ?int $impressions = null, + ): ?array { + if ($id === '') { + return null; + } + + return [ + 'id' => $id, + 'caption' => is_string($caption) && $caption !== '' ? $caption : null, + 'media_url' => is_string($mediaUrl) && $mediaUrl !== '' ? $mediaUrl : null, + 'permalink' => is_string($permalink) && $permalink !== '' ? $permalink : null, + 'published_at' => is_string($publishedAt) && $publishedAt !== '' ? $publishedAt : null, + 'impressions' => $impressions, + ]; + } + + /** + * @param array{id: string, caption: string|null, media_url: string|null, permalink: string|null, published_at: string|null, impressions: int|null} $post + * @return array{id: string, caption: string|null, media_url: string|null, permalink: string|null, published_at: string|null, impressions: int|null, reach: array{network: string, network_value: string, others: list, each_views: int, extra_views: int}} + */ + private function withReach(SocialAccount $account, array $post): array + { + $eachViews = max(self::MISSED_VIEWS_PER_NETWORK, $post['impressions'] ?? 0); + $others = $this->missedNetworks($account->platform, $eachViews); + + return [ + ...$post, + 'reach' => [ + 'network' => $this->networkLabel($account->platform), + 'network_value' => $account->platform->network(), + 'others' => $others, + 'each_views' => $eachViews, + 'extra_views' => $eachViews * count($others), + ], + ]; + } + + /** + * @return list + */ + private function missedNetworks(Platform $platform, int $eachViews): array + { + $preferred = [ + Platform::TikTok, + Platform::YouTube, + Platform::Instagram, + Platform::Facebook, + Platform::X, + ]; + + return collect($preferred) + ->filter(fn (Platform $candidate): bool => $candidate->isEnabled()) + ->reject(fn (Platform $candidate): bool => $candidate->network() === $platform->network()) + ->take(2) + ->map(fn (Platform $candidate): array => [ + 'value' => $candidate->value, + 'label' => $this->networkLabel($candidate), + 'views' => $eachViews, + ]) + ->values() + ->all(); + } + + private function networkLabel(Platform $platform): string + { + return match ($platform) { + Platform::InstagramFacebook => Platform::Instagram->label(), + Platform::YouTube => 'YouTube', + Platform::Facebook => 'Facebook', + default => $platform->label(), + }; + } + + /** + * @param list $metrics + */ + private function graphInsights(string $base, string $id, ?string $token, array $metrics): ?int + { + if ($id === '' || $token === null || $token === '') { + return null; + } + + foreach ($metrics as $metric) { + $response = $this->http()->get("{$base}/{$id}/insights", [ + 'metric' => $metric, + 'access_token' => $token, + ]); + + if ($response->failed()) { + continue; + } + + $value = data_get($response->json(), 'data.0.values.0.value'); + + if (is_numeric($value)) { + return (int) $value; + } + } + + return null; + } + + private function tiktokViews(SocialAccount $account, string $videoId): ?int + { + if ($videoId === '') { + return null; + } + + $base = (string) config('trypost.platforms.tiktok.api'); + $response = $this->http() + ->asJson() + ->withToken($account->access_token) + ->post("{$base}/video/query/?fields=id,view_count", [ + 'filters' => ['video_ids' => [$videoId]], + ]); + + if ($response->failed()) { + return null; + } + + $views = data_get($response->json(), 'data.videos.0.view_count'); + + return is_numeric($views) ? (int) $views : null; + } + + private function youtubeViews(SocialAccount $account, string $videoId): ?int + { + $base = (string) config('trypost.platforms.youtube.data_api'); + $response = $this->http() + ->withToken($account->access_token) + ->get("{$base}/videos", [ + 'part' => 'statistics', + 'id' => $videoId, + ]); + + if ($response->failed()) { + return null; + } + + $views = data_get($response->json(), 'items.0.statistics.viewCount'); + + return is_numeric($views) ? (int) $views : null; + } + + private function pinterestImpressions(SocialAccount $account, string $pinId): ?int + { + if ($pinId === '') { + return null; + } + + $base = (string) config('trypost.platforms.pinterest.api'); + $response = $this->http() + ->withToken($account->access_token) + ->get("{$base}/pins/{$pinId}/analytics", [ + 'start_date' => now()->subDays(90)->format('Y-m-d'), + 'end_date' => now()->format('Y-m-d'), + 'metric_types' => 'IMPRESSION', + ]); + + if ($response->failed()) { + return null; + } + + $impressions = data_get($response->json(), 'all.summary_metrics.IMPRESSION'); + + return is_numeric($impressions) ? (int) $impressions : null; + } + + private function logFailure(string $platform, string $body): void + { + Log::warning("{$platform} latest post fetch failed", [ + 'body' => mb_substr((string) TokenRedactor::redact($body), 0, 500), + ]); + } +} diff --git a/app/Enums/SocialAccount/Platform.php b/app/Enums/SocialAccount/Platform.php index e665ed453..41c37072a 100644 --- a/app/Enums/SocialAccount/Platform.php +++ b/app/Enums/SocialAccount/Platform.php @@ -268,6 +268,23 @@ public function requiredPublishScopes(): array }; } + /** + * Whether the network exposes post-level impressions, reach, or views. + * Welcome uses this to decide whether to fetch and show the latest post. + * Telegram has subscriber analytics only; LinkedIn personal, Bluesky, + * Mastodon, and Discord do not expose impression metrics. + */ + public function supportsImpressionAnalytics(): bool + { + return match ($this) { + self::Instagram, self::InstagramFacebook, self::Facebook, + self::X, self::Threads, self::TikTok, self::YouTube, + self::Pinterest, self::LinkedInPage => true, + self::LinkedIn, self::Bluesky, self::Mastodon, + self::Telegram, self::Discord => false, + }; + } + public function supportsTextOnly(): bool { return match ($this) { diff --git a/app/Http/Controllers/App/WelcomeController.php b/app/Http/Controllers/App/WelcomeController.php index d4267c7a8..9f5fee99f 100644 --- a/app/Http/Controllers/App/WelcomeController.php +++ b/app/Http/Controllers/App/WelcomeController.php @@ -5,10 +5,12 @@ namespace App\Http\Controllers\App; use App\Actions\Billing\StartSubscriptionCheckout; +use App\Actions\Welcome\FetchLatestSocialPost; use App\Enums\Plan\Slug; use App\Enums\PostHog\CheckoutEvent; use App\Enums\PostHog\WelcomeEvent; use App\Enums\SocialAccount\Platform as SocialPlatform; +use App\Enums\SocialAccount\Status; use App\Enums\User\Goal; use App\Enums\User\Persona; use App\Enums\User\ReferralSource; @@ -18,6 +20,8 @@ use App\Http\Requests\App\Welcome\StoreWelcomeReferralSourceRequest; use App\Http\Resources\App\SocialAccountResource; use App\Models\Plan; +use App\Models\SocialAccount; +use App\Models\User; use App\Services\PostHogService; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -34,9 +38,13 @@ public function persona(Request $request): InertiaResponse|RedirectResponse return $redirect; } - return Inertia::render('welcome/Persona', [ + $user = $request->user(); + + return Inertia::render('welcome/Chat', [ + 'step' => 'persona', + 'history' => $this->chatHistory($user, 'persona'), 'personas' => array_map(fn (Persona $persona): string => $persona->value, Persona::cases()), - 'selected' => $request->user()->persona?->value, + 'selectedPersona' => $user->persona?->value, ]); } @@ -72,9 +80,11 @@ public function goals(Request $request): InertiaResponse|RedirectResponse $user = $request->user(); - return Inertia::render('welcome/Goals', [ + return Inertia::render('welcome/Chat', [ + 'step' => 'goals', + 'history' => $this->chatHistory($user, 'goals'), 'goals' => array_map(fn (Goal $goal): string => $goal->value, Goal::cases()), - 'selected' => $user->goals ?? [], + 'selectedGoals' => $user->goals ?? [], ]); } @@ -110,9 +120,11 @@ public function referralSource(Request $request): InertiaResponse|RedirectRespon $user = $request->user(); - return Inertia::render('welcome/ReferralSource', [ + return Inertia::render('welcome/Chat', [ + 'step' => 'referral', + 'history' => $this->chatHistory($user, 'referral'), 'sources' => array_map(fn (ReferralSource $source): string => $source->value, ReferralSource::cases()), - 'selected' => $user->referral_source?->value, + 'selectedReferral' => $user->referral_source?->value, ]); } @@ -142,7 +154,7 @@ public function storeReferralSource( return redirect()->route('app.welcome.connect'); } - public function connect(Request $request): InertiaResponse|RedirectResponse + public function connect(Request $request, FetchLatestSocialPost $fetchLatest): InertiaResponse|RedirectResponse { if ($redirect = $this->redirectIfStepIncomplete($request, requireGoals: true, requireReferral: true)) { return $redirect; @@ -152,11 +164,20 @@ public function connect(Request $request): InertiaResponse|RedirectResponse abort_unless($workspace !== null, Response::HTTP_NOT_FOUND); - return Inertia::render('welcome/Connect', [ + $accounts = $workspace->socialAccounts()->orderBy('id')->get(); + $connected = $accounts->first( + fn (SocialAccount $account): bool => $account->status === Status::Connected + && $account->platform->supportsImpressionAnalytics(), + ); + + return Inertia::render('welcome/Chat', [ + 'step' => 'connect', + 'history' => $this->chatHistory($request->user(), 'connect'), 'platforms' => SocialPlatform::connectableOptions(), - 'accounts' => SocialAccountResource::collection( - $workspace->socialAccounts()->orderBy('id')->get(), - )->resolve(), + 'accounts' => SocialAccountResource::collection($accounts)->resolve(), + 'latestPost' => $connected !== null + ? Inertia::defer(fn (): ?array => $fetchLatest->handle($connected)) + : null, ]); } @@ -248,6 +269,42 @@ private function redirectIfStepIncomplete( return null; } + /** + * Answered welcome turns before the current step, reconstructed from + * stored user fields so a reload still looks like a chat thread. + * + * @return list}> + */ + private function chatHistory(User $user, string $currentStep): array + { + $history = []; + + if ($currentStep !== 'persona' && $user->persona) { + $history[] = [ + 'step' => 'persona', + 'values' => [$user->persona->value], + ]; + } + + if (in_array($currentStep, ['referral', 'connect'], true) && Goal::containsCurrent($user->goals)) { + $allowed = array_map(fn (Goal $goal): string => $goal->value, Goal::cases()); + + $history[] = [ + 'step' => 'goals', + 'values' => array_values(array_intersect($user->goals ?? [], $allowed)), + ]; + } + + if ($currentStep === 'connect' && $user->referral_source) { + $history[] = [ + 'step' => 'referral', + 'values' => [$user->referral_source->value], + ]; + } + + return $history; + } + private function redirectIfUnavailable(Request $request): ?RedirectResponse { $user = $request->user(); diff --git a/lang/ar/welcome.php b/lang/ar/welcome.php index 9b64c5b3c..d701cea7a 100644 --- a/lang/ar/welcome.php +++ b/lang/ar/welcome.php @@ -60,8 +60,15 @@ 'other' => 'شيء آخر', ], 'connect' => [ - 'title' => 'اربط حسابًا اجتماعيًا', - 'description' => 'اختر شبكة واحدة على الأقل يمكن لـ TryPost النشر عليها.', + 'title' => 'أي شبكة تستخدم أكثر؟', + 'description' => 'اختر التي تنشر عليها أكثر.', + 'follow_up' => 'اربط حساب :network.', + 'latest_post' => 'هذا أحدث منشور لديك.', + 'pitch_views' => '{0} هذا المنشور حصل على :views مشاهدة على :network.|{1} هذا المنشور حصل على :views مشاهدة على :network.|[2,*] هذا المنشور حصل على :views مشاهدة على :network.', + 'pitch_no_views' => 'هذا المنشور نُشر على :network فقط.', + 'pitch_missed' => 'المنشور نفسه على :first و:second يمكن أن يضيف بسهولة :each مشاهدة على كل منهما. هؤلاء :extra شخصًا لم يروه.', + 'pitch_sales' => 'قد تكون تخسر آلاف المبيعات.', + 'change_network' => 'غيّر الشبكة', 'required' => 'اربط حسابًا اجتماعيًا واحدًا على الأقل للمتابعة.', ], ]; diff --git a/lang/de/welcome.php b/lang/de/welcome.php index 4fbdf7264..9caed35ff 100644 --- a/lang/de/welcome.php +++ b/lang/de/welcome.php @@ -60,8 +60,15 @@ 'other' => 'Etwas anderes', ], 'connect' => [ - 'title' => 'Verbinde ein soziales Konto', - 'description' => 'Wähle mindestens ein Netzwerk, auf dem TryPost deine Inhalte veröffentlichen kann.', + 'title' => 'Welches Netzwerk nutzt du am meisten?', + 'description' => 'Wähle das, auf dem du am meisten postest.', + 'follow_up' => 'Verbinde dein :network-Konto.', + 'latest_post' => 'Das ist dein neuester Beitrag.', + 'pitch_views' => '{0} Dieser Beitrag hatte :views Views auf :network.|{1} Dieser Beitrag hatte :views View auf :network.|[2,*] Dieser Beitrag hatte :views Views auf :network.', + 'pitch_no_views' => 'Dieser Beitrag lief nur auf :network.', + 'pitch_missed' => 'Derselbe Beitrag auf :first und :second könnte leicht noch :each Views auf jedem holen. Das sind :extra Leute, die ihn nie gesehen haben.', + 'pitch_sales' => 'Du lässt vielleicht Tausende an Umsatz liegen.', + 'change_network' => 'Netzwerk wechseln', 'required' => 'Verbinde mindestens ein soziales Konto, um fortzufahren.', ], ]; diff --git a/lang/el/welcome.php b/lang/el/welcome.php index f4ad9557d..668512772 100644 --- a/lang/el/welcome.php +++ b/lang/el/welcome.php @@ -60,8 +60,15 @@ 'other' => 'Κάτι άλλο', ], 'connect' => [ - 'title' => 'Σύνδεσε έναν λογαριασμό κοινωνικής δικτύωσης', - 'description' => 'Επίλεξε τουλάχιστον ένα δίκτυο όπου το TryPost μπορεί να δημοσιεύει το περιεχόμενό σου.', + 'title' => 'Ποιο δίκτυο χρησιμοποιείς περισσότερο;', + 'description' => 'Διάλεξε αυτό στο οποίο δημοσιεύεις πιο συχνά.', + 'follow_up' => 'Σύνδεσε τον λογαριασμό :network.', + 'latest_post' => 'Αυτή είναι η πιο πρόσφατη ανάρτησή σου.', + 'pitch_views' => '{0} Αυτή η ανάρτηση είχε :views προβολές στο :network.|{1} Αυτή η ανάρτηση είχε :views προβολή στο :network.|[2,*] Αυτή η ανάρτηση είχε :views προβολές στο :network.', + 'pitch_no_views' => 'Αυτή η ανάρτηση βγήκε μόνο στο :network.', + 'pitch_missed' => 'Η ίδια ανάρτηση στο :first και στο :second μπορεί εύκολα να πάρει άλλες :each προβολές στο καθένα. Είναι :extra άνθρωποι που δεν την είδαν.', + 'pitch_sales' => 'Μπορεί να χάνεις χιλιάδες πωλήσεις.', + 'change_network' => 'Άλλαξε δίκτυο', 'required' => 'Σύνδεσε τουλάχιστον έναν λογαριασμό κοινωνικής δικτύωσης για να συνεχίσεις.', ], ]; diff --git a/lang/en/welcome.php b/lang/en/welcome.php index 1664e5aba..5ea41497c 100644 --- a/lang/en/welcome.php +++ b/lang/en/welcome.php @@ -60,8 +60,15 @@ 'other' => 'Something else', ], 'connect' => [ - 'title' => 'Connect a social account', - 'description' => 'Choose at least one network where TryPost can publish your content.', + 'title' => 'Which social do you use most?', + 'description' => 'Pick the one you post on the most.', + 'follow_up' => 'Connect your :network account.', + 'latest_post' => "Here's your latest post.", + 'pitch_views' => '{0} This post got :views views on :network.|{1} This post got :views view on :network.|[2,*] This post got :views views on :network.', + 'pitch_no_views' => 'This post only went out on :network.', + 'pitch_missed' => 'The same post on :first and :second could easily pick up another :each views on each. That is :extra people who never saw it.', + 'pitch_sales' => 'You might be leaving thousands in sales on the table.', + 'change_network' => 'Change network', 'required' => 'Connect at least one social account to continue.', ], ]; diff --git a/lang/es/welcome.php b/lang/es/welcome.php index f63779152..76e29980e 100644 --- a/lang/es/welcome.php +++ b/lang/es/welcome.php @@ -60,8 +60,15 @@ 'other' => 'Otra cosa', ], 'connect' => [ - 'title' => 'Conecta una red social', - 'description' => 'Elige al menos una red donde TryPost pueda publicar tu contenido.', + 'title' => '¿En qué red publicas más?', + 'description' => 'Elige la que más usas.', + 'follow_up' => 'Conecta tu cuenta de :network.', + 'latest_post' => 'Esta es tu última publicación.', + 'pitch_views' => '{0} Esta publicación tuvo :views views en :network.|{1} Esta publicación tuvo :views view en :network.|[2,*] Esta publicación tuvo :views views en :network.', + 'pitch_no_views' => 'Esta publicación solo salió en :network.', + 'pitch_missed' => 'La misma publicación en :first y :second podría sumar otras :each views en cada una. Son :extra personas que no la vieron.', + 'pitch_sales' => 'Puedes estar dejando miles en ventas sobre la mesa.', + 'change_network' => 'Cambiar de red', 'required' => 'Conecta al menos una red social para continuar.', ], ]; diff --git a/lang/fr/welcome.php b/lang/fr/welcome.php index bbc8fddd5..7f89f1616 100644 --- a/lang/fr/welcome.php +++ b/lang/fr/welcome.php @@ -60,8 +60,15 @@ 'other' => 'Autre chose', ], 'connect' => [ - 'title' => 'Connectez un réseau social', - 'description' => 'Choisissez au moins un réseau sur lequel TryPost peut publier votre contenu.', + 'title' => 'Quel réseau utilisez-vous le plus ?', + 'description' => 'Choisissez celui où vous publiez le plus.', + 'follow_up' => 'Connectez votre compte :network.', + 'latest_post' => 'Voici votre dernière publication.', + 'pitch_views' => '{0} Ce post a eu :views vues sur :network.|{1} Ce post a eu :views vue sur :network.|[2,*] Ce post a eu :views vues sur :network.', + 'pitch_no_views' => 'Ce post n’est sorti que sur :network.', + 'pitch_missed' => 'Le même post sur :first et :second pourrait facilement faire :each vues de plus sur chacun. Ce sont :extra personnes qui ne l’ont jamais vu.', + 'pitch_sales' => 'Vous laissez peut-être des milliers de ventes sur la table.', + 'change_network' => 'Changer de réseau', 'required' => 'Connectez au moins un réseau social pour continuer.', ], ]; diff --git a/lang/it/welcome.php b/lang/it/welcome.php index 8f34df080..e0c224214 100644 --- a/lang/it/welcome.php +++ b/lang/it/welcome.php @@ -60,8 +60,15 @@ 'other' => 'Qualcos\'altro', ], 'connect' => [ - 'title' => 'Collega un account social', - 'description' => 'Scegli almeno una rete su cui TryPost può pubblicare i tuoi contenuti.', + 'title' => 'Quale social usi di più?', + 'description' => 'Scegli quello su cui pubblichi di più.', + 'follow_up' => 'Collega il tuo account :network.', + 'latest_post' => 'Ecco il tuo ultimo post.', + 'pitch_views' => '{0} Questo post ha avuto :views view su :network.|{1} Questo post ha avuto :views view su :network.|[2,*] Questo post ha avuto :views view su :network.', + 'pitch_no_views' => 'Questo post è uscito solo su :network.', + 'pitch_missed' => 'Lo stesso post su :first e :second potrebbe facilmente prendere altre :each view su ciascuno. Sono :extra persone che non l’hanno visto.', + 'pitch_sales' => 'Potresti stare lasciando migliaia di vendite sul tavolo.', + 'change_network' => 'Cambia rete', 'required' => 'Collega almeno un account social per continuare.', ], ]; diff --git a/lang/ja/welcome.php b/lang/ja/welcome.php index 24eaac618..17ad842f8 100644 --- a/lang/ja/welcome.php +++ b/lang/ja/welcome.php @@ -60,8 +60,15 @@ 'other' => 'その他', ], 'connect' => [ - 'title' => 'SNSアカウントを接続', - 'description' => 'TryPostが投稿できるネットワークを少なくとも1つ選んでください。', + 'title' => 'いちばんよく使うSNSは?', + 'description' => 'いちばん投稿しているものを選んでください。', + 'follow_up' => ':network アカウントを接続してください。', + 'latest_post' => '最新の投稿はこちらです。', + 'pitch_views' => '{0} この投稿は:networkで:views回表示されました。|{1} この投稿は:networkで:views回表示されました。|[2,*] この投稿は:networkで:views回表示されました。', + 'pitch_no_views' => 'この投稿は:networkにしか出ていません。', + 'pitch_missed' => '同じ投稿を:firstと:secondにも出せば、それぞれあと:each回の表示は十分あり得ます。見ていない人が:extra人いるということです。', + 'pitch_sales' => '何千もの売上を逃しているかもしれません。', + 'change_network' => 'ネットワークを変更', 'required' => '続けるには、少なくとも1つのSNSアカウントを接続してください。', ], ]; diff --git a/lang/ko/welcome.php b/lang/ko/welcome.php index 8156ae271..fe9e6dc9b 100644 --- a/lang/ko/welcome.php +++ b/lang/ko/welcome.php @@ -60,8 +60,15 @@ 'other' => '기타', ], 'connect' => [ - 'title' => '소셜 계정을 연결하세요', - 'description' => 'TryPost가 콘텐츠를 게시할 네트워크를 하나 이상 선택하세요.', + 'title' => '가장 많이 쓰는 소셜은 어디인가요?', + 'description' => '가장 자주 올리는 곳을 고르세요.', + 'follow_up' => ':network 계정을 연결하세요.', + 'latest_post' => '가장 최근 게시물입니다.', + 'pitch_views' => '{0} 이 게시물은 :network에서 :views회 봤습니다.|{1} 이 게시물은 :network에서 :views회 봤습니다.|[2,*] 이 게시물은 :network에서 :views회 봤습니다.', + 'pitch_no_views' => '이 게시물은 :network에만 올라갔습니다.', + 'pitch_missed' => '같은 게시물을 :first와 :second에도 올리면 각각 :each회는 더 나올 수 있습니다. :extra명이 이걸 못 본 거예요.', + 'pitch_sales' => '매출 수천을 놓치고 있을 수 있어요.', + 'change_network' => '네트워크 바꾸기', 'required' => '계속하려면 소셜 계정을 하나 이상 연결하세요.', ], ]; diff --git a/lang/nl/welcome.php b/lang/nl/welcome.php index f6250a224..3f54b8688 100644 --- a/lang/nl/welcome.php +++ b/lang/nl/welcome.php @@ -60,8 +60,15 @@ 'other' => 'Iets anders', ], 'connect' => [ - 'title' => 'Verbind een social account', - 'description' => 'Kies minstens één netwerk waarop TryPost je content kan plaatsen.', + 'title' => 'Welk netwerk gebruik je het meest?', + 'description' => 'Kies het netwerk waarop je het meest post.', + 'follow_up' => 'Verbind je :network-account.', + 'latest_post' => 'Dit is je laatste post.', + 'pitch_views' => '{0} Deze post had :views views op :network.|{1} Deze post had :views view op :network.|[2,*] Deze post had :views views op :network.', + 'pitch_no_views' => 'Deze post ging alleen naar :network.', + 'pitch_missed' => 'Dezelfde post op :first en :second kan zomaar nog :each views per netwerk erbij krijgen. Dat zijn :extra mensen die het nooit zagen.', + 'pitch_sales' => 'Je laat misschien duizenden aan sales liggen.', + 'change_network' => 'Ander netwerk', 'required' => 'Verbind minstens één social account om door te gaan.', ], ]; diff --git a/lang/pl/welcome.php b/lang/pl/welcome.php index 3dbb28db9..6ec73dd15 100644 --- a/lang/pl/welcome.php +++ b/lang/pl/welcome.php @@ -60,8 +60,15 @@ 'other' => 'Coś innego', ], 'connect' => [ - 'title' => 'Połącz konto społecznościowe', - 'description' => 'Wybierz co najmniej jedną sieć, na której TryPost może publikować Twoje treści.', + 'title' => 'Z której sieci korzystasz najczęściej?', + 'description' => 'Wybierz tę, na której najczęściej publikujesz.', + 'follow_up' => 'Połącz konto :network.', + 'latest_post' => 'Oto twój najnowszy post.', + 'pitch_views' => '{0} Ten post miał :views wyświetleń na :network.|{1} Ten post miał :views wyświetlenie na :network.|[2,*] Ten post miał :views wyświetleń na :network.', + 'pitch_no_views' => 'Ten post poszedł tylko na :network.', + 'pitch_missed' => 'Ten sam post na :first i :second może łatwo dostać jeszcze po :each wyświetleń. To :extra osób, które go nie widziały.', + 'pitch_sales' => 'Możesz tracić tysiące sprzedaży.', + 'change_network' => 'Zmień sieć', 'required' => 'Połącz co najmniej jedno konto społecznościowe, aby kontynuować.', ], ]; diff --git a/lang/pt-BR/welcome.php b/lang/pt-BR/welcome.php index 6cca5b662..148fe17e0 100644 --- a/lang/pt-BR/welcome.php +++ b/lang/pt-BR/welcome.php @@ -60,8 +60,15 @@ 'other' => 'Outra coisa', ], 'connect' => [ - 'title' => 'Conecte uma rede social', - 'description' => 'Escolha pelo menos uma rede onde o TryPost possa publicar seu conteúdo.', + 'title' => 'Qual rede social você mais usa?', + 'description' => 'Escolhe a que você mais publica.', + 'follow_up' => 'Conecte sua conta do :network.', + 'latest_post' => 'Este é o seu post mais recente.', + 'pitch_views' => '{0} Esse post teve :views views no :network.|{1} Esse post teve :views view no :network.|[2,*] Esse post teve :views views no :network.', + 'pitch_no_views' => 'Esse post só foi pro :network.', + 'pitch_missed' => 'O mesmo post no :first e no :second podia ter mais :each views em cada. São :extra pessoas que não viram isso.', + 'pitch_sales' => 'Dá pra estar perdendo milhares em vendas.', + 'change_network' => 'Trocar rede', 'required' => 'Conecte pelo menos uma rede social para continuar.', ], ]; diff --git a/lang/ru/welcome.php b/lang/ru/welcome.php index 4dad73b90..e3737f53f 100644 --- a/lang/ru/welcome.php +++ b/lang/ru/welcome.php @@ -60,8 +60,15 @@ 'other' => 'Что-то другое', ], 'connect' => [ - 'title' => 'Подключите соцсеть', - 'description' => 'Выберите хотя бы одну сеть, где TryPost сможет публиковать ваш контент.', + 'title' => 'Какой соцсетью вы пользуетесь чаще всего?', + 'description' => 'Выберите ту, где вы публикуете больше всего.', + 'follow_up' => 'Подключите аккаунт :network.', + 'latest_post' => 'Вот ваш последний пост.', + 'pitch_views' => '{0} У этого поста :views просмотров в :network.|{1} У этого поста :views просмотр в :network.|[2,*] У этого поста :views просмотров в :network.', + 'pitch_no_views' => 'Этот пост вышел только в :network.', + 'pitch_missed' => 'Тот же пост в :first и :second легко наберёт ещё по :each просмотров. Это :extra человек, которые его не видели.', + 'pitch_sales' => 'Вы можете терять тысячи продаж.', + 'change_network' => 'Сменить сеть', 'required' => 'Подключите хотя бы одну соцсеть, чтобы продолжить.', ], ]; diff --git a/lang/tr/welcome.php b/lang/tr/welcome.php index 9d603e60b..60471088f 100644 --- a/lang/tr/welcome.php +++ b/lang/tr/welcome.php @@ -60,8 +60,15 @@ 'other' => 'Başka bir şey', ], 'connect' => [ - 'title' => 'Bir sosyal hesap bağla', - 'description' => 'TryPost’un içeriğini yayınlayabileceği en az bir ağ seç.', + 'title' => 'En çok hangi sosyal ağı kullanıyorsun?', + 'description' => 'En çok paylaşım yaptığın ağı seç.', + 'follow_up' => ':network hesabını bağla.', + 'latest_post' => 'İşte son paylaşımın.', + 'pitch_views' => '{0} Bu gönderi :network üzerinde :views görüntüleme aldı.|{1} Bu gönderi :network üzerinde :views görüntüleme aldı.|[2,*] Bu gönderi :network üzerinde :views görüntüleme aldı.', + 'pitch_no_views' => 'Bu gönderi yalnızca :network’te yayınlandı.', + 'pitch_missed' => 'Aynı gönderi :first ve :second’da her birinde rahatça :each görüntüleme daha alabilir. Bunu hiç görmeyen :extra kişi var.', + 'pitch_sales' => 'Binlerce satışı kaçırıyor olabilirsin.', + 'change_network' => 'Ağı değiştir', 'required' => 'Devam etmek için en az bir sosyal hesap bağla.', ], ]; diff --git a/lang/uk/welcome.php b/lang/uk/welcome.php index cb96dab64..80311eaea 100644 --- a/lang/uk/welcome.php +++ b/lang/uk/welcome.php @@ -60,8 +60,15 @@ 'other' => 'Щось інше', ], 'connect' => [ - 'title' => 'Підключіть соцмережу', - 'description' => 'Оберіть принаймні одну мережу, де TryPost зможе публікувати ваш контент.', + 'title' => 'Якою соцмережею ви користуєтесь найчастіше?', + 'description' => 'Оберіть ту, де ви публікуєте найбільше.', + 'follow_up' => 'Підключіть акаунт :network.', + 'latest_post' => 'Ось ваш останній допис.', + 'pitch_views' => '{0} Цей допис має :views переглядів у :network.|{1} Цей допис має :views перегляд у :network.|[2,*] Цей допис має :views переглядів у :network.', + 'pitch_no_views' => 'Цей допис вийшов лише в :network.', + 'pitch_missed' => 'Той самий допис у :first і :second легко набере ще по :each переглядів. Це :extra людей, які його не бачили.', + 'pitch_sales' => 'Ви можете втрачати тисячі продажів.', + 'change_network' => 'Змінити мережу', 'required' => 'Підключіть принаймні одну соцмережу, щоб продовжити.', ], ]; diff --git a/lang/zh/welcome.php b/lang/zh/welcome.php index 2d3737224..063c4475e 100644 --- a/lang/zh/welcome.php +++ b/lang/zh/welcome.php @@ -60,8 +60,15 @@ 'other' => '其他', ], 'connect' => [ - 'title' => '连接社交账号', - 'description' => '选择至少一个 TryPost 可以发布内容的平台。', + 'title' => '你最常用哪个社交平台?', + 'description' => '选你发得最多的那个。', + 'follow_up' => '连接你的 :network 账号。', + 'latest_post' => '这是你最近的一条帖子。', + 'pitch_views' => '{0} 这条帖子在:network上有:views次观看。|{1} 这条帖子在:network上有:views次观看。|[2,*] 这条帖子在:network上有:views次观看。', + 'pitch_no_views' => '这条帖子只发在了:network。', + 'pitch_missed' => '同一条发到:first和:second,每个平台再多:each次观看很常见。等于有:extra人没看到。', + 'pitch_sales' => '你可能正在丢掉成千上万的成交。', + 'change_network' => '更换平台', 'required' => '请至少连接一个社交账号后再继续。', ], ]; diff --git a/resources/js/components/accounts/NetworkConnectGrid.vue b/resources/js/components/accounts/NetworkConnectGrid.vue index 8c2d7d897..5a8283e18 100644 --- a/resources/js/components/accounts/NetworkConnectGrid.vue +++ b/resources/js/components/accounts/NetworkConnectGrid.vue @@ -42,10 +42,12 @@ const props = withDefaults( platforms: AvailablePlatform[]; connectedAccounts?: ConnectedAccount[]; gridClass?: string; + variant?: 'grid' | 'list'; }>(), { connectedAccounts: () => [], gridClass: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5', + variant: 'grid', }, ); @@ -260,7 +262,52 @@ const cardState = computed((): Record => { diff --git a/resources/js/components/welcome/PlatformChips.vue b/resources/js/components/welcome/PlatformChips.vue index 1bfc0287f..0b737baa2 100644 --- a/resources/js/components/welcome/PlatformChips.vue +++ b/resources/js/components/welcome/PlatformChips.vue @@ -2,25 +2,29 @@ import { IconCheck } from '@tabler/icons-vue'; import type { AvailablePlatform } from '@/components/accounts/NetworkConnectGrid.vue'; +import { welcomePlatformLabel } from '@/components/welcome/welcomePlatformLabel'; +import { getPlatformLogo } from '@/composables/usePlatformLogo'; -const props = defineProps<{ - platforms: AvailablePlatform[]; - modelValue: string; -}>(); +const props = withDefaults( + defineProps<{ + platforms: AvailablePlatform[]; + modelValue: string; + readonly?: boolean; + }>(), + { + readonly: false, + }, +); const emit = defineEmits<{ 'update:modelValue': [value: string]; }>(); -const logoFor = (value: string): string => - value === 'instagram-facebook' - ? '/images/accounts/instagram.png' - : `/images/accounts/${value}.png`; - -const shortLabel = (label: string): string => - label.includes('(') ? label.split('(')[0].trim() : label; - const select = (value: string): void => { + if (props.readonly) { + return; + } + emit('update:modelValue', value); }; @@ -34,25 +38,39 @@ const select = (value: string): void => { :aria-pressed="props.modelValue === platform.value" :data-testid="`welcome-platform-${platform.value}`" :dusk="`welcome-platform-${platform.value}`" + :disabled="props.readonly" :class="[ - 'inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm transition-colors', + 'inline-flex items-center gap-2 rounded-full border-2 border-foreground py-1.5 ps-1.5 pe-3 text-start shadow-2xs', + props.readonly + ? 'cursor-default' + : 'cursor-pointer transition-shadow hover:shadow-md', props.modelValue === platform.value - ? 'border-primary/40 bg-primary/10 text-foreground' - : 'border-border bg-background text-foreground hover:bg-muted', + ? 'bg-violet-100' + : 'bg-card', ]" @click="select(platform.value)" > - - {{ shortLabel(platform.label) }} - + + + + {{ welcomePlatformLabel(platform.label) }} + + + class="inline-flex size-4 shrink-0 items-center justify-center rounded-full border-2 border-foreground bg-foreground" + > + + diff --git a/resources/js/components/welcome/PublishMethodChips.vue b/resources/js/components/welcome/PublishMethodChips.vue new file mode 100644 index 000000000..4fde737b0 --- /dev/null +++ b/resources/js/components/welcome/PublishMethodChips.vue @@ -0,0 +1,105 @@ + + + diff --git a/resources/js/components/welcome/ReferralChips.vue b/resources/js/components/welcome/ReferralChips.vue index 784d9e872..00681348a 100644 --- a/resources/js/components/welcome/ReferralChips.vue +++ b/resources/js/components/welcome/ReferralChips.vue @@ -21,10 +21,18 @@ import { import { trans } from 'laravel-vue-i18n'; import type { FunctionalComponent } from 'vue'; -const props = defineProps<{ - sources: string[]; - modelValue: string; -}>(); +const props = withDefaults( + defineProps<{ + sources: string[]; + modelValue: string; + disabled?: boolean; + readonly?: boolean; + }>(), + { + disabled: false, + readonly: false, + }, +); const emit = defineEmits<{ 'update:modelValue': [value: string]; @@ -138,6 +146,10 @@ const sourceLabel = (value: string): string => const isSelected = (value: string): boolean => props.modelValue === value; const select = (value: string): void => { + if (props.disabled || props.readonly) { + return; + } + emit('update:modelValue', value); }; @@ -149,19 +161,21 @@ const select = (value: string): void => { :key="source" type="button" :aria-pressed="isSelected(source)" + :disabled="props.disabled || props.readonly" :data-testid="`welcome-source-${source}`" :dusk="`welcome-source-${source}`" :class="[ - 'inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm transition-colors', - isSelected(source) - ? 'border-primary/40 bg-primary/10 text-foreground' - : 'border-border bg-background text-foreground hover:bg-muted', + 'inline-flex items-center gap-2 rounded-full border-2 border-foreground py-1.5 ps-1.5 pe-3 text-start shadow-2xs', + props.readonly + ? 'cursor-default' + : 'cursor-pointer transition-shadow hover:shadow-md disabled:cursor-not-allowed disabled:opacity-60', + isSelected(source) ? 'bg-violet-100' : 'bg-card', ]" @click="select(source)" > @@ -178,12 +192,18 @@ const select = (value: string): void => { stroke-width="2" /> - {{ sourceLabel(source) }} - + {{ sourceLabel(source) }} + + + class="inline-flex size-4 shrink-0 items-center justify-center rounded-full border-2 border-foreground bg-foreground" + > + + diff --git a/resources/js/components/welcome/WelcomeQuestion.vue b/resources/js/components/welcome/WelcomeQuestion.vue new file mode 100644 index 000000000..47afbecd3 --- /dev/null +++ b/resources/js/components/welcome/WelcomeQuestion.vue @@ -0,0 +1,41 @@ + + + diff --git a/resources/js/components/welcome/welcomePlatformLabel.ts b/resources/js/components/welcome/welcomePlatformLabel.ts new file mode 100644 index 000000000..7f9eabcb1 --- /dev/null +++ b/resources/js/components/welcome/welcomePlatformLabel.ts @@ -0,0 +1,2 @@ +export const welcomePlatformLabel = (label: string): string => + label.replace(/\s*\(Facebook Business\)$/, '').trim(); diff --git a/resources/js/layouts/WelcomeLayout.vue b/resources/js/layouts/WelcomeLayout.vue index d3a222a1d..28045a8e8 100644 --- a/resources/js/layouts/WelcomeLayout.vue +++ b/resources/js/layouts/WelcomeLayout.vue @@ -1,14 +1,8 @@