diff --git a/.env.example b/.env.example index 4a631e7ee..93f7af6a8 100644 --- a/.env.example +++ b/.env.example @@ -191,11 +191,17 @@ STRIPE_SECRET= STRIPE_WEBHOOK_SECRET= # Set to false to allow generic signup trial without requiring a card. REQUIRE_CARD_FOR_TRIAL=true -# Free trial length in days, used only for the no-card generic trial above. +# Checkout trial length in days (card collected up front when REQUIRE_CARD_FOR_TRIAL=true). +# - >0: Stripe Checkout trial for N days for first-time signups only (no prior +# real subscription). Values below 2 are clamped to 2 (Stripe's 48h min). +# - 0: no Checkout trial; charge starts immediately. +# - Returning / canceled accounts never get another Checkout trial. +# - When REQUIRE_CARD_FOR_TRIAL=false, Checkout never applies a trial (generic +# signup trial uses this value instead; must be >= 1 in that mode). +# Empty = default 8. CASHIER_TRIAL_DAYS=8 -# Stripe Coupon ID (amount_off, duration=once) so the first invoice at -# checkout comes out to $1 instead of the full monthly price. -STRIPE_FIRST_MONTH_COUPON_ID= +# Show Stripe Checkout promotion-code field. +CASHIER_ALLOW_PROMOTION_CODES=true # Stripe Plan Price IDs (one per plan × interval). Used by PlanSeeder. STRIPE_WORKSPACE_MONTHLY= @@ -212,6 +218,10 @@ NIGHTWATCH_TOKEN= # BLUESKY_ENABLED=true # TELEGRAM_ENABLED=true +# Onboarding MCP client settings URLs (optional overrides) +# MCP_CLAUDE_SETTINGS_URL=https://claude.ai/customize/connectors +# MCP_CHATGPT_SETTINGS_URL=https://chatgpt.com/plugins#settings/Connectors?create-connector=true&redirectAfter=%2Fplugins + # Media Services UNSPLASH_ACCESS_KEY= UNSPLASH_SECRET_KEY= diff --git a/.superpowers/plans/2026-07-24-post-subscription-onboarding.md b/.superpowers/plans/2026-07-24-post-subscription-onboarding.md new file mode 100644 index 000000000..0ffb0611d --- /dev/null +++ b/.superpowers/plans/2026-07-24-post-subscription-onboarding.md @@ -0,0 +1,552 @@ +# Post-Subscription Onboarding Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rename pre-Stripe ICP to Welcome (`/welcome/{step}`), remove social-before-pay, and ship a single-page post-Stripe activation checklist at `/onboarding` (MCP + social + first post) with skip + residual nudge. + +**Architecture:** `WelcomeController` owns ICP + Stripe checkout (no social gate). `OnboardingController` owns the activation page behind `EnsureAccountReady`. `ResolveOnboardingStatus` derives MCP/social/post completion and residual visibility; Account stores only `onboarding_completed_at` / `onboarding_dismissed_at`. Frontend: one `onboarding/Index.vue` with Inertia `usePoll`; residual banner via shared Inertia prop in `AppSidebarLayout`. + +**Tech Stack:** Laravel 13, Inertia v3 + Vue 3, Cashier, Passport (`AccessToken`), Pest 4, Wayfinder, PostHog enums/jobs, Tailwind v4, `@tabler/icons-vue`. + +**Spec:** `.superpowers/specs/2026-07-24-post-subscription-onboarding-design.md` + +## Global Constraints + +- Activation is one page only — no `/onboarding/step-*` wizard. +- AHA = MCP connected + first post exists (draft OK); publish optional. +- Skippable; no hard-gate after first land. +- v1 MCP clients: Claude + ChatGPT only. +- First post step: MCP prompt primary + UI fallback to `route('app.posts.create')`. +- Social connect removed from pre-Stripe; lives on activation page via existing `NetworkConnectGrid`. +- MCP “connected” = user has non-revoked `AccessToken` with `workspace_id IS NULL` (OAuth/MCP session; PATs bind `workspace_id`). +- Self-hosted skips Welcome and activation (redirect calendar), same as today. +- Never hardcode URLs in tests — use `route()`. +- FormRequests under `app/Http/Requests/App//`; no inline `$request->validate`. +- Arrow functions only in Vue/TS; Tabler icons only; Wayfinder for frontend routes. +- After PHP changes: `vendor/bin/pint --dirty --format agent`. +- After route/controller changes: `php artisan wayfinder:generate`. +- i18n: all 15 locales that have `lang/*/onboarding.php`. +- Branch: `feature/post-subscription-onboarding` (already created). +- Do not push or open PR unless asked. + +## File structure (target) + +| Path | Responsibility | +|------|----------------| +| `app/Actions/Onboarding/ResolveOnboardingStatus.php` | Derive step booleans + residual + maybe mark completed | +| `app/Http/Controllers/App/WelcomeController.php` | ICP + subscribe + checkout | +| `app/Http/Controllers/App/OnboardingController.php` | Activation show / dismiss / complete | +| `app/Http/Requests/App/Welcome/*` | Persona / goals / referral FormRequests | +| `app/Enums/PostHog/WelcomeEvent.php` | Welcome analytics event names | +| `app/Enums/PostHog/OnboardingEvent.php` | Onboarding analytics event names | +| `resources/js/pages/welcome/{Persona,Goals,ReferralSource,Subscribe}.vue` | ICP UI | +| `resources/js/pages/onboarding/Index.vue` | Single activation checklist | +| `resources/js/layouts/WelcomeLayout.vue` | Renamed from OnboardingLayout | +| `resources/js/components/onboarding/OnboardingResidualBanner.vue` | Residual nudge | +| `lang/*/welcome.php` | ICP copy (moved from onboarding) | +| `lang/*/onboarding.php` | Activation copy (replaced) | +| `tests/Feature/Welcome/WelcomeControllerTest.php` | Renamed + updated ICP tests | +| `tests/Feature/Onboarding/OnboardingControllerTest.php` | New activation tests | +| `tests/Feature/Actions/Onboarding/ResolveOnboardingStatusTest.php` | Status derivation | + +--- + +### Task 1: Account onboarding timestamps + +**Files:** +- Create: migration via `php artisan make:migration add_onboarding_timestamps_to_accounts_table --no-interaction` +- Modify: `app/Models/Account.php` (`$fillable`, `$casts`) +- Modify: `database/factories/AccountFactory.php` if needed for defaults +- Test: `tests/Feature/Onboarding/AccountOnboardingTimestampsTest.php` + +**Interfaces:** +- Produces: `Account::$onboarding_completed_at`, `Account::$onboarding_dismissed_at` (`?CarbonInterface`) + +- [ ] **Step 1: Write the failing test** + +```php +create(); + $account = $user->account; + + $account->update([ + 'onboarding_completed_at' => now(), + 'onboarding_dismissed_at' => now()->subMinute(), + ]); + + $account->refresh(); + + expect($account->onboarding_completed_at)->not->toBeNull() + ->and($account->onboarding_dismissed_at)->not->toBeNull(); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `php artisan test --compact tests/Feature/Onboarding/AccountOnboardingTimestampsTest.php` +Expected: FAIL (unknown columns / not fillable) + +- [ ] **Step 3: Create migration + update Account** + +Migration `up`: + +```php +Schema::table('accounts', function (Blueprint $table) { + $table->timestamp('onboarding_completed_at')->nullable()->after('trial_ends_at'); + $table->timestamp('onboarding_dismissed_at')->nullable()->after('onboarding_completed_at'); +}); + +// Backfill: existing subscribed accounts must not see residual/activation. +Account::query() + ->whereHas('subscriptions', function ($query) { + $query->where('type', Account::SUBSCRIPTION_NAME) + ->whereIn('stripe_status', ['active', 'trialing']); + }) + ->whereNull('onboarding_dismissed_at') + ->whereNull('onboarding_completed_at') + ->update(['onboarding_dismissed_at' => now()]); +``` + +On `Account`: add both fields to `$fillable` and cast to `datetime`. + +- [ ] **Step 4: Migrate and run test** + +Run: `php artisan migrate --no-interaction && php artisan test --compact tests/Feature/Onboarding/AccountOnboardingTimestampsTest.php` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add database/migrations/*onboarding_timestamps* app/Models/Account.php tests/Feature/Onboarding/AccountOnboardingTimestampsTest.php +git commit -m "Add account onboarding completed and dismissed timestamps." +``` + +--- + +### Task 2: ResolveOnboardingStatus action + +**Files:** +- Create: `app/Actions/Onboarding/ResolveOnboardingStatus.php` +- Create: `tests/Feature/Actions/Onboarding/ResolveOnboardingStatusTest.php` + +**Interfaces:** +- Consumes: `User`, `Account`, `AccessToken` (workspace_id null = MCP), workspace socialAccounts, workspace posts +- Produces: + +```php +/** + * @return array{ + * mcp_connected: bool, + * social_connected: bool, + * first_post_created: bool, + * all_complete: bool, + * show_residual: bool, + * completed_at: ?string, + * dismissed_at: ?string + * } + */ +public function handle(User $user): array +``` + +When `all_complete` and `onboarding_completed_at` is null, set `onboarding_completed_at = now()` inside this action (idempotent). + +`show_residual` = not self-hosted AND account subscribed AND `completed_at` null AND `dismissed_at` null. + +MCP connected: + +```php +AccessToken::query() + ->where('user_id', $user->id) + ->whereNull('workspace_id') + ->where('revoked', false) + ->exists(); +``` + +- [ ] **Step 1: Write failing tests** covering: empty state; MCP-only (createToken + forceFill workspace_id null); social-only; post-only; all three auto-sets completed_at; dismissed hides residual; self-hosted show_residual false. + +- [ ] **Step 2: Run tests — expect FAIL** + +Run: `php artisan test --compact tests/Feature/Actions/Onboarding/ResolveOnboardingStatusTest.php` + +- [ ] **Step 3: Implement `ResolveOnboardingStatus`** + +- [ ] **Step 4: Run tests — expect PASS** + +- [ ] **Step 5: Pint + commit** + +```bash +vendor/bin/pint --dirty --format agent +git add app/Actions/Onboarding tests/Feature/Actions/Onboarding +git commit -m "Add ResolveOnboardingStatus for activation checklist state." +``` + +--- + +### Task 3: Rename ICP backend to Welcome + +**Files:** +- Create: `app/Http/Controllers/App/WelcomeController.php` (from `OnboardingController`, minus social-required checkout) +- Create: `app/Http/Requests/App/Welcome/StoreWelcomePersonaRequest.php` (ex-StoreOnboardingRequest) +- Create: `app/Http/Requests/App/Welcome/StoreWelcomeGoalsRequest.php` +- Create: `app/Http/Requests/App/Welcome/StoreWelcomeReferralSourceRequest.php` +- Modify: `routes/app.php` — welcome routes; legacy redirects for goals/referral/connect; point `EnsureAccountReady` target +- Modify: `app/Http/Middleware/App/EnsureAccountReady.php` → `route('app.welcome.persona')` +- Delete: old `OnboardingController` ICP methods later (after Task 7 owns new controller) — for this task, rename file to WelcomeController and leave `/onboarding` route temporarily redirecting unsubscribed users via middleware only +- Update all PHP references: `BillingController::subscribe`, tests `EnsureAccountReadyTest`, `WorkspaceBillingTest`, `BillingControllerTest`, `TrialMiddlewareAccessTest`, `SocialControllerTest` onboarding mentions + +**Route map to register (auth group, not EnsureAccountReady):** + +```php +Route::get('welcome', fn () => redirect()->route('app.welcome.persona'))->name('app.welcome'); +Route::get('welcome/persona', [WelcomeController::class, 'persona'])->name('app.welcome.persona'); +Route::post('welcome/persona', [WelcomeController::class, 'storePersona'])->name('app.welcome.persona.store'); +Route::get('welcome/goals', [WelcomeController::class, 'goals'])->name('app.welcome.goals'); +Route::post('welcome/goals', [WelcomeController::class, 'storeGoals'])->name('app.welcome.goals.store'); +Route::get('welcome/referral-source', [WelcomeController::class, 'referralSource'])->name('app.welcome.referral-source'); +Route::post('welcome/referral-source', [WelcomeController::class, 'storeReferralSource'])->name('app.welcome.referral-source.store'); +Route::get('welcome/subscribe', [WelcomeController::class, 'subscribe'])->name('app.welcome.subscribe'); +Route::post('welcome/checkout', [WelcomeController::class, 'checkout'])->name('app.welcome.checkout'); + +// Legacy ICP URLs +Route::redirect('onboarding/goals', '/welcome/goals'); +Route::redirect('onboarding/referral-source', '/welcome/referral-source'); +Route::redirect('onboarding/connect', '/welcome/subscribe'); +``` + +Do **not** redirect `GET onboarding` — that path becomes activation in Task 7 (behind `EnsureAccountReady`). + +**WelcomeController behavior changes vs today:** +- Inertia pages: `welcome/Persona`, `welcome/Goals`, `welcome/ReferralSource`, `welcome/Subscribe` +- After referral store → `app.welcome.subscribe` (not connect) +- `subscribe()`: plan props only — **no** platforms/accounts required +- `checkout()`: remove social-account existence check; `cancel_url` = `route('app.welcome.subscribe')` +- Subscribed users hitting Welcome → redirect `app.onboarding` if residual should show, else `app.calendar` (until OnboardingController exists, temporary redirect to calendar is OK then tighten in Task 7) + +- [ ] **Step 1: Move/adapt tests** to `tests/Feature/Welcome/WelcomeControllerTest.php` with new route names; add test that checkout works **without** social account; remove must_connect assertions. + +- [ ] **Step 2: Run Welcome tests — expect FAIL** (routes missing) + +- [ ] **Step 3: Implement WelcomeController + requests + routes + middleware redirect** + +- [ ] **Step 4: Run Welcome + middleware tests — expect PASS** + +Run: `php artisan test --compact tests/Feature/Welcome tests/Feature/Middleware/EnsureAccountReadyTest.php tests/Feature/BillingControllerTest.php tests/Feature/WorkspaceBillingTest.php` + +- [ ] **Step 5: Commit** + +```bash +git commit -m "Rename pre-subscription ICP flow to Welcome routes." +``` + +--- + +### Task 4: Welcome frontend + i18n + +**Files:** +- Create: `resources/js/layouts/WelcomeLayout.vue` (copy/rename `OnboardingLayout.vue`) +- Create: `resources/js/pages/welcome/Persona.vue`, `Goals.vue`, `ReferralSource.vue`, `Subscribe.vue` +- Create: `lang/*/welcome.php` — move ICP keys from `onboarding.php` (title, personas, goals_*, referral_*, continue; subscribe title/description from old connect minus must_connect) +- Replace: `lang/*/onboarding.php` can stay temporarily until Task 8; or clear ICP keys now +- Delete: `resources/js/pages/onboarding/{Index,Goals,ReferralSource,Connect}.vue`, `OnboardingLayout.vue` after welcome pages work +- Update Vue imports to `@/routes/app/welcome/...` after wayfinder generate + +**Subscribe.vue:** plan CTA only — no `NetworkConnectGrid`, no `hasConnected` gate. On submit: `trackBeginCheckout` then POST `app.welcome.checkout`. + +- [ ] **Step 1: Move lang files** — for each of 15 locales, create `welcome.php` from ICP portion of `onboarding.php`; add `subscribe.title` / `subscribe.description` (reuse former connect copy without must_connect). + +- [ ] **Step 2: Create Vue pages** mirroring old onboarding pages with new routes/i18n keys/`WelcomeLayout`. + +- [ ] **Step 3: `php artisan wayfinder:generate`** + +- [ ] **Step 4: Smoke** — `php artisan test --compact tests/Feature/Welcome` still PASS; fix Inertia component names in asserts (`welcome/Persona`, etc.). + +- [ ] **Step 5: Commit** + +```bash +git commit -m "Add Welcome Vue pages and i18n; drop social from subscribe." +``` + +--- + +### Task 5: Onboarding activation backend + +**Files:** +- Create: `app/Http/Controllers/App/OnboardingController.php` +- Create: `app/Enums/PostHog/OnboardingEvent.php` +- Modify: `routes/app.php` — inside `EnsureAccountReady` + `EnsureHasWorkspace` group: + +```php +Route::get('onboarding', [OnboardingController::class, 'index'])->name('app.onboarding'); +Route::post('onboarding/dismiss', [OnboardingController::class, 'dismiss'])->name('app.onboarding.dismiss'); +Route::post('onboarding/complete', [OnboardingController::class, 'complete'])->name('app.onboarding.complete'); +``` + +- Create: `tests/Feature/Onboarding/OnboardingControllerTest.php` (activation — do not reuse old ICP file name confusion; old ICP file already moved to Welcome) + +**`index` props:** + +```php +$status = app(ResolveOnboardingStatus::class)->handle($request->user()); + +return Inertia::render('onboarding/Index', [ + 'status' => $status, + 'mcpUrl' => url('/mcp/trypost'), + 'mcpClients' => [ + ['id' => 'claude', 'label' => 'Claude'], + ['id' => 'chatgpt', 'label' => 'ChatGPT'], + ], + 'samplePrompt' => __('onboarding.first_post.sample_prompt'), + 'platforms' => /* same map as old connect */, + 'accounts' => SocialAccountResource::collection($workspace->socialAccounts()->orderBy('id')->get())->resolve(), + 'createPostUrl' => route('app.posts.create'), +]); +``` + +On first view, capture `OnboardingEvent::Viewed` via `PostHogService` (once per request is fine). + +`dismiss`: set `onboarding_dismissed_at`, capture skipped, redirect `app.calendar`. + +`complete`: if not `all_complete`, redirect back to onboarding; else ensure `onboarding_completed_at`, capture completed, redirect `app.calendar`. + +Self-hosted: index/dismiss/complete → calendar. + +Tighten `WelcomeController`: subscribed users → `app.onboarding` when `show_residual`, else calendar. + +- [ ] **Step 1: Write activation feature tests** (subscribed + workspace): renders statuses; dismiss sets timestamp; complete requires all steps; unsubscribed redirected by middleware to welcome; self-hosted → calendar. + +- [ ] **Step 2: Run — expect FAIL** + +- [ ] **Step 3: Implement controller + enum + routes** + +- [ ] **Step 4: Run activation + Welcome subscribed-redirect tests — PASS** + +- [ ] **Step 5: Commit** + +```bash +git commit -m "Add post-subscription onboarding activation endpoints." +``` + +--- + +### Task 6: Onboarding activation UI (single page) + +**Files:** +- Create: `resources/js/pages/onboarding/Index.vue` +- Replace: `lang/*/onboarding.php` with activation copy only (steps, skip, residual, sample prompt, MCP instructions) +- Keep using `NetworkConnectGrid` for social section + +**UI structure (one page):** +1. Header + Skip button (`form.post` dismiss) +2. Checklist item MCP — copy `mcpUrl`, Claude/ChatGPT short instructions, checkmark from `status.mcp_connected` +3. Checklist item Social — `NetworkConnectGrid` with props +4. Checklist item First post — show `samplePrompt` + copy button; link/button to `createPostUrl`; checkmark from `status.first_post_created` +5. When `status.all_complete` — primary “Continue to TryPost” → POST complete (or `router.visit` calendar after complete) + +**Polling:** + +```ts +import { usePoll } from '@inertiajs/vue3'; + +usePoll(2000, { + only: ['status', 'accounts'], +}); +``` + +- [ ] **Step 1: Write `lang/en/onboarding.php` activation strings**, then copy keys to other 14 locales (English fallback OK if translation lagging, but keys must exist in all locales that previously had the file). + +- [ ] **Step 2: Build `Index.vue`** (arrow functions, Tabler icons, Wayfinder routes for dismiss/complete). + +- [ ] **Step 3: `php artisan wayfinder:generate`** + +- [ ] **Step 4: Feature test assertInertia component `onboarding/Index` + status keys** + +- [ ] **Step 5: Commit** + +```bash +git commit -m "Add single-page post-subscription onboarding UI." +``` + +--- + +### Task 7: Redirect billing processing → onboarding + +**Files:** +- Modify: `resources/js/pages/billing/Processing.vue` — replace `accounts.url()` with onboarding route +- Modify/add tests if any assert post-checkout destination; add feature coverage if missing + +```ts +import { index as onboarding } from '@/routes/app/onboarding'; // or named export from wayfinder +const goToOnboarding = () => router.visit(onboarding.url()); +// use goToOnboarding in setTimeout instead of goToAccounts +``` + +- [ ] **Step 1: Change Processing.vue redirect target** + +- [ ] **Step 2: Manually verify no remaining `accounts` import used for redirect** + +- [ ] **Step 3: Commit** + +```bash +git commit -m "Send post-checkout users to onboarding instead of accounts." +``` + +--- + +### Task 8: Residual banner + shared prop + +**Files:** +- Modify: `app/Http/Middleware/App/HandleInertiaRequests.php` — share lazy `onboardingResidual`: + +```php +'onboardingResidual' => fn () => $user && $account + ? data_get(app(ResolveOnboardingStatus::class)->handle($user), 'show_residual', false) + : false, +``` + +- Create: `resources/js/components/onboarding/OnboardingResidualBanner.vue` +- Modify: `resources/js/layouts/app/AppSidebarLayout.vue` — render banner above `` when `page.props.onboardingResidual` +- Create: `tests/Feature/Onboarding/OnboardingResidualShareTest.php` — hit a subscribed app route (e.g. calendar) and assert shared prop true/false + +Banner: short copy + Link to `/onboarding` + dismiss button posting `app.onboarding.dismiss` (or small form). + +Skip rendering on the onboarding page itself (`usePage().url` includes `/onboarding`). + +- [ ] **Step 1: Write share/residual tests** + +- [ ] **Step 2: Implement share + banner** + +- [ ] **Step 3: Run tests — PASS** + +- [ ] **Step 4: Commit** + +```bash +git commit -m "Add dismissible residual onboarding banner in app shell." +``` + +--- + +### Task 9: Welcome + Onboarding PostHog events + +**Files:** +- Create: `app/Enums/PostHog/WelcomeEvent.php` + +```php +enum WelcomeEvent: string +{ + case PersonaSaved = 'welcome.persona_saved'; + case GoalsSaved = 'welcome.goals_saved'; + case ReferralSaved = 'welcome.referral_saved'; + case CheckoutStarted = 'welcome.checkout_started'; +} +``` + +- Create: `app/Enums/PostHog/OnboardingEvent.php` + +```php +enum OnboardingEvent: string +{ + case Viewed = 'onboarding.viewed'; + case StepCompleted = 'onboarding.step_completed'; + case Skipped = 'onboarding.skipped'; + case Completed = 'onboarding.completed'; +} +``` + +- Modify: `WelcomeController` — `capture` on store/checkout (keep existing `identify` for persona/goals/referral) +- Modify: `OnboardingController` — viewed / skipped / completed +- Optional: in `ResolveOnboardingStatus` or controller poll path, when a step flips true, capture `StepCompleted` with `step` property — avoid duplicate spam by only capturing when transitioning (session flash keys `onboarding_step_mcp` etc. OR compare previous request; v1 acceptable: capture from frontend once via optional endpoint — prefer server session flags on the Account JSON column only if already needed; simplest v1: capture StepCompleted inside ResolveOnboardingStatus only when the step is true AND a cache key `onboarding_step:{accountId}:{step}` missing, TTL 30 days) + +**YAGNI for v1:** capture Viewed, Skipped, Completed, and Welcome saves/checkout; defer per-step transition events if costly — but spec lists them, so implement cache-gated StepCompleted in `ResolveOnboardingStatus` when a step is true. + +- [ ] **Step 1: Tests with `Bus::fake` + `SendEvent` asserts** on welcome store and onboarding dismiss/complete/view + +- [ ] **Step 2: Implement enums + captures** + +- [ ] **Step 3: Run related tests — PASS** + +- [ ] **Step 4: Commit** + +```bash +git commit -m "Track welcome and onboarding activation events in PostHog." +``` + +--- + +### Task 10: Cleanup, Wayfinder, full regression + +**Files:** +- Remove dead old onboarding ICP code/files if any remain +- Ensure `Connect.vue` deleted +- Grep for `app.onboarding.goals`, `onboarding/Index` (persona), `OnboardingLayout`, `StoreOnboardingRequest`, `pages/onboarding/Goals` +- Run pint + wayfinder + +- [ ] **Step 1: Grep cleanup** + +```bash +rg -n "app\.onboarding\.(store|goals|referral|connect|checkout)|OnboardingLayout|StoreOnboarding|pages/onboarding/Goals|onboarding/Connect" --glob '!vendor/**' --glob '!node_modules/**' +``` + +Expected: no stale ICP references (activation `app.onboarding` / `app.onboarding.dismiss` OK). + +- [ ] **Step 2: Pint + wayfinder** + +```bash +vendor/bin/pint --dirty --format agent +php artisan wayfinder:generate +``` + +- [ ] **Step 3: Run full related suite** + +```bash +php artisan test --compact \ + tests/Feature/Welcome \ + tests/Feature/Onboarding \ + tests/Feature/Actions/Onboarding \ + tests/Feature/Middleware/EnsureAccountReadyTest.php \ + tests/Feature/BillingControllerTest.php \ + tests/Feature/WorkspaceBillingTest.php \ + tests/Feature/Social/SocialControllerTest.php +``` + +Fix failures. + +- [ ] **Step 4: Final commit if needed** + +```bash +git commit -m "Clean up legacy onboarding ICP references after Welcome rename." +``` + +--- + +## Self-review (spec coverage) + +| Spec requirement | Task | +|------------------|------| +| Welcome `/welcome/{step}` URLs | 3–4 | +| Remove social pre-pay | 3–4 | +| Checkout without social | 3 | +| `/onboarding` single page | 5–6 | +| MCP + social + first post checklist | 5–6 | +| Draft counts / dual create path | 6 (`createPostUrl` + prompt) | +| Skippable + residual | 5, 8 | +| Account timestamps + backfill | 1 | +| ResolveOnboardingStatus | 2 | +| Processing → onboarding | 7 | +| EnsureAccountReady → welcome | 3 | +| PostHog events | 9 | +| Self-hosted skip | 3, 5 | +| Legacy redirects (except GET `/onboarding`) | 3 | +| i18n all locales | 4, 6 | + +**Resolved open details:** +- MCP detection = `AccessToken` with `workspace_id` null, not revoked. +- Complete = dedicated POST `app.onboarding.complete` + auto-set timestamp in ResolveOnboardingStatus. +- Residual = shared `onboardingResidual` + banner in `AppSidebarLayout`. +- MCP UX v1 = copy `mcpUrl` + Claude/ChatGPT labels (no fragile deep links required). diff --git a/.superpowers/specs/2026-07-24-post-subscription-onboarding-design.md b/.superpowers/specs/2026-07-24-post-subscription-onboarding-design.md new file mode 100644 index 000000000..ec1b5ca9a --- /dev/null +++ b/.superpowers/specs/2026-07-24-post-subscription-onboarding-design.md @@ -0,0 +1,198 @@ +# Post-Subscription Onboarding Design + +**Date:** 2026-07-24 +**Branch:** `feature/post-subscription-onboarding` +**Status:** Approved for planning + +## Problem + +TryPost’s current “onboarding” is ICP qualification (persona, goals, referral source) plus a mandatory social connect before Stripe. After payment, `/billing/processing` sends the user to `/accounts`. There is no activation moment — no guided path to connect an MCP agent, connect networks in context, or create a first post. + +The ICP for TryPost is people who create content via MCP clients (Claude, ChatGPT). Without a dedicated post-subscription activation experience, users miss the AHA moment. + +## Goals + +1. Rename and clarify **Welcome** (pre-Stripe ICP) vs **Onboarding** (post-Stripe activation). +2. After Stripe, land on a **single-page** activation checklist at `/onboarding`. +3. AHA = MCP connected + first post created (**draft is enough**; publish optional). +4. Flow is **skippable**, with a dismissible residual checklist in the app. +5. Move social connect out of pre-Stripe into post-Stripe onboarding. +6. v1 MCP clients: **Claude** and **ChatGPT** only. First-post step is dual: MCP primary + UI fallback. + +## Non-goals (v1) + +- Hard-gating the app until onboarding is complete. +- Multi-step wizard for activation (no `/onboarding/step-*`). +- Cursor (or other MCP clients) as first-class cards. +- Requiring publish as the AHA. +- Changing the pre-Stripe ICP questions themselves (only routes/names/structure). + +## End-to-end flow + +``` +/register + → /register/success + → /welcome (redirect → /welcome/persona) + → /welcome/persona + → /welcome/goals + → /welcome/referral-source + → /welcome/subscribe (plan + Stripe CTA; no social required) + → Stripe Checkout + → /billing/processing?session_id=… + → /onboarding (single-page checklist) + → Skip or complete → /calendar +``` + +Self-hosted: skip Welcome and activation Onboarding (same policy as today’s onboarding skip → calendar). + +## URL map + +### Welcome (ICP, pre-subscription) + +| Method | Path | Name | Purpose | +|--------|------|------|---------| +| GET | `/welcome` | `app.welcome` | Redirect to `app.welcome.persona` | +| GET/POST | `/welcome/persona` | `app.welcome.persona` / `.store` | Persona | +| GET/POST | `/welcome/goals` | `app.welcome.goals` / `.store` | Goals | +| GET/POST | `/welcome/referral-source` | `app.welcome.referral-source` / `.store` | Referral | +| GET | `/welcome/subscribe` | `app.welcome.subscribe` | Plan summary + subscribe CTA | +| POST | `/welcome/checkout` | `app.welcome.checkout` | Start Stripe Checkout | + +Legacy redirects (temporary): + +- `/onboarding` → `/welcome/persona` (only while unsubscribed / for old bookmarks; after activation ships, subscribed users hitting old paths should not bounce into Welcome) +- `/onboarding/goals` → `/welcome/goals` +- `/onboarding/referral-source` → `/welcome/referral-source` +- `/onboarding/connect` → `/welcome/subscribe` + +`EnsureAccountReady`: unsubscribed users → `app.welcome.persona` (not activation `/onboarding`). + +### Onboarding (activation, post-subscription) + +| Method | Path | Name | Purpose | +|--------|------|------|---------| +| GET | `/onboarding` | `app.onboarding` | Single-page activation checklist | +| POST | `/onboarding/dismiss` | `app.onboarding.dismiss` | Skip / do later | +| POST | `/onboarding/complete` | `app.onboarding.complete` | Explicit complete when all steps done (optional if auto-complete navigates) | + +`/billing/processing` success redirect target changes from `/accounts` to `/onboarding`. + +## Code rename (Welcome) + +| Today | After | +|-------|-------| +| `OnboardingController` | `WelcomeController` | +| `app/Http/Requests/App/Onboarding/*` | `app/Http/Requests/App/Welcome/*` (`StoreWelcomePersonaRequest`, `StoreWelcomeGoalsRequest`, `StoreWelcomeReferralSourceRequest`) | +| `resources/js/pages/onboarding/{Index,Goals,ReferralSource,Connect}.vue` | `resources/js/pages/welcome/{Persona,Goals,ReferralSource,Subscribe}.vue` | +| `OnboardingLayout.vue` | `WelcomeLayout.vue` | +| `lang/*/onboarding.php` (ICP strings) | `lang/*/welcome.php` | +| `tests/Feature/Onboarding/OnboardingControllerTest.php` | `tests/Feature/Welcome/WelcomeControllerTest.php` | +| `app.onboarding.*` (ICP) | `app.welcome.*` | + +`Connect.vue` is removed from Welcome. Social connect lives on the activation page (and remains available at `/accounts`). + +Checkout no longer requires an existing social account. + +## Onboarding page (single page, not a step form) + +One dedicated full-page screen with a **3-item checklist** on the same view: + +1. **Connect an agent** — Claude and ChatGPT cards with MCP URL / deep-link / copy instructions (link to docs where needed). +2. **Connect a social network** — reuse existing connect UI patterns (platforms + connected accounts), inlined on this page. +3. **Create your first post** — primary: ready-made MCP prompt (“open Claude/ChatGPT and ask…”); fallback: button to create in TryPost UI. Draft counts. + +### Live completion + +Steps auto-check from real state (poll or Inertia partial reload): + +| Step | Done when | +|------|-----------| +| MCP | User has a non-revoked OAuth access token used for MCP (not a Personal Access Token / API key) | +| Social | Current workspace has ≥ 1 social account | +| First post | Current workspace has ≥ 1 post (any status, any `created_via`) | + +Primary CTA: continue into the app when all three are done (or celebrate in place then go to calendar). +Secondary: **Skip / I’ll do this later**. + +### Gating policy + +- **Not blocking.** After the post-checkout land, users can skip and use the product. +- No middleware that traps subscribed users on `/onboarding` on every request. +- Residual checklist in the app shell until completed or dismissed. + +## Persistence + +Welcome ICP fields stay on `User`: `persona`, `goals`, `referral_source`. + +Activation flags on `Account` (billing/subscription owner — preferred over User so workspace members don’t each own account-level activation): + +- `onboarding_completed_at` (nullable timestamp) +- `onboarding_dismissed_at` (nullable timestamp) + +Semantics: + +- Steps themselves are derived from state, not stored booleans. +- `onboarding_completed_at` set when all three steps are true (server-side on dismiss-complete path or when status is loaded and all true). +- `onboarding_dismissed_at` set on Skip. +- Residual UI shows when account is subscribed and both timestamps are null. +- Dismiss hides residual even if steps remain incomplete. +- Completing all three sets `completed_at` and clears residual. + +Existing accounts already subscribed: treat as dismissed or completed (migration strategy: backfill `onboarding_dismissed_at = now()` for accounts that already have an active subscription, so we don’t force legacy users into activation). + +## Residual checklist + +Dismissible banner or compact checklist in the authenticated app layout (not a modal trap): + +- Visible when subscribed + `completed_at` null + `dismissed_at` null. +- Link back to `/onboarding`. +- Dismiss action hits `app.onboarding.dismiss`. + +Exact UI placement (sidebar vs top banner) is an implementation detail; prefer the lightest pattern that matches existing app chrome. + +## Analytics (PostHog) + +Follow existing enum pattern under `App\Enums\PostHog\` (e.g. `WelcomeEvent`, `OnboardingEvent`). + +Suggested events: + +- Welcome: `welcome.persona_saved`, `welcome.goals_saved`, `welcome.referral_saved`, `welcome.checkout_started` +- Onboarding: `onboarding.viewed`, `onboarding.step_completed` (property `step`: `mcp` | `social` | `first_post`), `onboarding.skipped`, `onboarding.completed` + +Step properties when useful: `created_via` (`web` | `mcp`) for first post; MCP client if detectable later (v1 may omit client if unreliable). + +## Architecture notes + +- **WelcomeController** — ICP + subscribe + checkout only; redirects subscribed users to `/onboarding` or calendar per rules (subscribed + activation incomplete → can visit `/onboarding`; subscribed + complete/dismissed → calendar). +- **OnboardingController** — show checklist props (step statuses, platforms, accounts, MCP server URL, sample prompt), dismiss, complete. +- Prefer a small action/service e.g. `App\Actions\Onboarding\ResolveOnboardingStatus` that returns the three booleans + whether residual should show — shared by page, layout shared props, and tests. +- Frontend: one `resources/js/pages/onboarding/Index.vue`; poll step status while the page is open so MCP/OAuth and MCP-created drafts flip checkmarks without a full navigation. +- Wayfinder regenerate after route/controller rename. +- i18n: all new copy in `lang/*/welcome.php` and `lang/*/onboarding.php` (all locales that already have onboarding strings). + +## Testing + +- Welcome: renamed feature tests; persona → goals → referral → subscribe; checkout **without** social account succeeds; subscribed users don’t see Welcome ICP. +- Legacy route redirects. +- Billing processing redirects to `/onboarding`. +- Onboarding page renders step statuses from fixtures (MCP token, social, post). +- Dismiss sets `onboarding_dismissed_at`; residual hidden. +- Auto-complete when all three true sets `onboarding_completed_at`. +- `EnsureAccountReady` → `/welcome/persona` when unsubscribed. +- Self-hosted skips both flows. +- Residual shared prop / layout behavior covered at least once. + +## Open implementation details (resolved in plan, not product) + +- Exact MCP “connected” query against Passport tokens/clients. +- Claude/ChatGPT deep-link vs copy-paste URL UX copy. +- Residual component placement in `AppLayout`. +- Whether `onboarding.complete` is a dedicated POST or automatic on status resolve. + +## Success criteria + +- New paid users land on `/onboarding` after Stripe, not `/accounts`. +- They can connect Claude or ChatGPT, connect a network, and create a draft (via MCP or UI) on one page. +- They can skip and still use the product, with an optional residual nudge. +- Pre-Stripe flow lives under `/welcome/{step}` with aligned PHP/Vue naming. +- ICP collection no longer requires social connect before payment. diff --git a/app/Actions/Billing/StartSubscriptionCheckout.php b/app/Actions/Billing/StartSubscriptionCheckout.php index 949defc1b..bbcb1a1f6 100644 --- a/app/Actions/Billing/StartSubscriptionCheckout.php +++ b/app/Actions/Billing/StartSubscriptionCheckout.php @@ -5,7 +5,7 @@ namespace App\Actions\Billing; use App\Models\Account; -use App\Support\Billing\FirstMonthCheckoutDiscount; +use App\Support\Billing\ConfigureSubscriptionCheckout; use Inertia\Inertia; use Symfony\Component\HttpFoundation\Response; @@ -13,9 +13,8 @@ class StartSubscriptionCheckout { /** * Create a Stripe Checkout session for the given price and return an Inertia - * redirect to it. Quantity tracks the account's workspace count; the first - * month's coupon is applied when the instance requires a card up front, so - * the first invoice charges $1 instead of running a $0 trial authorization. + * redirect to it. Quantity tracks the account's workspace count. Trial days + * and promotion codes come from Cashier env config. */ public function redirect(Account $account, string $priceId, string $cancelUrl): Response { @@ -27,7 +26,7 @@ public function redirect(Account $account, string $priceId, string $cancelUrl): $subscription = $account->newSubscription(Account::SUBSCRIPTION_NAME, $priceId) ->quantity(max(1, $account->workspaces()->count())); - FirstMonthCheckoutDiscount::apply($subscription, $account); + ConfigureSubscriptionCheckout::apply($subscription, $account); $session = $subscription->checkout([ 'success_url' => route('app.billing.processing').'?session_id={CHECKOUT_SESSION_ID}', diff --git a/app/Actions/Onboarding/ResolveOnboardingStatus.php b/app/Actions/Onboarding/ResolveOnboardingStatus.php new file mode 100644 index 000000000..0e63a2337 --- /dev/null +++ b/app/Actions/Onboarding/ResolveOnboardingStatus.php @@ -0,0 +1,258 @@ +account; + + if ($account?->onboarding_completed_at !== null) { + return [ + 'mcp_connected' => true, + 'social_connected' => true, + 'first_post_created' => true, + 'all_complete' => true, + 'show_residual' => false, + 'completed_at' => $account->onboarding_completed_at->toIso8601String(), + 'dismissed_at' => $account->onboarding_dismissed_at?->toIso8601String(), + ]; + } + + // All three steps are account-scoped so checklist checkmarks, residual + // progress, and cross-workspace completion stay aligned. + $mcpConnected = $this->accountHasMcpConnection($account); + $socialConnected = $this->accountHasSocialConnection($account); + $firstPostCreated = $this->accountHasPost($account); + $allComplete = $mcpConnected && $socialConnected && $firstPostCreated; + + $showResidual = ! config('trypost.self_hosted') + && $user->isAccountOwner() + && ($account?->hasAppAccess() ?? false) + && $account->onboarding_dismissed_at === null + && ! $allComplete; + + return [ + 'mcp_connected' => $mcpConnected, + 'social_connected' => $socialConnected, + 'first_post_created' => $firstPostCreated, + 'all_complete' => $allComplete, + 'show_residual' => $showResidual, + 'completed_at' => null, + 'dismissed_at' => $account?->onboarding_dismissed_at?->toIso8601String(), + ]; + } + + /** + * Read checklist state, capture step analytics, and stamp completion when done. + * Call from intentional onboarding surfaces — not from shared Inertia props. + * + * @return array{ + * mcp_connected: bool, + * social_connected: bool, + * first_post_created: bool, + * all_complete: bool, + * show_residual: bool, + * completed_at: ?string, + * dismissed_at: ?string + * } + */ + public function syncProgress(User $user): array + { + $status = $this->handle($user); + $account = $user->account; + + if ($account === null || $account->onboarding_completed_at !== null) { + return $status; + } + + if ($account->onboarding_dismissed_at === null) { + $this->captureCompletedStep($user, $account, 'mcp_connected', $status['mcp_connected']); + $this->captureCompletedStep($user, $account, 'social_connected', $status['social_connected']); + $this->captureCompletedStep($user, $account, 'first_post_created', $status['first_post_created']); + } + + if ($account->onboarding_dismissed_at !== null) { + return $status; + } + + if ($status['all_complete']) { + $this->markCompleted($user); + $account->refresh(); + + return [ + ...$status, + 'show_residual' => false, + 'completed_at' => $account->onboarding_completed_at?->toIso8601String(), + ]; + } + + return $status; + } + + /** + * Stamp account onboarding completion once and capture the funnel event. + */ + public function markCompleted(User $user): bool + { + $account = $user->account; + + if ( + $account === null + || $account->onboarding_completed_at !== null + || $account->onboarding_dismissed_at !== null + ) { + return false; + } + + $completedAt = now(); + + $updated = Account::query() + ->whereKey($account->id) + ->whereNull('onboarding_completed_at') + ->whereNull('onboarding_dismissed_at') + ->update(['onboarding_completed_at' => $completedAt]); + + if ($updated === 0) { + return false; + } + + $account->forceFill(['onboarding_completed_at' => $completedAt]); + $account->syncOriginalAttribute('onboarding_completed_at'); + + if (PostHogService::isEnabled()) { + $this->postHog->capture( + $user->id, + OnboardingEvent::Completed->value, + account: $account, + ); + } + + // Every stamp path (endpoint, syncProgress, observers) must clear residual + // banners account-wide — not only the explicit complete() action. + OnboardingStatusUpdated::broadcastForAccount($account); + + // Lets the next full Inertia visit (e.g. OAuth popup → router.reload) + // show celebration instead of bouncing straight to calendar. + if (request()->hasSession()) { + request()->session()->flash('onboarding_just_completed', true); + } + + return true; + } + + /** + * Sidebar residual banner payload, or false when the banner should not show. + * + * Pure read — safe for Inertia shared props and prefetch. Cross-workspace + * completion is stamped by syncProgress / observers, not here. + * + * @return array{completed: int, total: int}|false + */ + public function residual(User $user): array|false + { + $status = $this->handle($user); + + if (! $status['show_residual']) { + return false; + } + + return [ + 'completed' => collect([ + $status['mcp_connected'], + $status['social_connected'], + $status['first_post_created'], + ])->filter()->count(), + 'total' => self::TOTAL_STEPS, + ]; + } + + private function accountHasMcpConnection(?Account $account): bool + { + if ($account === null) { + return false; + } + + return AccessToken::query() + ->whereIn( + 'user_id', + User::query()->select('id')->where('account_id', $account->id), + ) + ->activeMcpOAuth() + ->exists(); + } + + private function accountHasSocialConnection(?Account $account): bool + { + if ($account === null) { + return false; + } + + return Workspace::query() + ->where('account_id', $account->id) + ->whereHas('socialAccounts') + ->exists(); + } + + private function accountHasPost(?Account $account): bool + { + if ($account === null) { + return false; + } + + return Workspace::query() + ->where('account_id', $account->id) + ->whereHas('posts') + ->exists(); + } + + private function captureCompletedStep(User $user, Account $account, string $step, bool $completed): void + { + if (! $completed || ! PostHogService::isEnabled()) { + return; + } + + // Durable once-per-account dedupe (no short TTL re-fire). + if (! Cache::add("onboarding_step:{$account->id}:{$step}", true, now()->addYears(100))) { + return; + } + + $this->postHog->capture( + $user->id, + OnboardingEvent::StepCompleted->value, + ['step' => $step], + $account, + ); + } +} diff --git a/app/Actions/User/CreateUser.php b/app/Actions/User/CreateUser.php index ead5eca04..c03276e0d 100644 --- a/app/Actions/User/CreateUser.php +++ b/app/Actions/User/CreateUser.php @@ -12,6 +12,7 @@ use App\Models\User; use App\Services\PostHogService; use Illuminate\Support\Facades\DB; +use RuntimeException; class CreateUser { @@ -30,8 +31,17 @@ public static function execute(array $data, array $utmParameters = []): User ]; if (! $requiresCardForTrial) { + $trialDays = (int) config('cashier.trial_days'); + + if ($trialDays < 1) { + throw new RuntimeException( + 'CASHIER_TRIAL_DAYS must be at least 1 when REQUIRE_CARD_FOR_TRIAL=false, ' + .'otherwise new accounts would have no trial and no subscription access.' + ); + } + $accountAttributes['plan_id'] = Plan::where('slug', Slug::Workspace)->value('id'); - $accountAttributes['trial_ends_at'] = now()->addDays(config('cashier.trial_days')); + $accountAttributes['trial_ends_at'] = now()->addDays($trialDays); } $account = Account::create($accountAttributes); diff --git a/app/Enums/PostHog/OnboardingEvent.php b/app/Enums/PostHog/OnboardingEvent.php new file mode 100644 index 000000000..ff72b671f --- /dev/null +++ b/app/Enums/PostHog/OnboardingEvent.php @@ -0,0 +1,13 @@ + + */ + public static function connectableOptions(): array + { + return collect(self::cases()) + ->filter(fn (self $platform): bool => $platform->isConnectable()) + ->map(fn (self $platform): array => [ + 'value' => $platform->value, + 'label' => $platform->label(), + 'color' => $platform->color(), + 'network' => $platform->network(), + ]) + ->values() + ->all(); + } + /** * Static, platform-specific data exposed to the frontend (e.g. TikTok privacy options, * compliance URLs). Returns an empty array for platforms with no extra config. diff --git a/app/Enums/User/Goal.php b/app/Enums/User/Goal.php index ced8c382e..836e4bd13 100644 --- a/app/Enums/User/Goal.php +++ b/app/Enums/User/Goal.php @@ -13,9 +13,6 @@ enum Goal: string case GrowAudience = 'grow_audience'; case DriveSales = 'drive_sales'; case ManageClients = 'manage_clients'; - case TeamCollaboration = 'team_collaboration'; - case AutomateApi = 'automate_api'; - case TrackPerformance = 'track_performance'; case JustExploring = 'just_exploring'; case Other = 'other'; } diff --git a/app/Events/OnboardingStatusUpdated.php b/app/Events/OnboardingStatusUpdated.php new file mode 100644 index 000000000..3eb50269d --- /dev/null +++ b/app/Events/OnboardingStatusUpdated.php @@ -0,0 +1,216 @@ +workspaces()->pluck('id') as $workspaceId) { + static::dispatch((string) $workspaceId); + } + } + + /** + * Broadcast to every workspace on the account and sync progress for the actor. + * Use when a step is account-scoped (e.g. MCP OAuth). + * + * Sync/analytics run afterCommit so Cache/PostHog never outlive a rolled-back + * CreatePost (or similar) transaction. + */ + public static function dispatchForAccount(?Account $account, ?User $actor = null): void + { + if ($account === null + || $account->onboarding_completed_at !== null + || $account->onboarding_dismissed_at !== null + ) { + return; + } + + $accountId = $account->id; + $actorId = $actor?->id; + + DB::afterCommit(function () use ($accountId, $actorId): void { + $account = Account::query()->find($accountId); + + if ($account === null + || $account->onboarding_completed_at !== null + || $account->onboarding_dismissed_at !== null + ) { + return; + } + + $workspaceIds = $account->workspaces()->pluck('id'); + + foreach ($workspaceIds as $workspaceId) { + static::dispatch((string) $workspaceId); + } + + $resolver = app(ResolveOnboardingStatus::class); + $actor = $actorId !== null + ? User::query()->with(['account', 'currentWorkspace'])->find($actorId) + : null; + + if ($actor !== null) { + // Steps are account-scoped — syncProgress stamps when MCP + social + // + post exist anywhere on the account. + $resolver->syncProgress($actor); + } + }); + } + + /** + * Broadcast only while the workspace account still has active onboarding. + * Sync the actor first (correct PostHog attribution), then any other users + * currently on this workspace so completion can stamp if they are ready. + * + * Sync/analytics run afterCommit so Cache/PostHog never outlive a rolled-back + * CreatePost (or similar) transaction. + */ + public static function dispatchForWorkspace(?string $workspaceId, ?User $actor = null): void + { + if (blank($workspaceId)) { + return; + } + + $account = Workspace::query()->find($workspaceId)?->account; + + if ($account === null + || $account->onboarding_completed_at !== null + || $account->onboarding_dismissed_at !== null + ) { + return; + } + + $actorId = $actor?->id; + + DB::afterCommit(function () use ($workspaceId, $actorId): void { + $account = Workspace::query()->find($workspaceId)?->account; + + if ($account === null + || $account->onboarding_completed_at !== null + || $account->onboarding_dismissed_at !== null + ) { + return; + } + + static::dispatch($workspaceId); + + $resolver = app(ResolveOnboardingStatus::class); + $syncedActor = false; + $actor = $actorId !== null + ? User::query()->with(['account', 'currentWorkspace'])->find($actorId) + : null; + + if ($actor !== null) { + // API/MCP tokens often mutate the request user's workspace in memory + // only — align the actor to this workspace for checklist resolution. + if ((string) $actor->current_workspace_id !== (string) $workspaceId) { + $workspace = Workspace::query()->find($workspaceId); + + if ($workspace !== null) { + $actor->setRelation('currentWorkspace', $workspace); + $actor->current_workspace_id = $workspace->id; + } + } + + if ((string) $actor->current_workspace_id === (string) $workspaceId) { + $resolver->syncProgress($actor); + $account->refresh(); + $syncedActor = true; + + if ($account->onboarding_completed_at !== null) { + return; + } + } + } + + User::query() + ->with(['account', 'currentWorkspace']) + ->where('current_workspace_id', $workspaceId) + ->where('account_id', $account->id) + ->when( + $syncedActor && $actor !== null, + fn ($query) => $query->whereKeyNot($actor->id), + ) + ->each(function (User $user) use ($resolver, $account): void { + if ($account->onboarding_completed_at !== null) { + return; + } + + $resolver->syncProgress($user); + $account->refresh(); + }); + + // Fan out Echo to sibling workspaces so residual progress updates + // everywhere (sync already ran for users on this workspace). + $account->refresh(); + + if ($account->onboarding_completed_at === null + && $account->onboarding_dismissed_at === null + ) { + $account->workspaces() + ->whereKeyNot($workspaceId) + ->pluck('id') + ->each(fn ($siblingId) => static::dispatch((string) $siblingId)); + } + }); + } + + public function broadcastAs(): string + { + return 'onboarding.status.updated'; + } + + /** + * @return array + */ + public function broadcastOn(): array + { + return [ + new PrivateChannel("workspace.{$this->workspaceId}"), + ]; + } + + /** + * @return array{workspace_id: string} + */ + public function broadcastWith(): array + { + return [ + 'workspace_id' => $this->workspaceId, + ]; + } + + public function broadcastQueue(): string + { + return 'broadcasts'; + } +} diff --git a/app/Http/Controllers/App/BillingController.php b/app/Http/Controllers/App/BillingController.php index e85d36c5d..82a9a16ee 100644 --- a/app/Http/Controllers/App/BillingController.php +++ b/app/Http/Controllers/App/BillingController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\App; use App\Models\Account; +use App\Support\Billing\CheckoutConversionData; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; @@ -18,7 +19,7 @@ class BillingController extends Controller { public function subscribe(): RedirectResponse { - return redirect()->route('app.onboarding'); + return redirect()->route('app.welcome.persona'); } public function processing(Request $request): Response|RedirectResponse @@ -40,6 +41,9 @@ public function processing(Request $request): Response|RedirectResponse return Inertia::render('billing/Processing', [ 'subscriptionActive' => $account && $account->subscribed(Account::SUBSCRIPTION_NAME), 'fromCheckout' => $fromCheckout, + 'redirectToOnboarding' => $account !== null + && $account->onboarding_completed_at === null + && $account->onboarding_dismissed_at === null, 'persona' => $request->user()->persona?->value, 'conversion' => $fromCheckout && $account?->stripe_id ? fn () => $this->buildConversionData($account, $sessionId) @@ -55,29 +59,12 @@ private function buildConversionData(Account $account, string $sessionId): ?arra try { $session = $account->stripe()->checkout->sessions->retrieve( $sessionId, - ['expand' => ['line_items.data.price']], ); } catch (Throwable) { return null; } - if (data_get($session, 'customer') !== $account->stripe_id) { - return null; - } - - $unitAmount = data_get($session, 'line_items.data.0.price.unit_amount'); - $currency = data_get($session, 'line_items.data.0.price.currency'); - $transactionId = data_get($session, 'id'); - - if (! is_int($unitAmount) || ! is_string($currency) || ! is_string($transactionId)) { - return null; - } - - return [ - 'value' => $unitAmount / 100, - 'currency' => strtoupper($currency), - 'transaction_id' => $transactionId, - ]; + return CheckoutConversionData::fromSession($session, (string) $account->stripe_id); } public function index(Request $request): Response|RedirectResponse diff --git a/app/Http/Controllers/App/McpSettingsController.php b/app/Http/Controllers/App/McpSettingsController.php new file mode 100644 index 000000000..282f876b2 --- /dev/null +++ b/app/Http/Controllers/App/McpSettingsController.php @@ -0,0 +1,117 @@ +user()->currentWorkspace; + + if (! $workspace) { + return redirect()->route('app.workspaces.create'); + } + + $this->authorize('view', $workspace); + + return Inertia::render('settings/workspace/Mcp', [ + 'mcpUrl' => route('mcp.trypost'), + 'mcpClients' => collect(config('trypost.mcp.clients', [])) + ->map(fn (array $client, string $id): array => [ + 'id' => $id, + 'label' => (string) data_get($client, 'label'), + 'logo' => (string) data_get($client, 'logo'), + 'settings_url' => (string) data_get($client, 'settings_url'), + ]) + ->values() + ->all(), + 'connectedClients' => $this->connectedClients($workspace->account_id, $request->user()), + ]); + } + + public function disconnect(Request $request, string $client): RedirectResponse + { + $workspace = $request->user()->currentWorkspace; + + if (! $workspace) { + return redirect()->route('app.workspaces.create'); + } + + $this->authorize('view', $workspace); + + $user = $request->user(); + + $tokens = AccessToken::query() + ->where('user_id', $user->id) + ->where('client_id', $client) + ->activeMcpOAuth() + ->get(); + + if ($tokens->isEmpty()) { + return back(); + } + + DB::transaction(function () use ($tokens): void { + $tokenIds = $tokens->pluck('id'); + + DB::table('oauth_refresh_tokens')->whereIn('access_token_id', $tokenIds)->update(['revoked' => true]); + + $tokens->each(function (AccessToken $token): void { + $token->forceFill(['revoked' => true])->saveQuietly(); + }); + }); + + OnboardingStatusUpdated::dispatchForAccount($user->account, $user); + + return back()->with('flash.success', __('mcp.disconnected')); + } + + /** + * Active OAuth MCP grants across the account (any teammate), excluding + * personal access API tokens. + * + * @return array + */ + private function connectedClients(string $accountId, User $currentUser): array + { + $tokens = AccessToken::query() + ->whereIn('user_id', User::query()->select('id')->where('account_id', $accountId)) + ->activeMcpOAuth() + ->with('client') + ->get(); + + $users = User::query() + ->whereIn('id', $tokens->pluck('user_id')->unique()->filter()->all()) + ->get() + ->keyBy('id'); + + return $tokens + ->groupBy(fn (AccessToken $token): string => "{$token->client_id}:{$token->user_id}") + ->map(function ($grouped) use ($currentUser, $users): array { + $first = $grouped->first(); + $owner = $users->get($first->user_id); + + return [ + 'client_id' => $first->client_id, + 'name' => $first->client->name, + 'user_id' => (string) $first->user_id, + 'user_name' => (string) ($owner?->name ?? ''), + 'can_disconnect' => $first->user_id === $currentUser->id, + 'last_used_at' => $grouped->max('last_used_at'), + ]; + }) + ->values() + ->all(); + } +} diff --git a/app/Http/Controllers/App/OnboardingController.php b/app/Http/Controllers/App/OnboardingController.php index a9628a224..a5cc2fdd0 100644 --- a/app/Http/Controllers/App/OnboardingController.php +++ b/app/Http/Controllers/App/OnboardingController.php @@ -4,18 +4,12 @@ namespace App\Http\Controllers\App; -use App\Actions\Billing\StartSubscriptionCheckout; -use App\Enums\Plan\Slug; +use App\Actions\Onboarding\ResolveOnboardingStatus; +use App\Enums\PostHog\OnboardingEvent; use App\Enums\SocialAccount\Platform as SocialPlatform; -use App\Enums\User\Goal; -use App\Enums\User\Persona; -use App\Enums\User\ReferralSource; -use App\Http\Requests\App\Onboarding\StoreOnboardingGoalsRequest; -use App\Http\Requests\App\Onboarding\StoreOnboardingReferralSourceRequest; -use App\Http\Requests\App\Onboarding\StoreOnboardingRequest; +use App\Events\OnboardingStatusUpdated; use App\Http\Resources\App\SocialAccountResource; use App\Models\Account; -use App\Models\Plan; use App\Services\PostHogService; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -25,233 +19,155 @@ class OnboardingController extends Controller { + public function __construct( + private readonly ResolveOnboardingStatus $resolveOnboardingStatus, + private readonly PostHogService $postHog, + ) {} + public function index(Request $request): Response|RedirectResponse { - if (config('trypost.self_hosted')) { - return redirect()->route('app.calendar'); + if ($redirect = $this->redirectIfSelfHosted()) { + return $redirect; } $user = $request->user(); + $workspace = $user->currentWorkspace; + // Capture before syncProgress — same-request auto-stamp still shows celebration. + $wasAlreadyComplete = $user->account?->onboarding_completed_at !== null; + $status = $this->resolveOnboardingStatus->syncProgress($user); - if ($user->account?->subscribed(Account::SUBSCRIPTION_NAME)) { + // Skip is always terminal (including Echo partial reloads). + if ($status['dismissed_at'] !== null) { return redirect()->route('app.calendar'); } - return Inertia::render('onboarding/Index', [ - 'personas' => array_map(fn (Persona $persona): string => $persona->value, Persona::cases()), - 'selected' => $user->persona?->value, - ]); - } + // Completed revisits leave — keep celebration for Echo/poll partials and + // for the immediate full reload after OAuth stamps completion. + // Never pull the flash on partials: poll/Echo would consume it before the + // post-OAuth full reload and bounce the user to calendar. + $isPartial = $request->hasHeader('X-Inertia-Partial-Component'); + $justCompleted = ! $isPartial + && (bool) $request->session()->pull('onboarding_just_completed'); - public function store(StoreOnboardingRequest $request, PostHogService $postHog): RedirectResponse - { - if (config('trypost.self_hosted')) { + if ($wasAlreadyComplete && ! $justCompleted && ! $isPartial) { return redirect()->route('app.calendar'); } - $user = $request->user(); - - if ($user->account?->subscribed(Account::SUBSCRIPTION_NAME)) { - return redirect()->route('app.calendar'); + if (! $isPartial && ! $wasAlreadyComplete) { + $this->postHog->capture( + $user->id, + OnboardingEvent::Viewed->value, + account: $user->account, + ); } - $persona = (string) $request->validated('persona'); - - $user->update(['persona' => $persona]); - - $postHog->identify($user->id, [ - 'persona' => $persona, + return Inertia::render('onboarding/Index', [ + 'status' => $status, + 'canDismiss' => $user->isAccountOwner(), + 'mcpUrl' => route('mcp.trypost'), + 'mcpClients' => collect(config('trypost.mcp.clients', [])) + ->map(fn (array $client, string $id): array => [ + 'id' => $id, + 'label' => (string) data_get($client, 'label'), + 'logo' => (string) data_get($client, 'logo'), + 'settings_url' => (string) data_get($client, 'settings_url'), + ]) + ->values() + ->all(), + 'samplePrompt' => __('onboarding.first_post.sample_prompt'), + 'platforms' => SocialPlatform::connectableOptions(), + 'accounts' => SocialAccountResource::collection( + $workspace->socialAccounts()->orderBy('id')->get(), + )->resolve(), ]); - - return redirect()->route('app.onboarding.goals'); } - public function goals(Request $request): Response|RedirectResponse + public function dismiss(Request $request): RedirectResponse { - if (config('trypost.self_hosted')) { - return redirect()->route('app.calendar'); + if ($redirect = $this->redirectIfSelfHosted()) { + return $redirect; } $user = $request->user(); - if ($user->account?->subscribed(Account::SUBSCRIPTION_NAME)) { - return redirect()->route('app.calendar'); - } + abort_unless($user->isAccountOwner(), SymfonyResponse::HTTP_FORBIDDEN); - if (! $user->persona) { - return redirect()->route('app.onboarding'); - } - - return Inertia::render('onboarding/Goals', [ - 'goals' => array_map(fn (Goal $goal): string => $goal->value, Goal::cases()), - 'selected' => $user->goals ?? [], - ]); - } - - public function storeGoals(StoreOnboardingGoalsRequest $request, PostHogService $postHog): RedirectResponse - { - if (config('trypost.self_hosted')) { - return redirect()->route('app.calendar'); - } - - $user = $request->user(); + $account = $user->account; - if ($user->account?->subscribed(Account::SUBSCRIPTION_NAME)) { + if ( + $account === null + || $account->onboarding_completed_at !== null + || $account->onboarding_dismissed_at !== null + ) { return redirect()->route('app.calendar'); } - if (! $user->persona) { - return redirect()->route('app.onboarding'); - } + $dismissedAt = now(); - $goals = array_values($request->validated('goals')); + $updated = Account::query() + ->whereKey($account->id) + ->whereNull('onboarding_completed_at') + ->whereNull('onboarding_dismissed_at') + ->update(['onboarding_dismissed_at' => $dismissedAt]); - $user->update(['goals' => $goals]); - - $postHog->identify($user->id, [ - 'goals' => $goals, - ]); - - return redirect()->route('app.onboarding.referral-source'); - } - - public function referralSource(Request $request): Response|RedirectResponse - { - if (config('trypost.self_hosted')) { + if ($updated === 0) { return redirect()->route('app.calendar'); } - $user = $request->user(); - - if ($user->account?->subscribed(Account::SUBSCRIPTION_NAME)) { - return redirect()->route('app.calendar'); - } + $account->forceFill(['onboarding_dismissed_at' => $dismissedAt]); + $account->syncOriginalAttribute('onboarding_dismissed_at'); - if (! $user->persona) { - return redirect()->route('app.onboarding'); - } + $this->postHog->capture( + $user->id, + OnboardingEvent::Skipped->value, + account: $account, + ); - if (! $user->goals) { - return redirect()->route('app.onboarding.goals'); - } + // Refresh residual banners on other tabs/devices immediately. + OnboardingStatusUpdated::broadcastForAccount($account); - return Inertia::render('onboarding/ReferralSource', [ - 'sources' => array_map(fn (ReferralSource $source): string => $source->value, ReferralSource::cases()), - 'selected' => $user->referral_source?->value, - ]); + return redirect()->route('app.calendar'); } - public function storeReferralSource(StoreOnboardingReferralSourceRequest $request, PostHogService $postHog): RedirectResponse + public function complete(Request $request): RedirectResponse { - if (config('trypost.self_hosted')) { - return redirect()->route('app.calendar'); + if ($redirect = $this->redirectIfSelfHosted()) { + return $redirect; } $user = $request->user(); + $account = $user->account; - if ($user->account?->subscribed(Account::SUBSCRIPTION_NAME)) { + // Already stamped (e.g. observer / syncProgress auto-complete) — just leave. + if ($account?->onboarding_completed_at !== null) { return redirect()->route('app.calendar'); } - if (! $user->persona) { - return redirect()->route('app.onboarding'); - } - - if (! $user->goals) { - return redirect()->route('app.onboarding.goals'); - } - - $referralSource = (string) $request->validated('referral_source'); - - $user->update(['referral_source' => $referralSource]); - - $postHog->identify($user->id, [ - 'referral_source' => $referralSource, - ]); - - return redirect()->route('app.onboarding.connect'); - } - - public function connect(Request $request): Response|RedirectResponse - { - if (config('trypost.self_hosted')) { + // Skip must stay terminal: never let Continue re-open completion after dismiss. + if ($account?->onboarding_dismissed_at !== null) { return redirect()->route('app.calendar'); } - $user = $request->user(); - - if ($user->account?->subscribed(Account::SUBSCRIPTION_NAME)) { - return redirect()->route('app.calendar'); - } + // Any teammate who finishes activation may stamp — observers/syncProgress + // already do the same. Dismiss remains owner-only. Steps are account-scoped. + $status = $this->resolveOnboardingStatus->handle($user); - if (! $user->persona) { + if (! $status['all_complete']) { return redirect()->route('app.onboarding'); } - if (! $user->goals) { - return redirect()->route('app.onboarding.goals'); - } - - if (! $user->referral_source) { - return redirect()->route('app.onboarding.referral-source'); - } + // markCompleted broadcasts account-wide so residual banners clear immediately. + $this->resolveOnboardingStatus->markCompleted($user); - $workspace = $user->currentWorkspace; - - if (! $workspace) { - return redirect()->route('app.workspaces.create'); - } - - $accounts = $workspace->socialAccounts()->orderBy('id')->get(); - - $platforms = collect(SocialPlatform::cases()) - ->filter(fn (SocialPlatform $platform): bool => $platform->isConnectable()) - ->map(fn (SocialPlatform $platform): array => [ - 'value' => $platform->value, - 'label' => $platform->label(), - 'color' => $platform->color(), - 'network' => $platform->network(), - ])->values(); - - $plan = Plan::where('slug', Slug::Workspace)->firstOrFail(); - - return Inertia::render('onboarding/Connect', [ - 'platforms' => $platforms, - 'accounts' => SocialAccountResource::collection($accounts)->resolve(), - 'plan' => [ - 'name' => $plan->name, - 'interval' => 'monthly', - ], - ]); + return redirect()->route('app.calendar'); } - public function checkout(Request $request, StartSubscriptionCheckout $checkout): SymfonyResponse|RedirectResponse + private function redirectIfSelfHosted(): ?RedirectResponse { - if (config('trypost.self_hosted')) { - return redirect()->route('app.calendar'); + if (! config('trypost.self_hosted')) { + return null; } - $user = $request->user(); - $account = $user->account; - - if ($account?->subscribed(Account::SUBSCRIPTION_NAME)) { - return redirect()->route('app.calendar'); - } - - $workspace = $user->currentWorkspace; - - if (! $workspace || ! $workspace->socialAccounts()->exists()) { - return redirect()->route('app.onboarding.connect') - ->with('flash.banner', __('onboarding.connect.must_connect')) - ->with('flash.bannerStyle', 'danger'); - } - - $plan = Plan::where('slug', Slug::Workspace)->firstOrFail(); - - return $checkout->redirect( - $account, - (string) $plan->stripe_monthly_price_id, - route('app.onboarding.connect'), - ); + return redirect()->route('app.calendar'); } } diff --git a/app/Http/Controllers/App/WelcomeController.php b/app/Http/Controllers/App/WelcomeController.php new file mode 100644 index 000000000..c551b8445 --- /dev/null +++ b/app/Http/Controllers/App/WelcomeController.php @@ -0,0 +1,210 @@ +redirectIfUnavailable($request)) { + return $redirect; + } + + return Inertia::render('welcome/Persona', [ + 'personas' => array_map(fn (Persona $persona): string => $persona->value, Persona::cases()), + 'selected' => $request->user()->persona?->value, + ]); + } + + public function storePersona(StoreWelcomePersonaRequest $request, PostHogService $postHog): RedirectResponse + { + if ($redirect = $this->redirectIfUnavailable($request)) { + return $redirect; + } + + $user = $request->user(); + $persona = (string) $request->validated('persona'); + + $user->update(['persona' => $persona]); + + $postHog->identify($user->id, [ + 'persona' => $persona, + ]); + $postHog->capture( + $user->id, + WelcomeEvent::PersonaSaved->value, + ['persona' => $persona], + $user->account, + ); + + return redirect()->route('app.welcome.goals'); + } + + public function goals(Request $request): Response|RedirectResponse + { + if ($redirect = $this->redirectIfStepIncomplete($request)) { + return $redirect; + } + + $user = $request->user(); + + return Inertia::render('welcome/Goals', [ + 'goals' => array_map(fn (Goal $goal): string => $goal->value, Goal::cases()), + 'selected' => $user->goals ?? [], + ]); + } + + public function storeGoals(StoreWelcomeGoalsRequest $request, PostHogService $postHog): RedirectResponse + { + if ($redirect = $this->redirectIfStepIncomplete($request)) { + return $redirect; + } + + $user = $request->user(); + $goals = array_values($request->validated('goals')); + + $user->update(['goals' => $goals]); + + $postHog->identify($user->id, [ + 'goals' => $goals, + ]); + $postHog->capture( + $user->id, + WelcomeEvent::GoalsSaved->value, + ['goals' => $goals], + $user->account, + ); + + return redirect()->route('app.welcome.referral-source'); + } + + public function referralSource(Request $request): Response|RedirectResponse + { + if ($redirect = $this->redirectIfStepIncomplete($request, requireGoals: true)) { + return $redirect; + } + + $user = $request->user(); + $plan = Plan::where('slug', Slug::Workspace)->firstOrFail(); + + return Inertia::render('welcome/ReferralSource', [ + 'sources' => array_map(fn (ReferralSource $source): string => $source->value, ReferralSource::cases()), + 'selected' => $user->referral_source?->value, + 'canCheckout' => $user->isAccountOwner(), + 'plan' => [ + 'name' => $plan->name, + 'interval' => 'monthly', + ], + ]); + } + + public function storeReferralSource( + StoreWelcomeReferralSourceRequest $request, + StartSubscriptionCheckout $checkout, + PostHogService $postHog, + ): SymfonyResponse|RedirectResponse { + if ($redirect = $this->redirectIfStepIncomplete($request, requireGoals: true)) { + return $redirect; + } + + $user = $request->user(); + + abort_unless($user->isAccountOwner(), SymfonyResponse::HTTP_FORBIDDEN); + + $referralSource = (string) $request->validated('referral_source'); + + $user->update(['referral_source' => $referralSource]); + + $postHog->identify($user->id, [ + 'referral_source' => $referralSource, + ]); + $postHog->capture( + $user->id, + WelcomeEvent::ReferralSaved->value, + ['referral_source' => $referralSource], + $user->account, + ); + + $plan = Plan::where('slug', Slug::Workspace)->firstOrFail(); + + $response = $checkout->redirect( + $user->account, + (string) $plan->stripe_monthly_price_id, + route('app.welcome.referral-source'), + ); + + $postHog->capture( + $user->id, + WelcomeEvent::CheckoutStarted->value, + [ + 'plan_name' => $plan->name, + 'interval' => 'monthly', + ], + $user->account, + ); + + return $response; + } + + private function redirectIfStepIncomplete(Request $request, bool $requireGoals = false): ?RedirectResponse + { + if ($redirect = $this->redirectIfUnavailable($request)) { + return $redirect; + } + + $user = $request->user(); + + if (! $user->persona) { + return redirect()->route('app.welcome.persona'); + } + + if ($requireGoals && ! $user->goals) { + return redirect()->route('app.welcome.goals'); + } + + return null; + } + + private function redirectIfUnavailable(Request $request): ?RedirectResponse + { + if (config('trypost.self_hosted')) { + return redirect()->route('app.calendar'); + } + + $user = $request->user(); + + // Match EnsureAccountReady — generic-trial (no-card) users already have + // app access and must not be sent through Stripe checkout again. + if ($user->account?->hasAppAccess()) { + $status = $this->resolveOnboardingStatus->handle($user); + + return redirect()->route($status['show_residual'] ? 'app.onboarding' : 'app.calendar'); + } + + return null; + } +} diff --git a/app/Http/Controllers/App/WorkspaceController.php b/app/Http/Controllers/App/WorkspaceController.php index 5b2326f2d..b43984e1a 100644 --- a/app/Http/Controllers/App/WorkspaceController.php +++ b/app/Http/Controllers/App/WorkspaceController.php @@ -83,7 +83,7 @@ public function create(Request $request): Response|RedirectResponse * Block creating a paid additional workspace without an active subscription. * Guards both the form (`create`) and the write (`store`) so a direct POST * can't bootstrap a second billable workspace — which would also inflate the - * checkout quantity past the fixed first-month coupon. + * Stripe Checkout seat quantity before the owner has paid. */ private function denyAdditionalWorkspaceWithoutSubscription(User $user): ?RedirectResponse { diff --git a/app/Http/Controllers/Auth/SocialController.php b/app/Http/Controllers/Auth/SocialController.php index ec2ed9788..f328b87c3 100644 --- a/app/Http/Controllers/Auth/SocialController.php +++ b/app/Http/Controllers/Auth/SocialController.php @@ -38,14 +38,7 @@ public function index(Request $request): Response $this->authorize('manageAccounts', $workspace); - $platforms = collect(SocialPlatform::cases()) - ->filter(fn ($platform) => $platform->isConnectable()) - ->map(fn ($platform) => [ - 'value' => $platform->value, - 'label' => $platform->label(), - 'color' => $platform->color(), - 'network' => $platform->network(), - ])->values(); + $platforms = SocialPlatform::connectableOptions(); return Inertia::render('accounts/Index', [ 'workspace' => $workspace, diff --git a/app/Http/Middleware/Api/LoadWorkspaceFromToken.php b/app/Http/Middleware/Api/LoadWorkspaceFromToken.php index dd9743646..4b7c5c555 100644 --- a/app/Http/Middleware/Api/LoadWorkspaceFromToken.php +++ b/app/Http/Middleware/Api/LoadWorkspaceFromToken.php @@ -31,7 +31,10 @@ public function handle(Request $request, Closure $next): Response return response()->json(['message' => 'No workspace selected.'], Response::HTTP_UNAUTHORIZED); } - if (! config('trypost.self_hosted') && ! $workspace->account?->hasActiveSubscription()) { + // Match web access (EnsureAccountReady): Stripe subscription OR generic + // no-card trial. MCP onboarding is a first-class checklist step for + // those accounts — requiring subscribed() alone returned 402 after OAuth. + if (! config('trypost.self_hosted') && ! $workspace->account?->hasAppAccess()) { return response()->json(['message' => 'Active subscription required.'], Response::HTTP_PAYMENT_REQUIRED); } diff --git a/app/Http/Middleware/App/EnsureAccountReady.php b/app/Http/Middleware/App/EnsureAccountReady.php index 7e15bfe6c..193cd1617 100644 --- a/app/Http/Middleware/App/EnsureAccountReady.php +++ b/app/Http/Middleware/App/EnsureAccountReady.php @@ -4,7 +4,6 @@ namespace App\Http\Middleware\App; -use App\Models\Account; use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; @@ -24,14 +23,9 @@ public function handle(Request $request, Closure $next): Response if (! config('trypost.self_hosted')) { $account = $user->account; - $requiresCardForTrial = (bool) config('trypost.billing.require_card_for_trial', true); - $hasAccess = $account && ( - $account->subscribed(Account::SUBSCRIPTION_NAME) - || (! $requiresCardForTrial && $account->isOnTrial()) - ); - - if (! $hasAccess) { - return redirect()->route('app.onboarding'); + + if (! $account?->hasAppAccess()) { + return redirect()->route('app.welcome.persona'); } } diff --git a/app/Http/Middleware/App/HandleInertiaRequests.php b/app/Http/Middleware/App/HandleInertiaRequests.php index 167185be8..563b337c7 100644 --- a/app/Http/Middleware/App/HandleInertiaRequests.php +++ b/app/Http/Middleware/App/HandleInertiaRequests.php @@ -4,6 +4,7 @@ namespace App\Http\Middleware\App; +use App\Actions\Onboarding\ResolveOnboardingStatus; use App\Enums\PostPlatform\ContentType; use App\Http\Resources\App\HandleInertiaRequests\AuthAccountResource; use App\Http\Resources\App\HandleInertiaRequests\AuthPlanResource; @@ -48,6 +49,9 @@ public function share(Request $request): array ], 'usage' => $account && ! $isSelfHosted ? $account->usage() : null, 'features' => $account && ! $isSelfHosted ? $account->featureLimits() : null, + 'onboardingResidual' => fn (): array|false => $user + ? app(ResolveOnboardingStatus::class)->residual($user) + : false, 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', 'flash' => $request->session()->get('flash', []), 'applicationUrl' => config('app.url'), diff --git a/app/Http/Requests/App/Onboarding/StoreOnboardingGoalsRequest.php b/app/Http/Requests/App/Welcome/StoreWelcomeGoalsRequest.php similarity index 81% rename from app/Http/Requests/App/Onboarding/StoreOnboardingGoalsRequest.php rename to app/Http/Requests/App/Welcome/StoreWelcomeGoalsRequest.php index e127eab3c..987fa2c4e 100644 --- a/app/Http/Requests/App/Onboarding/StoreOnboardingGoalsRequest.php +++ b/app/Http/Requests/App/Welcome/StoreWelcomeGoalsRequest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace App\Http\Requests\App\Onboarding; +namespace App\Http\Requests\App\Welcome; use App\Enums\User\Goal; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; -class StoreOnboardingGoalsRequest extends FormRequest +class StoreWelcomeGoalsRequest extends FormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/App/Onboarding/StoreOnboardingRequest.php b/app/Http/Requests/App/Welcome/StoreWelcomePersonaRequest.php similarity index 81% rename from app/Http/Requests/App/Onboarding/StoreOnboardingRequest.php rename to app/Http/Requests/App/Welcome/StoreWelcomePersonaRequest.php index ee0d74283..c22f29fc7 100644 --- a/app/Http/Requests/App/Onboarding/StoreOnboardingRequest.php +++ b/app/Http/Requests/App/Welcome/StoreWelcomePersonaRequest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace App\Http\Requests\App\Onboarding; +namespace App\Http\Requests\App\Welcome; use App\Enums\User\Persona; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; -class StoreOnboardingRequest extends FormRequest +class StoreWelcomePersonaRequest extends FormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/App/Onboarding/StoreOnboardingReferralSourceRequest.php b/app/Http/Requests/App/Welcome/StoreWelcomeReferralSourceRequest.php similarity index 80% rename from app/Http/Requests/App/Onboarding/StoreOnboardingReferralSourceRequest.php rename to app/Http/Requests/App/Welcome/StoreWelcomeReferralSourceRequest.php index 47c9a5ae4..960089ec0 100644 --- a/app/Http/Requests/App/Onboarding/StoreOnboardingReferralSourceRequest.php +++ b/app/Http/Requests/App/Welcome/StoreWelcomeReferralSourceRequest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace App\Http\Requests\App\Onboarding; +namespace App\Http\Requests\App\Welcome; use App\Enums\User\ReferralSource; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; -class StoreOnboardingReferralSourceRequest extends FormRequest +class StoreWelcomeReferralSourceRequest extends FormRequest { public function authorize(): bool { diff --git a/app/Models/AccessToken.php b/app/Models/AccessToken.php index 0f81b83c0..0b6554aa4 100644 --- a/app/Models/AccessToken.php +++ b/app/Models/AccessToken.php @@ -4,9 +4,13 @@ namespace App\Models; +use App\Observers\AccessTokenObserver; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Laravel\Passport\Token; +#[ObservedBy(AccessTokenObserver::class)] class AccessToken extends Token { /** @@ -41,4 +45,41 @@ public function workspace(): BelongsTo { return $this->belongsTo(Workspace::class); } + + /** + * Active OAuth grants used by MCP clients (excludes personal access API keys). + * + * @param Builder $query + * @return Builder + */ + public function scopeActiveMcpOAuth(Builder $query): Builder + { + return $query + ->where('revoked', false) + ->where(function (Builder $expires): void { + $expires->whereNull('expires_at') + ->orWhere('expires_at', '>', now()); + }) + ->whereHas( + 'client', + fn (Builder $client): Builder => $client + ->where('revoked', false) + ->whereJsonDoesntContain('grant_types', 'personal_access'), + ); + } + + public function isMcpOAuthClient(): bool + { + $this->loadMissing('client'); + + if ($this->client === null || $this->client->revoked || $this->client->hasGrantType('personal_access')) { + return false; + } + + if ($this->expires_at !== null && $this->expires_at->isPast()) { + return false; + } + + return true; + } } diff --git a/app/Models/Account.php b/app/Models/Account.php index b8e31b00a..e5fc55a92 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -38,10 +38,14 @@ public static function postsCountCacheKey(string $accountId): string 'billing_email', 'plan_id', 'trial_ends_at', + 'onboarding_completed_at', + 'onboarding_dismissed_at', ]; protected $casts = [ 'trial_ends_at' => 'datetime', + 'onboarding_completed_at' => 'datetime', + 'onboarding_dismissed_at' => 'datetime', ]; public function owner(): BelongsTo @@ -78,6 +82,22 @@ public function hasActiveSubscription(): bool return $this->subscribed(self::SUBSCRIPTION_NAME); } + /** + * Whether the account may use the app (active subscription, or a generic + * trial when REQUIRE_CARD_FOR_TRIAL is disabled). + */ + public function hasAppAccess(): bool + { + if (config('trypost.self_hosted')) { + return true; + } + + $requiresCardForTrial = (bool) config('trypost.billing.require_card_for_trial', true); + + return $this->subscribed(self::SUBSCRIPTION_NAME) + || (! $requiresCardForTrial && $this->isOnTrial()); + } + /** * Align the Stripe subscription quantity with the number of workspaces the * account owns. Each workspace is a billed unit. No-op in self-hosted mode diff --git a/app/Observers/AccessTokenObserver.php b/app/Observers/AccessTokenObserver.php new file mode 100644 index 000000000..e5fc55679 --- /dev/null +++ b/app/Observers/AccessTokenObserver.php @@ -0,0 +1,47 @@ +revoked) { + return; + } + + $this->broadcastIfMcpOAuth($accessToken); + } + + public function updated(AccessToken $accessToken): void + { + if (! $accessToken->wasChanged('revoked')) { + return; + } + + $this->broadcastIfMcpOAuth($accessToken); + } + + private function broadcastIfMcpOAuth(AccessToken $accessToken): void + { + if (! $accessToken->isMcpOAuthClient()) { + return; + } + + $user = User::query() + ->with(['account', 'currentWorkspace']) + ->find($accessToken->user_id); + + OnboardingStatusUpdated::dispatchForAccount($user?->account, $user); + } +} diff --git a/app/Observers/PostObserver.php b/app/Observers/PostObserver.php index 1486cfc04..165472d11 100644 --- a/app/Observers/PostObserver.php +++ b/app/Observers/PostObserver.php @@ -6,9 +6,12 @@ use App\Enums\Automation\Trigger\Type as TriggerType; use App\Enums\Post\Status as PostStatus; +use App\Events\OnboardingStatusUpdated; use App\Events\PostCreated; use App\Jobs\Automation\DispatchPostTriggerAutomationsJob; use App\Models\Post; +use App\Models\User; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; class PostObserver @@ -16,6 +19,53 @@ class PostObserver public function created(Post $post): void { DB::afterCommit(fn () => PostCreated::dispatch($post)); + + $account = $post->workspace?->account; + + if ($account?->onboarding_completed_at !== null + || $account?->onboarding_dismissed_at !== null + ) { + return; + } + + // Only the first post unlocks the onboarding step — later creates would + // just spam Echo reloads while activation is still open. + $isFirstPost = Post::query() + ->where('workspace_id', $post->workspace_id) + ->whereKeyNot($post->id) + ->doesntExist(); + + if ($isFirstPost) { + OnboardingStatusUpdated::dispatchForWorkspace( + $post->workspace_id, + $this->actorFor($post), + ); + } + } + + public function deleted(Post $post): void + { + OnboardingStatusUpdated::dispatchForWorkspace( + $post->workspace_id, + $this->actorFor($post), + ); + } + + /** + * Prefer the authenticated request user (may carry an in-memory API/MCP + * workspace) over the persisted post author for checklist sync. + */ + private function actorFor(Post $post): ?User + { + $user = Auth::user(); + + if ($user instanceof User + && (string) $user->account_id === (string) $post->workspace?->account_id + ) { + return $user; + } + + return $post->user; } public function saved(Post $post): void diff --git a/app/Observers/SocialAccountObserver.php b/app/Observers/SocialAccountObserver.php index fb41f5971..f2f17b670 100644 --- a/app/Observers/SocialAccountObserver.php +++ b/app/Observers/SocialAccountObserver.php @@ -5,10 +5,13 @@ namespace App\Observers; use App\Enums\SocialAccount\Platform; +use App\Events\OnboardingStatusUpdated; use App\Exceptions\SocialAccount\NetworkAlreadyConnectedException; use App\Jobs\PostHog\SyncAccountUsage; use App\Models\SocialAccount; +use App\Models\User; use App\Services\PostHogService; +use Illuminate\Support\Facades\Auth; class SocialAccountObserver { @@ -44,11 +47,36 @@ public function creating(SocialAccount $socialAccount): void public function created(SocialAccount $socialAccount): void { $this->syncUsage($socialAccount); + + OnboardingStatusUpdated::dispatchForWorkspace( + $socialAccount->workspace_id, + $this->actorFor($socialAccount), + ); } public function deleted(SocialAccount $socialAccount): void { $this->syncUsage($socialAccount); + + OnboardingStatusUpdated::dispatchForWorkspace( + $socialAccount->workspace_id, + $this->actorFor($socialAccount), + ); + } + + private function actorFor(SocialAccount $socialAccount): ?User + { + $user = Auth::user(); + + if (! $user instanceof User) { + return null; + } + + if ((string) $user->account_id !== (string) $socialAccount->workspace?->account_id) { + return null; + } + + return $user; } private function syncUsage(SocialAccount $socialAccount): void diff --git a/app/Policies/AccountPolicy.php b/app/Policies/AccountPolicy.php index b7042f16e..78460f5fd 100644 --- a/app/Policies/AccountPolicy.php +++ b/app/Policies/AccountPolicy.php @@ -32,10 +32,7 @@ public function useAi(User $user, Account $account): Response return Response::allow(); } - $requiresCardForTrial = (bool) config('trypost.billing.require_card_for_trial', true); - - $hasAccess = $account->subscribed(Account::SUBSCRIPTION_NAME) - || (! $requiresCardForTrial && $account->isOnTrial()); + $hasAccess = $account->hasAppAccess(); if (! $hasAccess) { return Response::deny(__('billing.flash.subscription_required')); diff --git a/app/Support/Billing/CashierCheckoutEnv.php b/app/Support/Billing/CashierCheckoutEnv.php new file mode 100644 index 000000000..8925b7f9c --- /dev/null +++ b/app/Support/Billing/CashierCheckoutEnv.php @@ -0,0 +1,39 @@ + $amountTotal / 100, + 'currency' => strtoupper((string) $currency), + 'transaction_id' => (string) $transactionId, + ]; + } +} diff --git a/app/Support/Billing/ConfigureSubscriptionCheckout.php b/app/Support/Billing/ConfigureSubscriptionCheckout.php new file mode 100644 index 000000000..885df602c --- /dev/null +++ b/app/Support/Billing/ConfigureSubscriptionCheckout.php @@ -0,0 +1,60 @@ + 0 && self::qualifiesForCheckoutTrial($account)) { + // Stripe Checkout's minimum trial is 48 hours; clamp misconfigured 1-day values. + $subscription->trialDays(max(self::MIN_CHECKOUT_TRIAL_DAYS, $trialDays)); + } + + if ((bool) config('cashier.allow_promotion_codes', true)) { + $subscription->allowPromotionCodes(); + } + + return $subscription; + } + + /** + * Checkout trials are for genuinely new card-required signups. Returning + * accounts (any prior subscription that left incomplete) and no-card + * generic-trial installs should charge immediately / use promo codes. + */ + public static function qualifiesForCheckoutTrial(Account $account): bool + { + if (! (bool) config('trypost.billing.require_card_for_trial', true)) { + return false; + } + + return ! $account->subscriptions() + ->where('type', Account::SUBSCRIPTION_NAME) + ->whereNotIn('stripe_status', [ + StripeSubscription::STATUS_INCOMPLETE, + StripeSubscription::STATUS_INCOMPLETE_EXPIRED, + ]) + ->exists(); + } +} diff --git a/app/Support/Billing/FirstMonthCheckoutDiscount.php b/app/Support/Billing/FirstMonthCheckoutDiscount.php deleted file mode 100644 index 2fba2131c..000000000 --- a/app/Support/Billing/FirstMonthCheckoutDiscount.php +++ /dev/null @@ -1,66 +0,0 @@ -allowPromotionCodes(); - } - - $couponId = config('cashier.first_month_coupon_id'); - - if (! is_string($couponId) || $couponId === '') { - throw new RuntimeException( - 'STRIPE_FIRST_MONTH_COUPON_ID must be set when REQUIRE_CARD_FOR_TRIAL is enabled, ' - .'otherwise checkout would charge the full price instead of the $1 first month.' - ); - } - - return $subscription->withCoupon($couponId); - } - - /** - * The fixed-amount first-month coupon only applies to a genuinely new - * customer checking out a single workspace: the fixed `amount_off` is only - * correct for a quantity of one, and the $1 offer is for first-time signups - * — not a returning account re-subscribing with workspaces it kept from a - * lapsed subscription. A subscription that never left `incomplete` never - * became real, so a new customer retrying after a failed first attempt - * still qualifies; any started subscription (even canceled) does not. - */ - private static function qualifiesForPaidFirstMonth(Account $account): bool - { - if (! (bool) config('trypost.billing.require_card_for_trial', true)) { - return false; - } - - return $account->workspaces()->count() === 1 - && ! $account->subscriptions() - ->whereNotIn('stripe_status', [ - StripeSubscription::STATUS_INCOMPLETE, - StripeSubscription::STATUS_INCOMPLETE_EXPIRED, - ]) - ->exists(); - } -} diff --git a/app/Support/Onboarding/DismissAccountsWithAppAccess.php b/app/Support/Onboarding/DismissAccountsWithAppAccess.php new file mode 100644 index 000000000..cb568c463 --- /dev/null +++ b/app/Support/Onboarding/DismissAccountsWithAppAccess.php @@ -0,0 +1,36 @@ +whereNull('onboarding_dismissed_at') + ->whereNull('onboarding_completed_at') + ->orderBy('id') + ->chunkById(100, function ($accounts) use ($dismissedAt): void { + foreach ($accounts as $account) { + if (! $account->hasAppAccess()) { + continue; + } + + $account->forceFill([ + 'onboarding_dismissed_at' => $dismissedAt, + ])->saveQuietly(); + } + }); + } +} diff --git a/config/cashier.php b/config/cashier.php index 523c76b09..045825418 100644 --- a/config/cashier.php +++ b/config/cashier.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use App\Support\Billing\CashierCheckoutEnv; use Laravel\Cashier\Console\WebhookCommand; use Laravel\Cashier\Invoices\DompdfInvoiceRenderer; @@ -131,25 +132,29 @@ | Trial Period |-------------------------------------------------------------------------- | - | The number of days for the trial period. Set to 0 to disable trials. + | Days of Stripe Checkout trial when REQUIRE_CARD_FOR_TRIAL is enabled, and + | the generic no-card trial length when it is disabled. Set to 0 to disable + | Checkout trials (customer is charged immediately at checkout). + | + | Empty / missing env falls back to 8 (an empty string must not become 0). + | Values of 1 are clamped to 2 at checkout — Stripe requires ≥ 48 hours. | */ - 'trial_days' => env('CASHIER_TRIAL_DAYS', 8), + 'trial_days' => CashierCheckoutEnv::trialDays(env('CASHIER_TRIAL_DAYS', 8)), /* |-------------------------------------------------------------------------- - | Paid First Month Coupon + | Allow Promotion Codes |-------------------------------------------------------------------------- | - | Stripe Coupon ID applied at checkout so the first invoice comes out to - | $1 instead of the full monthly price — a real charge validates the - | card up front instead of a $0 trial authorization. Must be an - | `amount_off` coupon with `duration: once`, so it discounts only the - | first invoice and the full price bills automatically afterward. + | Show Stripe Checkout's promotion-code field so customers can redeem + | promotion codes created in the Stripe Dashboard. | */ - 'first_month_coupon_id' => env('STRIPE_FIRST_MONTH_COUPON_ID'), + 'allow_promotion_codes' => CashierCheckoutEnv::allowPromotionCodes( + env('CASHIER_ALLOW_PROMOTION_CODES', true) + ), ]; diff --git a/config/trypost.php b/config/trypost.php index 08cd5054a..26dbdb199 100644 --- a/config/trypost.php +++ b/config/trypost.php @@ -219,4 +219,32 @@ ], ], + /* + |-------------------------------------------------------------------------- + | Onboarding MCP Clients + |-------------------------------------------------------------------------- + | + | AI assistants shown on the post-subscription onboarding page. Hosts are + | env-overridable so self-hosted installs can point at alternate consoles. + | + */ + + 'mcp' => [ + 'clients' => [ + 'claude' => [ + 'label' => 'Claude', + 'logo' => '/images/ai/claude.svg', + 'settings_url' => env('MCP_CLAUDE_SETTINGS_URL', 'https://claude.ai/customize/connectors'), + ], + 'chatgpt' => [ + 'label' => 'ChatGPT', + 'logo' => '/images/ai/chatgpt-white.svg', + 'settings_url' => env( + 'MCP_CHATGPT_SETTINGS_URL', + 'https://chatgpt.com/plugins#settings/Connectors?create-connector=true&redirectAfter=%2Fplugins', + ), + ], + ], + ], + ]; diff --git a/database/migrations/2026_07_24_160704_add_onboarding_timestamps_to_accounts_table.php b/database/migrations/2026_07_24_160704_add_onboarding_timestamps_to_accounts_table.php new file mode 100644 index 000000000..29541a30a --- /dev/null +++ b/database/migrations/2026_07_24_160704_add_onboarding_timestamps_to_accounts_table.php @@ -0,0 +1,36 @@ +timestamp('onboarding_completed_at')->nullable()->after('trial_ends_at'); + $table->timestamp('onboarding_dismissed_at')->nullable()->after('onboarding_completed_at'); + }); + + // Match hasAppAccess() — active/trialing/past_due/grace + generic trial — + // so existing customers never see the new residual activation banner. + DismissAccountsWithAppAccess::run(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('accounts', function (Blueprint $table) { + $table->dropColumn(['onboarding_completed_at', 'onboarding_dismissed_at']); + }); + } +}; diff --git a/database/migrations/2026_07_29_183500_backfill_onboarding_dismissed_for_accounts_with_app_access.php b/database/migrations/2026_07_29_183500_backfill_onboarding_dismissed_for_accounts_with_app_access.php new file mode 100644 index 000000000..b621f5709 --- /dev/null +++ b/database/migrations/2026_07_29_183500_backfill_onboarding_dismissed_for_accounts_with_app_access.php @@ -0,0 +1,19 @@ + 'MCP', + 'subtitle' => 'اربط مساعدي الذكاء الاصطناعي لإنشاء المنشورات وإدارتها بحساب TryPost الخاص بك.', + 'step_add' => 'الصق الاسم أو الرابط أو الإعداد أدناه في تطبيقك. يفتح تسجيل الدخول في المتصفح عند أول اتصال.', + 'name_label' => 'الاسم', + 'url_label' => 'رابط الخادم', + 'config_label' => 'الإعداد', + 'connected_title' => 'التطبيقات المتصلة', + 'connected_description' => 'المساعدون الذين سجّل دخولهم أي شخص على هذا الحساب. يمكنك قطع اتصال تطبيقاتك فقط.', + 'connected_empty' => 'لا يوجد اتصال بعد. استخدم Claude أو ChatGPT أو عميلًا آخر أعلاه.', + 'connected_by' => 'متصل بواسطة :name', + 'disconnect' => 'قطع الاتصال', + 'disconnect_title' => 'قطع اتصال التطبيق', + 'disconnect_confirm' => 'يؤدي هذا إلى تسجيل خروج التطبيق من TryPost. سيحتاج إلى إعادة الاتصال قبل استخدام MCP مجددًا.', + 'disconnected' => 'تم قطع اتصال التطبيق.', + 'copied' => 'تم النسخ', + 'last_used' => 'آخر استخدام', + 'never' => 'أبدًا', + 'documentation_title' => 'التوثيق', + 'documentation_description' => 'أدلة الإعداد لكل عميل، والأدوات المتاحة، وحل المشكلات.', + 'view_docs' => 'عرض التوثيق', + 'connector_name' => 'TryPost', + + 'other_clients_title' => 'تطبيقات أخرى', + 'other_clients_description' => 'Cursor وVS Code وClaude Code وأي تطبيق يدعم MCP.', + + 'clients' => [ + 'cursor' => 'أضف TryPost كخادم MCP بعيد في Cursor.', + 'cursor_name' => 'Cursor', + 'vscode' => 'الصق الإعداد أدناه في إعدادات MCP في VS Code.', + 'vscode_name' => 'VS Code', + 'claude_code' => 'الصق الإعداد أدناه في إعدادات MCP في Claude Code.', + 'claude_code_name' => 'Claude Code', + 'other' => 'يعمل مع أي عميل يقرأ إعداد mcpServers.', + 'other_name' => 'أخرى', + ], +]; diff --git a/lang/ar/onboarding.php b/lang/ar/onboarding.php index 5103f1d30..75f488e94 100644 --- a/lang/ar/onboarding.php +++ b/lang/ar/onboarding.php @@ -3,55 +3,45 @@ declare(strict_types=1); return [ - 'title' => 'مرحبًا بك في TryPost', - 'description' => 'أخبرنا بما يصفك أنت أو نشاطك التجاري على أفضل وجه حتى نتمكن من تخصيص تجربتك.', - 'continue' => 'متابعة', - 'personas' => [ - 'creator' => 'صانع محتوى', - 'freelancer' => 'مستقل', - 'developer' => 'مطوّر', - 'startup' => 'شركة ناشئة', - 'agency' => 'وكالة', - 'small_business' => 'نشاط تجاري صغير', - 'marketer' => 'مسوّق', - 'online_store' => 'متجر إلكتروني', - 'other' => 'أخرى', + 'title' => 'البدء', + 'welcome' => 'مرحبًا بك في TryPost، :name', + 'welcome_anonymous' => 'مرحبًا بك في TryPost', + 'description' => 'اتبع الخطوات أدناه لمعرفة كيف يعمل TryPost ونشر منشورك الأول.', + 'skip' => 'تخطِّ الآن', + 'continue' => 'المتابعة إلى TryPost', + 'status' => [ + 'complete' => 'مكتمل', + 'todo' => 'مطلوب', ], - 'goals_title' => 'ما هدفك من استخدام TryPost؟', - 'goals_description' => 'اختر كل ما يناسبك وسنقوم بإعداد TryPost من أجلك.', - 'goals' => [ - 'save_time' => 'توفير الوقت بالنشر في كل مكان دفعة واحدة', - 'ai_content' => 'إنشاء منشورات أسرع بالذكاء الاصطناعي', - 'plan_calendar' => 'التخطيط لمنشوراتي على التقويم', - 'stay_on_brand' => 'الحفاظ على اتساق كل منشور مع العلامة التجارية', - 'grow_audience' => 'تنمية جمهوري وزيادة التفاعل', - 'drive_sales' => 'الحصول على المزيد من الزيارات والمبيعات', - 'manage_clients' => 'إدارة عدة علامات تجارية أو عملاء', - 'team_collaboration' => 'العمل مع فريقي', - 'automate_api' => 'أتمتة النشر عبر الواجهة البرمجية أو MCP أو الكود', - 'track_performance' => 'معرفة أداء منشوراتي', - 'just_exploring' => 'مجرد استكشاف في الوقت الحالي', - 'other' => 'شيء آخر', + 'mcp' => [ + 'title' => 'اربط مساعد الذكاء الاصطناعي', + 'description' => 'أضِف TryPost كخادم MCP ليتمكّن مساعدك من إنشاء منشورات التواصل وإدارتها نيابةً عنك.', + 'copy_step' => 'انسخ عنوان خادم TryPost', + 'open_step' => 'افتح مساعد الذكاء الاصطناعي', + 'copy' => 'نسخ الرابط', + 'copied' => 'تم نسخ رابط MCP.', + 'connect' => 'الاتصال عبر :client', + 'clients' => [ + 'claude' => 'افتح Settings → Connectors، أضِف موصلًا مخصصًا، ثم الصق الرابط أعلاه.', + 'chatgpt' => 'افتح Settings → Apps & Connectors، أنشئ موصلًا مخصصًا، ثم الصق الرابط أعلاه.', + ], ], - 'referral_source_title' => 'كيف وجدتنا؟', - 'referral_source_description' => 'يساعدنا هذا على فهم كيفية اكتشاف الأشخاص لـ TryPost.', - 'referral_source' => [ - 'google' => 'Google أو البحث', - 'x' => 'X (Twitter)', - 'linkedin' => 'LinkedIn', - 'youtube' => 'YouTube', - 'tiktok' => 'TikTok', - 'instagram' => 'Instagram أو Threads', - 'reddit' => 'Reddit', - 'product_hunt' => 'Product Hunt', - 'ai_assistant' => 'مساعد ذكاء اصطناعي (ChatGPT، Claude…)', - 'friend' => 'صديق أو زميل', - 'blog' => 'مدونة أو نشرة إخبارية أو مقال', - 'other' => 'شيء آخر', + 'social' => [ + 'title' => 'اربط حسابًا اجتماعيًا', + 'description' => 'اختر شبكة واحدة واحدة على الأقل يمكن لـ TryPost النشر عليها.', ], - 'connect' => [ - 'title' => 'اربط شبكتك الأولى', - 'description' => 'اربط حسابًا اجتماعيًا واحدًا على الأقل لبدء الجدولة. يمكنك إضافة المزيد في أي وقت.', - 'must_connect' => 'اربط شبكة واحدة على الأقل للمتابعة.', + 'first_post' => [ + 'title' => 'أنشئ منشورك الأول', + 'description' => 'جرّب هذا الموجّه مع مساعدك المتصل، أو أنشئ المنشور مباشرة في TryPost.', + 'prompt_label' => 'موجّه نموذجي', + 'sample_prompt' => 'أنشئ منشورًا اجتماعيًا ودّيًا يعرّف بعلامتي التجارية وكيّفه لكل شبكة متصلة.', + 'copy_prompt' => 'نسخ الموجّه', + 'copied' => 'تم نسخ الموجّه النموذجي.', + 'create_button' => 'إنشاء منشورك الأول', + 'or' => 'أو', + ], + 'ready' => [ + 'title' => 'أنت جاهز للنشر', + 'description' => 'كل شيء جاهز. تابع إلى TryPost وابدأ بتخطيط محتواك.', ], ]; diff --git a/lang/ar/settings.php b/lang/ar/settings.php index fc98446e5..c2daf632a 100644 --- a/lang/ar/settings.php +++ b/lang/ar/settings.php @@ -129,6 +129,7 @@ 'brand' => 'العلامة التجارية', 'users' => 'الأعضاء', 'api_keys' => 'مفاتيح API', + 'mcp' => 'MCP', ], 'title' => 'إعدادات مساحة العمل', 'logo_heading' => 'شعار مساحة العمل', diff --git a/lang/ar/sidebar.php b/lang/ar/sidebar.php index a9587e13e..a875ea2b0 100644 --- a/lang/ar/sidebar.php +++ b/lang/ar/sidebar.php @@ -8,6 +8,8 @@ 'create_workspace' => 'إنشاء مساحة عمل', 'create_post' => 'إنشاء منشور', 'profile' => 'الملف الشخصي', + 'my_account' => 'حسابي', + 'account_settings' => 'الحساب والفوترة', 'log_out' => 'تسجيل الخروج', 'workspace' => 'مساحة العمل: :name', @@ -30,6 +32,8 @@ 'analytics' => 'التحليلات', 'automations' => 'الأتمتة', 'settings' => 'الإعدادات', + 'onboarding' => 'البدء', + 'onboarding_hint' => 'أكمل الإعداد', 'posts' => [ 'calendar' => 'التقويم', @@ -44,7 +48,9 @@ 'signatures' => 'التوقيعات', 'labels' => 'التسميات', 'assets' => 'الوسائط', + 'settings' => 'الإعدادات', 'api_keys' => 'مفاتيح API', + 'mcp' => 'MCP', ], 'notifications' => 'الإشعارات', diff --git a/lang/ar/welcome.php b/lang/ar/welcome.php new file mode 100644 index 000000000..e37b87daa --- /dev/null +++ b/lang/ar/welcome.php @@ -0,0 +1,52 @@ + 'ما الذي يصفك بشكل أفضل؟', + 'description' => 'اختر الخيار الأقرب وسنخصص تجربتك.', + 'continue' => 'متابعة', + 'checkout_owner_only' => 'اطلب من مالك الحساب إكمال الدفع وبدء الاشتراك.', + 'progress' => 'تقدم الترحيب', + 'go_to_step' => 'الانتقال إلى الخطوة :step', + 'personas' => [ + 'creator' => 'صانع محتوى', + 'freelancer' => 'مستقل', + 'developer' => 'مطوّر', + 'startup' => 'شركة ناشئة', + 'agency' => 'وكالة', + 'small_business' => 'نشاط تجاري صغير', + 'marketer' => 'مسوّق', + 'online_store' => 'متجر إلكتروني', + 'other' => 'أخرى', + ], + 'goals_title' => 'ما هدفك؟', + 'goals_description' => 'اختر كل ما يناسبك وسنقوم بإعداد TryPost من أجلك.', + 'goals' => [ + 'save_time' => 'توفير الوقت بالنشر في كل مكان دفعة واحدة', + 'ai_content' => 'إنشاء منشورات أسرع بالذكاء الاصطناعي', + 'plan_calendar' => 'التخطيط لمنشوراتي على التقويم', + 'stay_on_brand' => 'الحفاظ على اتساق كل منشور مع العلامة التجارية', + 'grow_audience' => 'تنمية جمهوري وزيادة التفاعل', + 'drive_sales' => 'الحصول على المزيد من الزيارات والمبيعات', + 'manage_clients' => 'إدارة عدة علامات تجارية أو عملاء', + 'just_exploring' => 'مجرد استكشاف في الوقت الحالي', + 'other' => 'شيء آخر', + ], + 'referral_source_title' => 'كيف وجدتنا؟', + 'referral_source_description' => 'يساعدنا هذا على فهم كيفية اكتشاف الأشخاص لـ TryPost.', + 'referral_source' => [ + 'google' => 'Google أو البحث', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram أو Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'مساعد ذكاء اصطناعي (ChatGPT، Claude…)', + 'friend' => 'صديق أو زميل', + 'blog' => 'مدونة أو نشرة إخبارية أو مقال', + 'other' => 'شيء آخر', + ], +]; diff --git a/lang/de/mcp.php b/lang/de/mcp.php new file mode 100644 index 000000000..958b89989 --- /dev/null +++ b/lang/de/mcp.php @@ -0,0 +1,41 @@ + 'MCP', + 'subtitle' => 'Verbinde KI-Assistenten, damit sie Beiträge mit deinem TryPost-Konto erstellen und verwalten können.', + 'step_add' => 'Füge Name, URL oder Config unten in deine App ein. Die Anmeldung öffnet sich beim ersten Verbinden im Browser.', + 'name_label' => 'Name', + 'url_label' => 'Server-URL', + 'config_label' => 'Config', + 'connected_title' => 'Verbundene Apps', + 'connected_description' => 'Assistenten, die jemand in diesem Konto angemeldet hat. Du kannst nur deine eigenen trennen.', + 'connected_empty' => 'Noch nichts verbunden. Nutze Claude, ChatGPT oder einen anderen Client oben.', + 'connected_by' => 'Verbunden von :name', + 'disconnect' => 'Trennen', + 'disconnect_title' => 'App trennen', + 'disconnect_confirm' => 'Dadurch wird die App von TryPost abgemeldet. Sie muss sich neu verbinden, bevor sie MCP wieder nutzen kann.', + 'disconnected' => 'App getrennt.', + 'copied' => 'Kopiert', + 'last_used' => 'Zuletzt verwendet', + 'never' => 'Nie', + 'documentation_title' => 'Dokumentation', + 'documentation_description' => 'Einrichtungsguides pro Client, verfügbare Tools und Fehlerhilfe.', + 'view_docs' => 'Dokumentation ansehen', + 'connector_name' => 'TryPost', + + 'other_clients_title' => 'Andere Apps', + 'other_clients_description' => 'Cursor, VS Code, Claude Code und alles andere, das MCP spricht.', + + 'clients' => [ + 'cursor' => 'Füge TryPost in Cursor als Remote-MCP-Server hinzu.', + 'cursor_name' => 'Cursor', + 'vscode' => 'Füge die Konfiguration unten in die MCP-Einstellungen von VS Code ein.', + 'vscode_name' => 'VS Code', + 'claude_code' => 'Füge die Konfiguration unten in die MCP-Einstellungen von Claude Code ein.', + 'claude_code_name' => 'Claude Code', + 'other' => 'Funktioniert mit jedem Client, der eine mcpServers-Config liest.', + 'other_name' => 'Andere', + ], +]; diff --git a/lang/de/onboarding.php b/lang/de/onboarding.php index 9ccf8ad72..b36f8bd1a 100644 --- a/lang/de/onboarding.php +++ b/lang/de/onboarding.php @@ -3,55 +3,45 @@ declare(strict_types=1); return [ - 'title' => 'Willkommen bei TryPost', - 'description' => 'Sag uns, was dich oder dein Unternehmen am besten beschreibt, damit wir dein Erlebnis anpassen können.', - 'continue' => 'Weiter', - 'personas' => [ - 'creator' => 'Content Creator', - 'freelancer' => 'Freelancer', - 'developer' => 'Entwickler', - 'startup' => 'Startup', - 'agency' => 'Agentur', - 'small_business' => 'Kleinunternehmen', - 'marketer' => 'Marketer', - 'online_store' => 'Onlineshop', - 'other' => 'Sonstiges', + 'title' => 'Erste Schritte', + 'welcome' => 'Willkommen bei TryPost, :name', + 'welcome_anonymous' => 'Willkommen bei TryPost', + 'description' => 'Folge den Schritten unten, um zu sehen, wie TryPost funktioniert, und deinen ersten Beitrag zu veröffentlichen.', + 'skip' => 'Vorerst überspringen', + 'continue' => 'Weiter zu TryPost', + 'status' => [ + 'complete' => 'Erledigt', + 'todo' => 'Offen', ], - 'goals_title' => 'Was ist dein Ziel mit TryPost?', - 'goals_description' => 'Wähle alles aus, was passt, und wir richten TryPost für dich ein.', - 'goals' => [ - 'save_time' => 'Zeit sparen, indem ich überall gleichzeitig poste', - 'ai_content' => 'Beiträge schneller mit KI erstellen', - 'plan_calendar' => 'Meine Beiträge in einem Kalender planen', - 'stay_on_brand' => 'Jeden Beitrag markenkonform halten', - 'grow_audience' => 'Meine Reichweite und mein Engagement steigern', - 'drive_sales' => 'Mehr Traffic und Verkäufe erzielen', - 'manage_clients' => 'Mehrere Marken oder Kunden verwalten', - 'team_collaboration' => 'Mit meinem Team arbeiten', - 'automate_api' => 'Das Posten per API, MCP oder Code automatisieren', - 'track_performance' => 'Sehen, wie meine Beiträge performen', - 'just_exploring' => 'Ich schaue mich vorerst nur um', - 'other' => 'Etwas anderes', + 'mcp' => [ + 'title' => 'Verbinde deinen KI-Assistenten', + 'description' => 'Füge TryPost als MCP-Server hinzu, damit dein Assistent Social-Beiträge für dich erstellen und verwalten kann.', + 'copy_step' => 'Kopiere deine TryPost-Server-URL', + 'open_step' => 'Öffne deinen KI-Assistenten', + 'copy' => 'URL kopieren', + 'copied' => 'MCP-URL kopiert.', + 'connect' => 'Mit :client verbinden', + 'clients' => [ + 'claude' => 'Öffne Settings → Connectors, füge einen benutzerdefinierten Connector hinzu und füge die URL oben ein.', + 'chatgpt' => 'Öffne Settings → Apps & Connectors, erstelle einen benutzerdefinierten Connector und füge die URL oben ein.', + ], ], - 'referral_source_title' => 'Wie hast du uns gefunden?', - 'referral_source_description' => 'Das hilft uns zu verstehen, wie Menschen TryPost entdecken.', - 'referral_source' => [ - 'google' => 'Google oder Suche', - 'x' => 'X (Twitter)', - 'linkedin' => 'LinkedIn', - 'youtube' => 'YouTube', - 'tiktok' => 'TikTok', - 'instagram' => 'Instagram oder Threads', - 'reddit' => 'Reddit', - 'product_hunt' => 'Product Hunt', - 'ai_assistant' => 'KI-Assistent (ChatGPT, Claude…)', - 'friend' => 'Freund oder Kollege', - 'blog' => 'Blog, Newsletter oder Artikel', - 'other' => 'Etwas anderes', + 'social' => [ + 'title' => 'Verbinde ein Social-Konto', + 'description' => 'Wähle mindestens ein Netzwerk, in dem TryPost deine Inhalte veröffentlichen kann.', ], - 'connect' => [ - 'title' => 'Verbinde dein erstes Netzwerk', - 'description' => 'Verknüpfe mindestens ein Social-Media-Konto, um mit der Planung zu beginnen. Du kannst jederzeit weitere hinzufügen.', - 'must_connect' => 'Verbinde mindestens ein Netzwerk, um fortzufahren.', + 'first_post' => [ + 'title' => 'Erstelle deinen ersten Beitrag', + 'description' => 'Probiere diesen Starter-Prompt mit deinem verbundenen Assistenten aus, oder erstelle den Beitrag direkt in TryPost.', + 'prompt_label' => 'Beispiel-Prompt', + 'sample_prompt' => 'Erstelle einen freundlichen Social-Beitrag, der meine Marke vorstellt, und passe ihn für jedes verbundene Netzwerk an.', + 'copy_prompt' => 'Prompt kopieren', + 'copied' => 'Beispiel-Prompt kopiert.', + 'create_button' => 'Ersten Beitrag erstellen', + 'or' => 'oder', + ], + 'ready' => [ + 'title' => 'Du bist bereit zum Veröffentlichen', + 'description' => 'Alles klar. Weiter zu TryPost und plane deine Inhalte.', ], ]; diff --git a/lang/de/settings.php b/lang/de/settings.php index 2d19b929c..6a9254b46 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -131,6 +131,7 @@ 'brand' => 'Marke', 'users' => 'Mitglieder', 'api_keys' => 'API-Keys', + 'mcp' => 'MCP', ], 'title' => 'Workspace-Einstellungen', 'logo_heading' => 'Workspace-Logo', diff --git a/lang/de/sidebar.php b/lang/de/sidebar.php index c9a593f97..0fddfdde4 100644 --- a/lang/de/sidebar.php +++ b/lang/de/sidebar.php @@ -8,6 +8,8 @@ 'create_workspace' => 'Workspace erstellen', 'create_post' => 'Beitrag erstellen', 'profile' => 'Profil', + 'my_account' => 'Mein Konto', + 'account_settings' => 'Konto & Abrechnung', 'log_out' => 'Abmelden', 'workspace' => 'Workspace: :name', @@ -30,6 +32,8 @@ 'analytics' => 'Analytics', 'automations' => 'Automatisierungen', 'settings' => 'Einstellungen', + 'onboarding' => 'Erste Schritte', + 'onboarding_hint' => 'Einrichtung abschließen', 'posts' => [ 'calendar' => 'Kalender', @@ -44,7 +48,9 @@ 'signatures' => 'Signaturen', 'labels' => 'Labels', 'assets' => 'Assets', + 'settings' => 'Einstellungen', 'api_keys' => 'API-Keys', + 'mcp' => 'MCP', ], 'notifications' => 'Benachrichtigungen', diff --git a/lang/de/welcome.php b/lang/de/welcome.php new file mode 100644 index 000000000..32371db4a --- /dev/null +++ b/lang/de/welcome.php @@ -0,0 +1,52 @@ + 'Was beschreibt dich am besten?', + 'description' => 'Wähle die passende Option, und wir passen dein Erlebnis an.', + 'continue' => 'Weiter', + 'checkout_owner_only' => 'Bitte den Kontoinhaber, den Checkout abzuschließen und das Abo zu starten.', + 'progress' => 'Willkommensfortschritt', + 'go_to_step' => 'Zu Schritt :step gehen', + 'personas' => [ + 'creator' => 'Content Creator', + 'freelancer' => 'Freelancer', + 'developer' => 'Entwickler', + 'startup' => 'Startup', + 'agency' => 'Agentur', + 'small_business' => 'Kleinunternehmen', + 'marketer' => 'Marketer', + 'online_store' => 'Onlineshop', + 'other' => 'Sonstiges', + ], + 'goals_title' => 'Was ist dein Ziel?', + 'goals_description' => 'Wähle alles aus, was passt, und wir richten TryPost für dich ein.', + 'goals' => [ + 'save_time' => 'Zeit sparen, indem ich überall gleichzeitig poste', + 'ai_content' => 'Beiträge schneller mit KI erstellen', + 'plan_calendar' => 'Meine Beiträge in einem Kalender planen', + 'stay_on_brand' => 'Jeden Beitrag markenkonform halten', + 'grow_audience' => 'Meine Reichweite und mein Engagement steigern', + 'drive_sales' => 'Mehr Traffic und Verkäufe erzielen', + 'manage_clients' => 'Mehrere Marken oder Kunden verwalten', + 'just_exploring' => 'Ich schaue mich vorerst nur um', + 'other' => 'Etwas anderes', + ], + 'referral_source_title' => 'Wie hast du uns gefunden?', + 'referral_source_description' => 'Das hilft uns zu verstehen, wie Menschen TryPost entdecken.', + 'referral_source' => [ + 'google' => 'Google oder Suche', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram oder Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'KI-Assistent (ChatGPT, Claude…)', + 'friend' => 'Freund oder Kollege', + 'blog' => 'Blog, Newsletter oder Artikel', + 'other' => 'Etwas anderes', + ], +]; diff --git a/lang/el/mcp.php b/lang/el/mcp.php new file mode 100644 index 000000000..4e0ace84f --- /dev/null +++ b/lang/el/mcp.php @@ -0,0 +1,41 @@ + 'MCP', + 'subtitle' => 'Συνδέστε βοηθούς AI για να δημιουργούν και να διαχειρίζονται αναρτήσεις με τον λογαριασμό TryPost σας.', + 'step_add' => 'Επικολλήστε το όνομα, το URL ή το config παρακάτω στην εφαρμογή σας. Η σύνδεση ανοίγει στο πρόγραμμα περιήγησης την πρώτη φορά.', + 'name_label' => 'Όνομα', + 'url_label' => 'URL διακομιστή', + 'config_label' => 'Config', + 'connected_title' => 'Συνδεδεμένες εφαρμογές', + 'connected_description' => 'Βοηθοί με σύνδεση από οποιονδήποτε σε αυτόν τον λογαριασμό. Μπορείτε να αποσυνδέσετε μόνο τους δικούς σας.', + 'connected_empty' => 'Τίποτα συνδεδεμένο ακόμα. Χρησιμοποιήστε Claude, ChatGPT ή άλλο client παραπάνω.', + 'connected_by' => 'Συνδέθηκε από :name', + 'disconnect' => 'Αποσύνδεση', + 'disconnect_title' => 'Αποσύνδεση εφαρμογής', + 'disconnect_confirm' => 'Αυτό αποσυνδέει την εφαρμογή από το TryPost. Θα χρειαστεί να συνδεθεί ξανά πριν χρησιμοποιήσει το MCP.', + 'disconnected' => 'Η εφαρμογή αποσυνδέθηκε.', + 'copied' => 'Αντιγράφηκε', + 'last_used' => 'Τελευταία χρήση', + 'never' => 'Ποτέ', + 'documentation_title' => 'Τεκμηρίωση', + 'documentation_description' => 'Οδηγοί ανά client, διαθέσιμα tools και αντιμετώπιση προβλημάτων.', + 'view_docs' => 'Δείτε την τεκμηρίωση', + 'connector_name' => 'TryPost', + + 'other_clients_title' => 'Άλλες εφαρμογές', + 'other_clients_description' => 'Cursor, VS Code, Claude Code και ό,τι άλλο μιλάει MCP.', + + 'clients' => [ + 'cursor' => 'Προσθέστε το TryPost ως απομακρυσμένο MCP server στο Cursor.', + 'cursor_name' => 'Cursor', + 'vscode' => 'Επικολλήστε το config παρακάτω στις ρυθμίσεις MCP του VS Code.', + 'vscode_name' => 'VS Code', + 'claude_code' => 'Επικολλήστε το config παρακάτω στις ρυθμίσεις MCP του Claude Code.', + 'claude_code_name' => 'Claude Code', + 'other' => 'Λειτουργεί με κάθε client που διαβάζει config mcpServers.', + 'other_name' => 'Άλλα', + ], +]; diff --git a/lang/el/onboarding.php b/lang/el/onboarding.php index d0eccc361..d091a84e0 100644 --- a/lang/el/onboarding.php +++ b/lang/el/onboarding.php @@ -3,55 +3,45 @@ declare(strict_types=1); return [ - 'title' => 'Καλώς ήρθατε στο TryPost', - 'description' => 'Πείτε μας τι σας περιγράφει καλύτερα, εσάς ή την επιχείρησή σας, ώστε να προσαρμόσουμε την εμπειρία σας.', - 'continue' => 'Συνέχεια', - 'personas' => [ - 'creator' => 'Δημιουργός περιεχομένου', - 'freelancer' => 'Ελεύθερος επαγγελματίας', - 'developer' => 'Προγραμματιστής', - 'startup' => 'Startup', - 'agency' => 'Πρακτορείο', - 'small_business' => 'Μικρή επιχείρηση', - 'marketer' => 'Marketer', - 'online_store' => 'Ηλεκτρονικό κατάστημα', - 'other' => 'Άλλο', + 'title' => 'Ξεκινώντας', + 'welcome' => 'Καλώς ήρθες στο TryPost, :name', + 'welcome_anonymous' => 'Καλώς ήρθες στο TryPost', + 'description' => 'Ακολούθησε τα παρακάτω βήματα για να δεις πώς λειτουργεί το TryPost και να δημοσιεύσεις την πρώτη σου ανάρτηση.', + 'skip' => 'Παράλειψη προς το παρόν', + 'continue' => 'Συνέχεια στο TryPost', + 'status' => [ + 'complete' => 'Ολοκληρώθηκε', + 'todo' => 'Εκκρεμεί', ], - 'goals_title' => 'Ποιος είναι ο στόχος σας με το TryPost;', - 'goals_description' => 'Επιλέξτε ό,τι σας ταιριάζει και θα ρυθμίσουμε το TryPost για εσάς.', - 'goals' => [ - 'save_time' => 'Εξοικονόμηση χρόνου δημοσιεύοντας παντού ταυτόχρονα', - 'ai_content' => 'Δημιουργία δημοσιεύσεων ταχύτερα με AI', - 'plan_calendar' => 'Προγραμματισμός των δημοσιεύσεών μου σε ημερολόγιο', - 'stay_on_brand' => 'Διατήρηση κάθε δημοσίευσης εναρμονισμένης με τη μάρκα', - 'grow_audience' => 'Ανάπτυξη του κοινού και της αλληλεπίδρασής μου', - 'drive_sales' => 'Περισσότερη επισκεψιμότητα και πωλήσεις', - 'manage_clients' => 'Διαχείριση πολλών μαρκών ή πελατών', - 'team_collaboration' => 'Συνεργασία με την ομάδα μου', - 'automate_api' => 'Αυτοματοποίηση δημοσιεύσεων με το API, το MCP ή κώδικα', - 'track_performance' => 'Παρακολούθηση της απόδοσης των δημοσιεύσεών μου', - 'just_exploring' => 'Απλώς εξερευνώ προς το παρόν', - 'other' => 'Κάτι άλλο', + 'mcp' => [ + 'title' => 'Σύνδεσε τον βοηθό AI σου', + 'description' => 'Πρόσθεσε το TryPost ως διακομιστή MCP ώστε ο βοηθός σου να δημιουργεί και να διαχειρίζεται social posts για εσένα.', + 'copy_step' => 'Αντίγραψε το URL του διακομιστή TryPost', + 'open_step' => 'Άνοιξε τον βοηθό AI σου', + 'copy' => 'Αντιγραφή URL', + 'copied' => 'Το URL MCP αντιγράφηκε.', + 'connect' => 'Σύνδεση με :client', + 'clients' => [ + 'claude' => 'Άνοιξε Settings → Connectors, πρόσθεσε έναν προσαρμοσμένο connector και επικόλλησε το παραπάνω URL.', + 'chatgpt' => 'Άνοιξε Settings → Apps & Connectors, δημιούργησε έναν προσαρμοσμένο connector και επικόλλησε το παραπάνω URL.', + ], ], - 'referral_source_title' => 'Πώς μας βρήκατε;', - 'referral_source_description' => 'Αυτό μας βοηθά να καταλάβουμε πώς οι άνθρωποι ανακαλύπτουν το TryPost.', - 'referral_source' => [ - 'google' => 'Google ή αναζήτηση', - 'x' => 'X (Twitter)', - 'linkedin' => 'LinkedIn', - 'youtube' => 'YouTube', - 'tiktok' => 'TikTok', - 'instagram' => 'Instagram ή Threads', - 'reddit' => 'Reddit', - 'product_hunt' => 'Product Hunt', - 'ai_assistant' => 'Βοηθός AI (ChatGPT, Claude…)', - 'friend' => 'Φίλος ή συνάδελφος', - 'blog' => 'Blog, newsletter ή άρθρο', - 'other' => 'Κάτι άλλο', + 'social' => [ + 'title' => 'Σύνδεσε έναν λογαριασμό social', + 'description' => 'Διάλεξε τουλάχιστον ένα δίκτυο όπου το TryPost μπορεί να δημοσιεύει το περιεχόμενό σου.', ], - 'connect' => [ - 'title' => 'Συνδέστε το πρώτο σας δίκτυο', - 'description' => 'Συνδέστε τουλάχιστον έναν λογαριασμό κοινωνικού δικτύου για να ξεκινήσετε τον προγραμματισμό. Μπορείτε να προσθέσετε κι άλλους ανά πάσα στιγμή.', - 'must_connect' => 'Συνδέστε τουλάχιστον ένα δίκτυο για να συνεχίσετε.', + 'first_post' => [ + 'title' => 'Δημιούργησε την πρώτη σου ανάρτηση', + 'description' => 'Δοκίμασε αυτό το αρχικό prompt με τον συνδεδεμένο βοηθό σου ή δημιούργησε την ανάρτηση απευθείας στο TryPost.', + 'prompt_label' => 'Δείγμα prompt', + 'sample_prompt' => 'Δημιούργησε μια φιλική social ανάρτηση που παρουσιάζει το brand μου και προσαρμοσέ την για κάθε συνδεδεμένο δίκτυο.', + 'copy_prompt' => 'Αντιγραφή prompt', + 'copied' => 'Το δείγμα prompt αντιγράφηκε.', + 'create_button' => 'Δημιούργησε την πρώτη σου ανάρτηση', + 'or' => 'ή', + ], + 'ready' => [ + 'title' => 'Είσαι έτοιμος να δημοσιεύσεις', + 'description' => 'Όλα είναι έτοιμα. Συνέχισε στο TryPost και ξεκίνα να σχεδιάζεις το περιεχόμενό σου.', ], ]; diff --git a/lang/el/settings.php b/lang/el/settings.php index 0a2a638ed..df0e8c342 100644 --- a/lang/el/settings.php +++ b/lang/el/settings.php @@ -129,6 +129,7 @@ 'brand' => 'Μάρκα', 'users' => 'Μέλη', 'api_keys' => 'Κλειδιά API', + 'mcp' => 'MCP', ], 'title' => 'Ρυθμίσεις workspace', 'logo_heading' => 'Λογότυπο workspace', diff --git a/lang/el/sidebar.php b/lang/el/sidebar.php index a19adb710..4f8738e79 100644 --- a/lang/el/sidebar.php +++ b/lang/el/sidebar.php @@ -8,6 +8,8 @@ 'create_workspace' => 'Δημιουργία workspace', 'create_post' => 'Δημιουργία δημοσίευσης', 'profile' => 'Προφίλ', + 'my_account' => 'Ο λογαριασμός μου', + 'account_settings' => 'Λογαριασμός και χρέωση', 'log_out' => 'Αποσύνδεση', 'workspace' => 'Workspace: :name', @@ -30,6 +32,8 @@ 'analytics' => 'Στατιστικά', 'automations' => 'Αυτοματισμοί', 'settings' => 'Ρυθμίσεις', + 'onboarding' => 'Ξεκινώντας', + 'onboarding_hint' => 'Ολοκλήρωση ρύθμισης', 'posts' => [ 'calendar' => 'Ημερολόγιο', @@ -44,7 +48,9 @@ 'signatures' => 'Υπογραφές', 'labels' => 'Ετικέτες', 'assets' => 'Στοιχεία', + 'settings' => 'Ρυθμίσεις', 'api_keys' => 'Κλειδιά API', + 'mcp' => 'MCP', ], 'notifications' => 'Ειδοποιήσεις', diff --git a/lang/el/welcome.php b/lang/el/welcome.php new file mode 100644 index 000000000..4593ca583 --- /dev/null +++ b/lang/el/welcome.php @@ -0,0 +1,52 @@ + 'Τι σας περιγράφει καλύτερα;', + 'description' => 'Επιλέξτε την πιο κοντινή επιλογή και θα προσαρμόσουμε την εμπειρία σας.', + 'continue' => 'Συνέχεια', + 'checkout_owner_only' => 'Ζητήστε από τον κάτοχο του λογαριασμού να ολοκληρώσει την πληρωμή και να ξεκινήσει τη συνδρομή.', + 'progress' => 'Πρόοδος καλωσορίσματος', + 'go_to_step' => 'Μετάβαση στο βήμα :step', + 'personas' => [ + 'creator' => 'Δημιουργός περιεχομένου', + 'freelancer' => 'Ελεύθερος επαγγελματίας', + 'developer' => 'Προγραμματιστής', + 'startup' => 'Startup', + 'agency' => 'Πρακτορείο', + 'small_business' => 'Μικρή επιχείρηση', + 'marketer' => 'Marketer', + 'online_store' => 'Ηλεκτρονικό κατάστημα', + 'other' => 'Άλλο', + ], + 'goals_title' => 'Ποιος είναι ο στόχος σας;', + 'goals_description' => 'Επιλέξτε ό,τι σας ταιριάζει και θα ρυθμίσουμε το TryPost για εσάς.', + 'goals' => [ + 'save_time' => 'Εξοικονόμηση χρόνου δημοσιεύοντας παντού ταυτόχρονα', + 'ai_content' => 'Δημιουργία δημοσιεύσεων ταχύτερα με AI', + 'plan_calendar' => 'Προγραμματισμός των δημοσιεύσεών μου σε ημερολόγιο', + 'stay_on_brand' => 'Διατήρηση κάθε δημοσίευσης εναρμονισμένης με τη μάρκα', + 'grow_audience' => 'Ανάπτυξη του κοινού και της αλληλεπίδρασής μου', + 'drive_sales' => 'Περισσότερη επισκεψιμότητα και πωλήσεις', + 'manage_clients' => 'Διαχείριση πολλών μαρκών ή πελατών', + 'just_exploring' => 'Απλώς εξερευνώ προς το παρόν', + 'other' => 'Κάτι άλλο', + ], + 'referral_source_title' => 'Πώς μας βρήκατε;', + 'referral_source_description' => 'Αυτό μας βοηθά να καταλάβουμε πώς οι άνθρωποι ανακαλύπτουν το TryPost.', + 'referral_source' => [ + 'google' => 'Google ή αναζήτηση', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram ή Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'Βοηθός AI (ChatGPT, Claude…)', + 'friend' => 'Φίλος ή συνάδελφος', + 'blog' => 'Blog, newsletter ή άρθρο', + 'other' => 'Κάτι άλλο', + ], +]; diff --git a/lang/en/mcp.php b/lang/en/mcp.php new file mode 100644 index 000000000..c04037086 --- /dev/null +++ b/lang/en/mcp.php @@ -0,0 +1,41 @@ + 'MCP', + 'subtitle' => 'Connect AI assistants so they can create and manage posts with your TryPost account.', + 'step_add' => 'Paste the name, URL, or config below into your app. Sign-in opens in the browser the first time it connects.', + 'name_label' => 'Name', + 'url_label' => 'Server URL', + 'config_label' => 'Config', + 'connected_title' => 'Connected apps', + 'connected_description' => 'Assistants signed in by anyone on this account. You can only disconnect your own apps.', + 'connected_empty' => 'Nothing connected yet. Use Claude, ChatGPT, or another client above.', + 'connected_by' => 'Connected by :name', + 'disconnect' => 'Disconnect', + 'disconnect_title' => 'Disconnect app', + 'disconnect_confirm' => 'This signs the app out of TryPost. It will need to reconnect before it can use MCP again.', + 'disconnected' => 'App disconnected.', + 'copied' => 'Copied', + 'last_used' => 'Last used', + 'never' => 'Never', + 'documentation_title' => 'Documentation', + 'documentation_description' => 'Client setup guides, available tools, and troubleshooting.', + 'view_docs' => 'View docs', + 'connector_name' => 'TryPost', + + 'other_clients_title' => 'Other apps', + 'other_clients_description' => 'Cursor, VS Code, Claude Code, and anything else that speaks MCP.', + + 'clients' => [ + 'cursor' => 'Add TryPost as a remote MCP server in Cursor.', + 'cursor_name' => 'Cursor', + 'vscode' => 'Paste the config below into VS Code\'s MCP settings.', + 'vscode_name' => 'VS Code', + 'claude_code' => 'Paste the config below into Claude Code\'s MCP settings.', + 'claude_code_name' => 'Claude Code', + 'other' => 'Works with any client that reads an mcpServers config.', + 'other_name' => 'Other', + ], +]; diff --git a/lang/en/onboarding.php b/lang/en/onboarding.php index e76cf6717..a6c1f2673 100644 --- a/lang/en/onboarding.php +++ b/lang/en/onboarding.php @@ -3,55 +3,45 @@ declare(strict_types=1); return [ - 'title' => 'Welcome to TryPost', - 'description' => 'Tell us what best describes you or your business so we can tailor your experience.', - 'continue' => 'Continue', - 'personas' => [ - 'creator' => 'Content creator', - 'freelancer' => 'Freelancer', - 'developer' => 'Developer', - 'startup' => 'Startup', - 'agency' => 'Agency', - 'small_business' => 'Small business', - 'marketer' => 'Marketer', - 'online_store' => 'Online store', - 'other' => 'Other', + 'title' => 'Getting started', + 'welcome' => 'Welcome to TryPost, :name', + 'welcome_anonymous' => 'Welcome to TryPost', + 'description' => 'Follow the steps below to see how TryPost works and publish your first post.', + 'skip' => 'Skip for now', + 'continue' => 'Continue to TryPost', + 'status' => [ + 'complete' => 'Complete', + 'todo' => 'To do', ], - 'goals_title' => 'What\'s your goal with TryPost?', - 'goals_description' => 'Pick everything that fits and we\'ll set TryPost up for you.', - 'goals' => [ - 'save_time' => 'Save time by posting everywhere at once', - 'ai_content' => 'Create posts faster with AI', - 'plan_calendar' => 'Plan my posts on a calendar', - 'stay_on_brand' => 'Keep every post on brand', - 'grow_audience' => 'Grow my audience and engagement', - 'drive_sales' => 'Get more traffic and sales', - 'manage_clients' => 'Manage several brands or clients', - 'team_collaboration' => 'Work with my team', - 'automate_api' => 'Automate posting with the API, MCP or code', - 'track_performance' => 'See how my posts perform', - 'just_exploring' => 'Just exploring for now', - 'other' => 'Something else', + 'mcp' => [ + 'title' => 'Connect your AI assistant', + 'description' => 'Add TryPost as an MCP server so your assistant can create and manage social posts for you.', + 'copy_step' => 'Copy your TryPost server URL', + 'open_step' => 'Open your AI assistant', + 'copy' => 'Copy URL', + 'copied' => 'MCP URL copied.', + 'connect' => 'Connect with :client', + 'clients' => [ + 'claude' => 'Open Settings → Connectors, add a custom connector, then paste the URL above.', + 'chatgpt' => 'Open Settings → Apps & Connectors, create a custom connector, then paste the URL above.', + ], ], - 'referral_source_title' => 'How did you find us?', - 'referral_source_description' => 'This helps us understand how people discover TryPost.', - 'referral_source' => [ - 'google' => 'Google or search', - 'x' => 'X (Twitter)', - 'linkedin' => 'LinkedIn', - 'youtube' => 'YouTube', - 'tiktok' => 'TikTok', - 'instagram' => 'Instagram or Threads', - 'reddit' => 'Reddit', - 'product_hunt' => 'Product Hunt', - 'ai_assistant' => 'AI assistant (ChatGPT, Claude…)', - 'friend' => 'Friend or colleague', - 'blog' => 'Blog, newsletter or article', - 'other' => 'Something else', + 'social' => [ + 'title' => 'Connect a social account', + 'description' => 'Choose at least one network where TryPost can publish your content.', ], - 'connect' => [ - 'title' => 'Connect your first network', - 'description' => 'Link at least one social account to start scheduling. You can add more anytime.', - 'must_connect' => 'Connect at least one network to continue.', + 'first_post' => [ + 'title' => 'Create your first post', + 'description' => 'Try this starter prompt with your connected assistant, or create the post directly in TryPost.', + 'prompt_label' => 'Sample prompt', + 'sample_prompt' => 'Create a friendly social post introducing my brand and adapt it for each connected network.', + 'copy_prompt' => 'Copy prompt', + 'copied' => 'Sample prompt copied.', + 'create_button' => 'Create your first post', + 'or' => 'or', + ], + 'ready' => [ + 'title' => 'You are ready to publish', + 'description' => 'You are set. Continue to TryPost and start planning your content.', ], ]; diff --git a/lang/en/settings.php b/lang/en/settings.php index 94858730c..972a10c7e 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -129,6 +129,7 @@ 'brand' => 'Brand', 'users' => 'Members', 'api_keys' => 'API Keys', + 'mcp' => 'MCP', ], 'title' => 'Workspace settings', 'logo_heading' => 'Workspace logo', diff --git a/lang/en/sidebar.php b/lang/en/sidebar.php index f9cddbd69..02841ba57 100644 --- a/lang/en/sidebar.php +++ b/lang/en/sidebar.php @@ -8,6 +8,8 @@ 'create_workspace' => 'Create workspace', 'create_post' => 'Create post', 'profile' => 'Profile', + 'my_account' => 'My account', + 'account_settings' => 'Account & billing', 'log_out' => 'Log out', 'workspace' => 'Workspace: :name', @@ -30,6 +32,8 @@ 'analytics' => 'Analytics', 'automations' => 'Automations', 'settings' => 'Settings', + 'onboarding' => 'Getting started', + 'onboarding_hint' => 'Finish setup', 'posts' => [ 'calendar' => 'Calendar', @@ -44,7 +48,9 @@ 'signatures' => 'Signatures', 'labels' => 'Labels', 'assets' => 'Assets', + 'settings' => 'Settings', 'api_keys' => 'API Keys', + 'mcp' => 'MCP', ], 'notifications' => 'Notifications', diff --git a/lang/en/welcome.php b/lang/en/welcome.php new file mode 100644 index 000000000..d5dae77a1 --- /dev/null +++ b/lang/en/welcome.php @@ -0,0 +1,52 @@ + 'What best describes you?', + 'description' => 'Choose the closest match and we\'ll tailor your experience.', + 'continue' => 'Continue', + 'checkout_owner_only' => 'Ask the account owner to finish checkout and start your subscription.', + 'progress' => 'Welcome progress', + 'go_to_step' => 'Go to step :step', + 'personas' => [ + 'creator' => 'Content creator', + 'freelancer' => 'Freelancer', + 'developer' => 'Developer', + 'startup' => 'Startup', + 'agency' => 'Agency', + 'small_business' => 'Small business', + 'marketer' => 'Marketer', + 'online_store' => 'Online store', + 'other' => 'Other', + ], + 'goals_title' => 'What\'s your goal?', + 'goals_description' => 'Pick everything that fits and we\'ll set TryPost up for you.', + 'goals' => [ + 'save_time' => 'Save time by posting everywhere at once', + 'ai_content' => 'Create posts faster with AI', + 'plan_calendar' => 'Plan my posts on a calendar', + 'stay_on_brand' => 'Keep every post on brand', + 'grow_audience' => 'Grow my audience and engagement', + 'drive_sales' => 'Get more traffic and sales', + 'manage_clients' => 'Manage several brands or clients', + 'just_exploring' => 'Just exploring for now', + 'other' => 'Something else', + ], + 'referral_source_title' => 'How did you find us?', + 'referral_source_description' => 'This helps us understand how people discover TryPost.', + 'referral_source' => [ + 'google' => 'Google or search', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram or Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'AI assistant (ChatGPT, Claude…)', + 'friend' => 'Friend or colleague', + 'blog' => 'Blog, newsletter or article', + 'other' => 'Something else', + ], +]; diff --git a/lang/es/mcp.php b/lang/es/mcp.php new file mode 100644 index 000000000..598bcad80 --- /dev/null +++ b/lang/es/mcp.php @@ -0,0 +1,41 @@ + 'MCP', + 'subtitle' => 'Conecta asistentes de IA para que creen y gestionen posts con tu cuenta de TryPost.', + 'step_add' => 'Pega el nombre, la URL o la config abajo en tu app. El inicio de sesión se abre en el navegador la primera vez.', + 'name_label' => 'Nombre', + 'url_label' => 'URL del servidor', + 'config_label' => 'Config', + 'connected_title' => 'Apps conectadas', + 'connected_description' => 'Asistentes con sesión de cualquiera en esta cuenta. Solo puedes desconectar los tuyos.', + 'connected_empty' => 'Nada conectado aún. Usa Claude, ChatGPT u otro cliente arriba.', + 'connected_by' => 'Conectado por :name', + 'disconnect' => 'Desconectar', + 'disconnect_title' => 'Desconectar app', + 'disconnect_confirm' => 'Esto cierra la sesión de la app en TryPost. Tendrá que reconectar para usar MCP otra vez.', + 'disconnected' => 'App desconectada.', + 'copied' => 'Copiado', + 'last_used' => 'Último uso', + 'never' => 'Nunca', + 'documentation_title' => 'Documentación', + 'documentation_description' => 'Guías por cliente, tools disponibles y solución de problemas.', + 'view_docs' => 'Ver documentación', + 'connector_name' => 'TryPost', + + 'other_clients_title' => 'Otras apps', + 'other_clients_description' => 'Cursor, VS Code, Claude Code y cualquier app que hable MCP.', + + 'clients' => [ + 'cursor' => 'Añade TryPost como servidor MCP remoto en Cursor.', + 'cursor_name' => 'Cursor', + 'vscode' => 'Pega la configuración de abajo en los ajustes MCP de VS Code.', + 'vscode_name' => 'VS Code', + 'claude_code' => 'Pega la configuración de abajo en los ajustes MCP de Claude Code.', + 'claude_code_name' => 'Claude Code', + 'other' => 'Funciona con cualquier cliente que lea una config mcpServers.', + 'other_name' => 'Otros', + ], +]; diff --git a/lang/es/onboarding.php b/lang/es/onboarding.php index 4fb16d047..72426577b 100644 --- a/lang/es/onboarding.php +++ b/lang/es/onboarding.php @@ -3,55 +3,45 @@ declare(strict_types=1); return [ - 'title' => 'Bienvenido a TryPost', - 'description' => 'Cuéntanos qué te describe mejor a ti o a tu negocio para personalizar tu experiencia.', - 'continue' => 'Continuar', - 'personas' => [ - 'creator' => 'Creador de contenido', - 'freelancer' => 'Freelancer', - 'developer' => 'Desarrollador', - 'startup' => 'Startup', - 'agency' => 'Agencia', - 'small_business' => 'Pequeña empresa', - 'marketer' => 'Profesional de marketing', - 'online_store' => 'Tienda online', - 'other' => 'Otro', + 'title' => 'Primeros pasos', + 'welcome' => 'Bienvenido a TryPost, :name', + 'welcome_anonymous' => 'Bienvenido a TryPost', + 'description' => 'Sigue los pasos a continuación para ver cómo funciona TryPost y publicar tu primer post.', + 'skip' => 'Omitir por ahora', + 'continue' => 'Continuar a TryPost', + 'status' => [ + 'complete' => 'Completado', + 'todo' => 'Pendiente', ], - 'goals_title' => '¿Cuál es tu objetivo con TryPost?', - 'goals_description' => 'Marca todo lo que encaje y adaptamos TryPost a ti.', - 'goals' => [ - 'save_time' => 'Ahorrar tiempo publicando en todas mis redes a la vez', - 'ai_content' => 'Crear publicaciones más rápido con IA', - 'plan_calendar' => 'Planificar mis publicaciones en un calendario', - 'stay_on_brand' => 'Mantener la coherencia de mi marca', - 'grow_audience' => 'Hacer crecer mi audiencia y engagement', - 'drive_sales' => 'Conseguir más tráfico y ventas', - 'manage_clients' => 'Gestionar varias marcas o clientes', - 'team_collaboration' => 'Trabajar con mi equipo', - 'automate_api' => 'Automatizar publicaciones con la API, MCP o código', - 'track_performance' => 'Ver cómo rinden mis publicaciones', - 'just_exploring' => 'Solo estoy explorando por ahora', - 'other' => 'Otra cosa', + 'mcp' => [ + 'title' => 'Conecta tu asistente de IA', + 'description' => 'Añade TryPost como servidor MCP para que tu asistente pueda crear y gestionar posts por ti.', + 'copy_step' => 'Copia la URL del servidor TryPost', + 'open_step' => 'Abre tu asistente de IA', + 'copy' => 'Copiar URL', + 'copied' => 'URL de MCP copiada.', + 'connect' => 'Conectar con :client', + 'clients' => [ + 'claude' => 'Abre Settings → Connectors, añade un conector personalizado y pega la URL de arriba.', + 'chatgpt' => 'Abre Settings → Apps & Connectors, crea un conector personalizado y pega la URL de arriba.', + ], ], - 'referral_source_title' => '¿Cómo nos encontraste?', - 'referral_source_description' => 'Esto nos ayuda a entender cómo la gente descubre TryPost.', - 'referral_source' => [ - 'google' => 'Google o búsqueda', - 'x' => 'X (Twitter)', - 'linkedin' => 'LinkedIn', - 'youtube' => 'YouTube', - 'tiktok' => 'TikTok', - 'instagram' => 'Instagram o Threads', - 'reddit' => 'Reddit', - 'product_hunt' => 'Product Hunt', - 'ai_assistant' => 'Asistente de IA (ChatGPT, Claude…)', - 'friend' => 'Amigo o colega', - 'blog' => 'Blog, newsletter o artículo', - 'other' => 'Otra cosa', + 'social' => [ + 'title' => 'Conecta una cuenta social', + 'description' => 'Elige al menos una red donde TryPost pueda publicar tu contenido.', ], - 'connect' => [ - 'title' => 'Conecta tu primera red', - 'description' => 'Vincula al menos una cuenta social para empezar a programar. Puedes añadir más cuando quieras.', - 'must_connect' => 'Conecta al menos una red para continuar.', + 'first_post' => [ + 'title' => 'Crea tu primer post', + 'description' => 'Prueba este prompt inicial con tu asistente conectado, o crea el post directamente en TryPost.', + 'prompt_label' => 'Prompt de ejemplo', + 'sample_prompt' => 'Crea un post social amable presentando mi marca y adáptalo para cada red conectada.', + 'copy_prompt' => 'Copiar prompt', + 'copied' => 'Prompt de ejemplo copiado.', + 'create_button' => 'Crear tu primer post', + 'or' => 'o', + ], + 'ready' => [ + 'title' => 'Listo para publicar', + 'description' => 'Todo listo. Continúa a TryPost y empieza a planificar tu contenido.', ], ]; diff --git a/lang/es/settings.php b/lang/es/settings.php index 34053b17c..04e05fafe 100644 --- a/lang/es/settings.php +++ b/lang/es/settings.php @@ -129,6 +129,7 @@ 'brand' => 'Marca', 'users' => 'Miembros', 'api_keys' => 'API Keys', + 'mcp' => 'MCP', ], 'title' => 'Configuración del workspace', 'logo_heading' => 'Logo del workspace', diff --git a/lang/es/sidebar.php b/lang/es/sidebar.php index c6f768d49..46accb9c6 100644 --- a/lang/es/sidebar.php +++ b/lang/es/sidebar.php @@ -8,6 +8,8 @@ 'create_workspace' => 'Crear workspace', 'create_post' => 'Crear post', 'profile' => 'Perfil', + 'my_account' => 'Mi cuenta', + 'account_settings' => 'Cuenta y facturación', 'log_out' => 'Cerrar sesión', 'workspace' => 'Workspace: :name', @@ -30,6 +32,8 @@ 'analytics' => 'Analytics', 'automations' => 'Automatizaciones', 'settings' => 'Configuración', + 'onboarding' => 'Primeros pasos', + 'onboarding_hint' => 'Termina la configuración', 'posts' => [ 'calendar' => 'Calendario', @@ -44,7 +48,9 @@ 'signatures' => 'Firmas', 'labels' => 'Etiquetas', 'assets' => 'Medios', + 'settings' => 'Configuración', 'api_keys' => 'API Keys', + 'mcp' => 'MCP', ], 'notifications' => 'Notificaciones', diff --git a/lang/es/welcome.php b/lang/es/welcome.php new file mode 100644 index 000000000..ba4524bfd --- /dev/null +++ b/lang/es/welcome.php @@ -0,0 +1,52 @@ + '¿Qué te describe mejor?', + 'description' => 'Elige la opción más cercana y personalizaremos tu experiencia.', + 'continue' => 'Continuar', + 'checkout_owner_only' => 'Pide al propietario de la cuenta que complete el checkout e inicie la suscripción.', + 'progress' => 'Progreso de bienvenida', + 'go_to_step' => 'Ir al paso :step', + 'personas' => [ + 'creator' => 'Creador de contenido', + 'freelancer' => 'Freelancer', + 'developer' => 'Desarrollador', + 'startup' => 'Startup', + 'agency' => 'Agencia', + 'small_business' => 'Pequeña empresa', + 'marketer' => 'Profesional de marketing', + 'online_store' => 'Tienda online', + 'other' => 'Otro', + ], + 'goals_title' => '¿Cuál es tu objetivo?', + 'goals_description' => 'Marca todo lo que encaje y adaptamos TryPost a ti.', + 'goals' => [ + 'save_time' => 'Ahorrar tiempo publicando en todas mis redes a la vez', + 'ai_content' => 'Crear publicaciones más rápido con IA', + 'plan_calendar' => 'Planificar mis publicaciones en un calendario', + 'stay_on_brand' => 'Mantener la coherencia de mi marca', + 'grow_audience' => 'Hacer crecer mi audiencia y engagement', + 'drive_sales' => 'Conseguir más tráfico y ventas', + 'manage_clients' => 'Gestionar varias marcas o clientes', + 'just_exploring' => 'Solo estoy explorando por ahora', + 'other' => 'Otra cosa', + ], + 'referral_source_title' => '¿Cómo nos encontraste?', + 'referral_source_description' => 'Esto nos ayuda a entender cómo la gente descubre TryPost.', + 'referral_source' => [ + 'google' => 'Google o búsqueda', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram o Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'Asistente de IA (ChatGPT, Claude…)', + 'friend' => 'Amigo o colega', + 'blog' => 'Blog, newsletter o artículo', + 'other' => 'Otra cosa', + ], +]; diff --git a/lang/fr/mcp.php b/lang/fr/mcp.php new file mode 100644 index 000000000..21b10dfe0 --- /dev/null +++ b/lang/fr/mcp.php @@ -0,0 +1,41 @@ + 'MCP', + 'subtitle' => 'Connectez des assistants IA pour créer et gérer des posts avec votre compte TryPost.', + 'step_add' => 'Collez le nom, l’URL ou la config ci-dessous dans votre app. La connexion s’ouvre dans le navigateur la première fois.', + 'name_label' => 'Nom', + 'url_label' => 'URL du serveur', + 'config_label' => 'Config', + 'connected_title' => 'Apps connectées', + 'connected_description' => 'Assistants connectés par n’importe qui sur ce compte. Vous ne pouvez déconnecter que les vôtres.', + 'connected_empty' => 'Rien de connecté pour l’instant. Utilisez Claude, ChatGPT ou un autre client ci-dessus.', + 'connected_by' => 'Connecté par :name', + 'disconnect' => 'Déconnecter', + 'disconnect_title' => 'Déconnecter l’app', + 'disconnect_confirm' => 'Cela déconnecte l’app de TryPost. Elle devra se reconnecter pour utiliser MCP à nouveau.', + 'disconnected' => 'App déconnectée.', + 'copied' => 'Copié', + 'last_used' => 'Dernière utilisation', + 'never' => 'Jamais', + 'documentation_title' => 'Documentation', + 'documentation_description' => 'Guides par client, tools disponibles et dépannage.', + 'view_docs' => 'Voir la documentation', + 'connector_name' => 'TryPost', + + 'other_clients_title' => 'Autres apps', + 'other_clients_description' => 'Cursor, VS Code, Claude Code et toute app qui parle MCP.', + + 'clients' => [ + 'cursor' => 'Ajoutez TryPost comme serveur MCP distant dans Cursor.', + 'cursor_name' => 'Cursor', + 'vscode' => 'Collez la configuration ci-dessous dans les paramètres MCP de VS Code.', + 'vscode_name' => 'VS Code', + 'claude_code' => 'Collez la configuration ci-dessous dans les paramètres MCP de Claude Code.', + 'claude_code_name' => 'Claude Code', + 'other' => 'Fonctionne avec tout client qui lit une config mcpServers.', + 'other_name' => 'Autres', + ], +]; diff --git a/lang/fr/onboarding.php b/lang/fr/onboarding.php index 7d103c102..41c4a7b62 100644 --- a/lang/fr/onboarding.php +++ b/lang/fr/onboarding.php @@ -3,55 +3,45 @@ declare(strict_types=1); return [ - 'title' => 'Bienvenue sur TryPost', - 'description' => 'Dites-nous ce qui vous décrit le mieux, vous ou votre entreprise, afin que nous puissions personnaliser votre expérience.', - 'continue' => 'Continuer', - 'personas' => [ - 'creator' => 'Créateur de contenu', - 'freelancer' => 'Freelance', - 'developer' => 'Développeur', - 'startup' => 'Startup', - 'agency' => 'Agence', - 'small_business' => 'Petite entreprise', - 'marketer' => 'Marketeur', - 'online_store' => 'Boutique en ligne', - 'other' => 'Autre', + 'title' => 'Premiers pas', + 'welcome' => 'Bienvenue sur TryPost, :name', + 'welcome_anonymous' => 'Bienvenue sur TryPost', + 'description' => 'Suivez les étapes ci-dessous pour découvrir comment TryPost fonctionne et publier votre premier post.', + 'skip' => 'Passer pour l’instant', + 'continue' => 'Continuer vers TryPost', + 'status' => [ + 'complete' => 'Terminé', + 'todo' => 'À faire', ], - 'goals_title' => 'Quel est votre objectif avec TryPost ?', - 'goals_description' => 'Choisissez tout ce qui vous correspond et nous configurerons TryPost pour vous.', - 'goals' => [ - 'save_time' => 'Gagner du temps en publiant partout à la fois', - 'ai_content' => 'Créer des publications plus vite avec l\'IA', - 'plan_calendar' => 'Planifier mes publications sur un calendrier', - 'stay_on_brand' => 'Garder chaque publication fidèle à ma marque', - 'grow_audience' => 'Développer mon audience et mon engagement', - 'drive_sales' => 'Obtenir plus de trafic et de ventes', - 'manage_clients' => 'Gérer plusieurs marques ou clients', - 'team_collaboration' => 'Travailler avec mon équipe', - 'automate_api' => 'Automatiser la publication avec l\'API, le MCP ou du code', - 'track_performance' => 'Voir les performances de mes publications', - 'just_exploring' => 'Je découvre pour l\'instant', - 'other' => 'Autre chose', + 'mcp' => [ + 'title' => 'Connectez votre assistant IA', + 'description' => 'Ajoutez TryPost comme serveur MCP pour que votre assistant puisse créer et gérer vos posts sociaux.', + 'copy_step' => 'Copiez l’URL du serveur TryPost', + 'open_step' => 'Ouvrez votre assistant IA', + 'copy' => 'Copier l’URL', + 'copied' => 'URL MCP copiée.', + 'connect' => 'Connecter avec :client', + 'clients' => [ + 'claude' => 'Ouvrez Settings → Connectors, ajoutez un connecteur personnalisé, puis collez l’URL ci-dessus.', + 'chatgpt' => 'Ouvrez Settings → Apps & Connectors, créez un connecteur personnalisé, puis collez l’URL ci-dessus.', + ], ], - 'referral_source_title' => 'Comment nous avez-vous connus ?', - 'referral_source_description' => 'Cela nous aide à comprendre comment les gens découvrent TryPost.', - 'referral_source' => [ - 'google' => 'Google ou recherche', - 'x' => 'X (Twitter)', - 'linkedin' => 'LinkedIn', - 'youtube' => 'YouTube', - 'tiktok' => 'TikTok', - 'instagram' => 'Instagram ou Threads', - 'reddit' => 'Reddit', - 'product_hunt' => 'Product Hunt', - 'ai_assistant' => 'Assistant IA (ChatGPT, Claude…)', - 'friend' => 'Ami ou collègue', - 'blog' => 'Blog, newsletter ou article', - 'other' => 'Autre chose', + 'social' => [ + 'title' => 'Connectez un compte social', + 'description' => 'Choisissez au moins un réseau où TryPost pourra publier votre contenu.', ], - 'connect' => [ - 'title' => 'Connectez votre premier réseau', - 'description' => 'Associez au moins un compte social pour commencer à programmer. Vous pourrez en ajouter d\'autres à tout moment.', - 'must_connect' => 'Connectez au moins un réseau pour continuer.', + 'first_post' => [ + 'title' => 'Créez votre premier post', + 'description' => 'Essayez ce prompt de démarrage avec votre assistant connecté, ou créez le post directement dans TryPost.', + 'prompt_label' => 'Prompt d’exemple', + 'sample_prompt' => 'Crée un post social amical présentant ma marque et adapte-le pour chaque réseau connecté.', + 'copy_prompt' => 'Copier le prompt', + 'copied' => 'Prompt d’exemple copié.', + 'create_button' => 'Créer votre premier post', + 'or' => 'ou', + ], + 'ready' => [ + 'title' => 'Vous êtes prêt à publier', + 'description' => 'Tout est bon. Continuez vers TryPost et commencez à planifier votre contenu.', ], ]; diff --git a/lang/fr/settings.php b/lang/fr/settings.php index 83e092baf..e9676df27 100644 --- a/lang/fr/settings.php +++ b/lang/fr/settings.php @@ -129,6 +129,7 @@ 'brand' => 'Marque', 'users' => 'Membres', 'api_keys' => 'Clés API', + 'mcp' => 'MCP', ], 'title' => 'Paramètres de l\'espace de travail', 'logo_heading' => 'Logo de l\'espace de travail', diff --git a/lang/fr/sidebar.php b/lang/fr/sidebar.php index a63250252..845f6ebca 100644 --- a/lang/fr/sidebar.php +++ b/lang/fr/sidebar.php @@ -8,6 +8,8 @@ 'create_workspace' => 'Créer un espace de travail', 'create_post' => 'Créer une publication', 'profile' => 'Profil', + 'my_account' => 'Mon compte', + 'account_settings' => 'Compte et facturation', 'log_out' => 'Se déconnecter', 'workspace' => 'Espace de travail : :name', @@ -30,6 +32,8 @@ 'analytics' => 'Statistiques', 'automations' => 'Automatisations', 'settings' => 'Paramètres', + 'onboarding' => 'Premiers pas', + 'onboarding_hint' => 'Terminer la configuration', 'posts' => [ 'calendar' => 'Calendrier', @@ -44,7 +48,9 @@ 'signatures' => 'Signatures', 'labels' => 'Étiquettes', 'assets' => 'Médias', + 'settings' => 'Paramètres', 'api_keys' => 'Clés API', + 'mcp' => 'MCP', ], 'notifications' => 'Notifications', diff --git a/lang/fr/welcome.php b/lang/fr/welcome.php new file mode 100644 index 000000000..6edc92e8e --- /dev/null +++ b/lang/fr/welcome.php @@ -0,0 +1,52 @@ + 'Qu\'est-ce qui vous décrit le mieux ?', + 'description' => 'Choisissez l\'option la plus proche et nous personnaliserons votre expérience.', + 'continue' => 'Continuer', + 'checkout_owner_only' => 'Demandez au propriétaire du compte de finaliser le paiement et de démarrer l\'abonnement.', + 'progress' => 'Progression d’accueil', + 'go_to_step' => 'Aller à l’étape :step', + 'personas' => [ + 'creator' => 'Créateur de contenu', + 'freelancer' => 'Freelance', + 'developer' => 'Développeur', + 'startup' => 'Startup', + 'agency' => 'Agence', + 'small_business' => 'Petite entreprise', + 'marketer' => 'Marketeur', + 'online_store' => 'Boutique en ligne', + 'other' => 'Autre', + ], + 'goals_title' => 'Quel est votre objectif ?', + 'goals_description' => 'Choisissez tout ce qui vous correspond et nous configurerons TryPost pour vous.', + 'goals' => [ + 'save_time' => 'Gagner du temps en publiant partout à la fois', + 'ai_content' => 'Créer des publications plus vite avec l\'IA', + 'plan_calendar' => 'Planifier mes publications sur un calendrier', + 'stay_on_brand' => 'Garder chaque publication fidèle à ma marque', + 'grow_audience' => 'Développer mon audience et mon engagement', + 'drive_sales' => 'Obtenir plus de trafic et de ventes', + 'manage_clients' => 'Gérer plusieurs marques ou clients', + 'just_exploring' => 'Je découvre pour l\'instant', + 'other' => 'Autre chose', + ], + 'referral_source_title' => 'Comment nous avez-vous connus ?', + 'referral_source_description' => 'Cela nous aide à comprendre comment les gens découvrent TryPost.', + 'referral_source' => [ + 'google' => 'Google ou recherche', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram ou Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'Assistant IA (ChatGPT, Claude…)', + 'friend' => 'Ami ou collègue', + 'blog' => 'Blog, newsletter ou article', + 'other' => 'Autre chose', + ], +]; diff --git a/lang/it/mcp.php b/lang/it/mcp.php new file mode 100644 index 000000000..75bb16a28 --- /dev/null +++ b/lang/it/mcp.php @@ -0,0 +1,41 @@ + 'MCP', + 'subtitle' => 'Collega assistenti IA così possono creare e gestire post con il tuo account TryPost.', + 'step_add' => 'Incolla nome, URL o config qui sotto nella tua app. Il login si apre nel browser al primo collegamento.', + 'name_label' => 'Nome', + 'url_label' => 'URL del server', + 'config_label' => 'Config', + 'connected_title' => 'App collegate', + 'connected_description' => 'Assistenti con accesso di chiunque su questo account. Puoi disconnettere solo i tuoi.', + 'connected_empty' => 'Nessuna connessione ancora. Usa Claude, ChatGPT o un altro client sopra.', + 'connected_by' => 'Connesso da :name', + 'disconnect' => 'Scollega', + 'disconnect_title' => 'Scollega app', + 'disconnect_confirm' => 'Questo scollega l’app da TryPost. Dovrà riconnettersi prima di usare di nuovo MCP.', + 'disconnected' => 'App scollegata.', + 'copied' => 'Copiato', + 'last_used' => 'Ultimo uso', + 'never' => 'Mai', + 'documentation_title' => 'Documentazione', + 'documentation_description' => 'Guide per client, tools disponibili e risoluzione problemi.', + 'view_docs' => 'Vedi documentazione', + 'connector_name' => 'TryPost', + + 'other_clients_title' => 'Altre app', + 'other_clients_description' => 'Cursor, VS Code, Claude Code e qualsiasi app che parla MCP.', + + 'clients' => [ + 'cursor' => 'Aggiungi TryPost come server MCP remoto in Cursor.', + 'cursor_name' => 'Cursor', + 'vscode' => 'Incolla la config qui sotto nelle impostazioni MCP di VS Code.', + 'vscode_name' => 'VS Code', + 'claude_code' => 'Incolla la config qui sotto nelle impostazioni MCP di Claude Code.', + 'claude_code_name' => 'Claude Code', + 'other' => 'Funziona con qualsiasi client che legge una config mcpServers.', + 'other_name' => 'Altri', + ], +]; diff --git a/lang/it/onboarding.php b/lang/it/onboarding.php index 0beba0331..89077d215 100644 --- a/lang/it/onboarding.php +++ b/lang/it/onboarding.php @@ -3,55 +3,45 @@ declare(strict_types=1); return [ - 'title' => 'Benvenuto su TryPost', - 'description' => 'Dicci cosa descrive meglio te o la tua attività così possiamo personalizzare la tua esperienza.', - 'continue' => 'Continua', - 'personas' => [ - 'creator' => 'Creatore di contenuti', - 'freelancer' => 'Freelance', - 'developer' => 'Sviluppatore', - 'startup' => 'Startup', - 'agency' => 'Agenzia', - 'small_business' => 'Piccola impresa', - 'marketer' => 'Marketer', - 'online_store' => 'Negozio online', - 'other' => 'Altro', + 'title' => 'Primi passi', + 'welcome' => 'Benvenuto su TryPost, :name', + 'welcome_anonymous' => 'Benvenuto su TryPost', + 'description' => 'Segui i passaggi qui sotto per vedere come funziona TryPost e pubblicare il tuo primo post.', + 'skip' => 'Salta per ora', + 'continue' => 'Continua su TryPost', + 'status' => [ + 'complete' => 'Completato', + 'todo' => 'Da fare', ], - 'goals_title' => 'Qual è il tuo obiettivo con TryPost?', - 'goals_description' => 'Scegli tutto ciò che fa per te e configureremo TryPost per te.', - 'goals' => [ - 'save_time' => 'Risparmiare tempo pubblicando ovunque in una volta', - 'ai_content' => 'Creare post più velocemente con l\'IA', - 'plan_calendar' => 'Pianificare i miei post su un calendario', - 'stay_on_brand' => 'Mantenere ogni post in linea con il brand', - 'grow_audience' => 'Far crescere il mio pubblico e il coinvolgimento', - 'drive_sales' => 'Ottenere più traffico e vendite', - 'manage_clients' => 'Gestire più brand o clienti', - 'team_collaboration' => 'Lavorare con il mio team', - 'automate_api' => 'Automatizzare la pubblicazione con API, MCP o codice', - 'track_performance' => 'Vedere come vanno i miei post', - 'just_exploring' => 'Sto solo dando un\'occhiata', - 'other' => 'Qualcos\'altro', + 'mcp' => [ + 'title' => 'Collega il tuo assistente IA', + 'description' => 'Aggiungi TryPost come server MCP così il tuo assistente può creare e gestire i post social per te.', + 'copy_step' => 'Copia l’URL del server TryPost', + 'open_step' => 'Apri il tuo assistente IA', + 'copy' => 'Copia URL', + 'copied' => 'URL MCP copiato.', + 'connect' => 'Collega con :client', + 'clients' => [ + 'claude' => 'Apri Settings → Connectors, aggiungi un connettore personalizzato e incolla l’URL qui sopra.', + 'chatgpt' => 'Apri Settings → Apps & Connectors, crea un connettore personalizzato e incolla l’URL qui sopra.', + ], ], - 'referral_source_title' => 'Come ci hai trovato?', - 'referral_source_description' => 'Questo ci aiuta a capire come le persone scoprono TryPost.', - 'referral_source' => [ - 'google' => 'Google o ricerca', - 'x' => 'X (Twitter)', - 'linkedin' => 'LinkedIn', - 'youtube' => 'YouTube', - 'tiktok' => 'TikTok', - 'instagram' => 'Instagram o Threads', - 'reddit' => 'Reddit', - 'product_hunt' => 'Product Hunt', - 'ai_assistant' => 'Assistente IA (ChatGPT, Claude…)', - 'friend' => 'Amico o collega', - 'blog' => 'Blog, newsletter o articolo', - 'other' => 'Qualcos\'altro', + 'social' => [ + 'title' => 'Collega un account social', + 'description' => 'Scegli almeno una rete dove TryPost possa pubblicare i tuoi contenuti.', ], - 'connect' => [ - 'title' => 'Collega la tua prima rete', - 'description' => 'Collega almeno un account social per iniziare a programmare. Puoi aggiungerne altri in qualsiasi momento.', - 'must_connect' => 'Collega almeno una rete per continuare.', + 'first_post' => [ + 'title' => 'Crea il tuo primo post', + 'description' => 'Prova questo prompt iniziale con il tuo assistente collegato, oppure crea il post direttamente in TryPost.', + 'prompt_label' => 'Prompt di esempio', + 'sample_prompt' => 'Crea un post social amichevole per presentare il mio brand e adattalo a ogni rete collegata.', + 'copy_prompt' => 'Copia prompt', + 'copied' => 'Prompt di esempio copiato.', + 'create_button' => 'Crea il tuo primo post', + 'or' => 'oppure', + ], + 'ready' => [ + 'title' => 'Sei pronto a pubblicare', + 'description' => 'Tutto a posto. Continua su TryPost e inizia a pianificare i tuoi contenuti.', ], ]; diff --git a/lang/it/settings.php b/lang/it/settings.php index e34c3e9d3..145a71e0d 100644 --- a/lang/it/settings.php +++ b/lang/it/settings.php @@ -129,6 +129,7 @@ 'brand' => 'Brand', 'users' => 'Membri', 'api_keys' => 'Chiavi API', + 'mcp' => 'MCP', ], 'title' => 'Impostazioni del workspace', 'logo_heading' => 'Logo del workspace', diff --git a/lang/it/sidebar.php b/lang/it/sidebar.php index fa517924a..5baa2ed16 100644 --- a/lang/it/sidebar.php +++ b/lang/it/sidebar.php @@ -8,6 +8,8 @@ 'create_workspace' => 'Crea workspace', 'create_post' => 'Crea post', 'profile' => 'Profilo', + 'my_account' => 'Il mio account', + 'account_settings' => 'Account e fatturazione', 'log_out' => 'Esci', 'workspace' => 'Workspace: :name', @@ -30,6 +32,8 @@ 'analytics' => 'Statistiche', 'automations' => 'Automazioni', 'settings' => 'Impostazioni', + 'onboarding' => 'Primi passi', + 'onboarding_hint' => 'Completa la configurazione', 'posts' => [ 'calendar' => 'Calendario', @@ -44,7 +48,9 @@ 'signatures' => 'Firme', 'labels' => 'Etichette', 'assets' => 'Risorse', + 'settings' => 'Impostazioni', 'api_keys' => 'Chiavi API', + 'mcp' => 'MCP', ], 'notifications' => 'Notifiche', diff --git a/lang/it/welcome.php b/lang/it/welcome.php new file mode 100644 index 000000000..36133819a --- /dev/null +++ b/lang/it/welcome.php @@ -0,0 +1,52 @@ + 'Cosa ti descrive meglio?', + 'description' => 'Scegli l\'opzione più vicina e personalizzeremo la tua esperienza.', + 'continue' => 'Continua', + 'checkout_owner_only' => 'Chiedi al proprietario dell\'account di completare il checkout e avviare l\'abbonamento.', + 'progress' => 'Progresso di benvenuto', + 'go_to_step' => 'Vai al passaggio :step', + 'personas' => [ + 'creator' => 'Creatore di contenuti', + 'freelancer' => 'Freelance', + 'developer' => 'Sviluppatore', + 'startup' => 'Startup', + 'agency' => 'Agenzia', + 'small_business' => 'Piccola impresa', + 'marketer' => 'Marketer', + 'online_store' => 'Negozio online', + 'other' => 'Altro', + ], + 'goals_title' => 'Qual è il tuo obiettivo?', + 'goals_description' => 'Scegli tutto ciò che fa per te e configureremo TryPost per te.', + 'goals' => [ + 'save_time' => 'Risparmiare tempo pubblicando ovunque in una volta', + 'ai_content' => 'Creare post più velocemente con l\'IA', + 'plan_calendar' => 'Pianificare i miei post su un calendario', + 'stay_on_brand' => 'Mantenere ogni post in linea con il brand', + 'grow_audience' => 'Far crescere il mio pubblico e il coinvolgimento', + 'drive_sales' => 'Ottenere più traffico e vendite', + 'manage_clients' => 'Gestire più brand o clienti', + 'just_exploring' => 'Sto solo dando un\'occhiata', + 'other' => 'Qualcos\'altro', + ], + 'referral_source_title' => 'Come ci hai trovato?', + 'referral_source_description' => 'Questo ci aiuta a capire come le persone scoprono TryPost.', + 'referral_source' => [ + 'google' => 'Google o ricerca', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram o Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'Assistente IA (ChatGPT, Claude…)', + 'friend' => 'Amico o collega', + 'blog' => 'Blog, newsletter o articolo', + 'other' => 'Qualcos\'altro', + ], +]; diff --git a/lang/ja/mcp.php b/lang/ja/mcp.php new file mode 100644 index 000000000..42a906b32 --- /dev/null +++ b/lang/ja/mcp.php @@ -0,0 +1,41 @@ + 'MCP', + 'subtitle' => 'TryPostアカウントで投稿の作成・管理ができるよう、AIアシスタントを接続します。', + 'step_add' => '下の名前・URL・設定をアプリに貼り付けてください。初回接続時はブラウザでログインが開きます。', + 'name_label' => '名前', + 'url_label' => 'サーバーURL', + 'config_label' => '設定', + 'connected_title' => '接続済みアプリ', + 'connected_description' => 'このアカウントの誰かがサインインしたアシスタント。切断できるのは自分の接続だけです。', + 'connected_empty' => 'まだ接続がありません。上の Claude、ChatGPT、または他のクライアントを使ってください。', + 'connected_by' => ':name が接続', + 'disconnect' => '切断', + 'disconnect_title' => 'アプリを切断', + 'disconnect_confirm' => 'TryPostからアプリを切断します。再度MCPを使うには再接続が必要です。', + 'disconnected' => 'アプリを切断しました。', + 'copied' => 'コピーしました', + 'last_used' => '最終使用', + 'never' => 'なし', + 'documentation_title' => 'ドキュメント', + 'documentation_description' => 'クライアント別のセットアップ、利用可能なツール、トラブルシューティング。', + 'view_docs' => 'ドキュメントを見る', + 'connector_name' => 'TryPost', + + 'other_clients_title' => 'その他のアプリ', + 'other_clients_description' => 'Cursor、VS Code、Claude Code、その他MCP対応アプリ。', + + 'clients' => [ + 'cursor' => 'CursorでTryPostをリモートMCPサーバーとして追加します。', + 'cursor_name' => 'Cursor', + 'vscode' => '下の設定をVS CodeのMCP設定に貼り付けます。', + 'vscode_name' => 'VS Code', + 'claude_code' => '下の設定をClaude CodeのMCP設定に貼り付けます。', + 'claude_code_name' => 'Claude Code', + 'other' => 'mcpServers設定を読むクライアントならどれでも使えます。', + 'other_name' => 'その他', + ], +]; diff --git a/lang/ja/onboarding.php b/lang/ja/onboarding.php index db92adcf2..3e09d930d 100644 --- a/lang/ja/onboarding.php +++ b/lang/ja/onboarding.php @@ -3,55 +3,45 @@ declare(strict_types=1); return [ - 'title' => 'TryPost へようこそ', - 'description' => 'あなたやあなたのビジネスに最も当てはまるものを教えてください。体験を最適化します。', - 'continue' => '続ける', - 'personas' => [ - 'creator' => 'コンテンツクリエイター', - 'freelancer' => 'フリーランス', - 'developer' => '開発者', - 'startup' => 'スタートアップ', - 'agency' => '代理店', - 'small_business' => '中小企業', - 'marketer' => 'マーケター', - 'online_store' => 'オンラインストア', - 'other' => 'その他', + 'title' => 'はじめに', + 'welcome' => 'TryPostへようこそ、:nameさん', + 'welcome_anonymous' => 'TryPostへようこそ', + 'description' => '以下のステップでTryPostの使い方を確認し、最初の投稿を公開しましょう。', + 'skip' => '今はスキップ', + 'continue' => 'TryPostへ進む', + 'status' => [ + 'complete' => '完了', + 'todo' => '未完了', ], - 'goals_title' => 'TryPost での目標は何ですか?', - 'goals_description' => '当てはまるものをすべて選んでください。TryPost をあなた向けに設定します。', - 'goals' => [ - 'save_time' => 'すべての場所へ一度に投稿して時間を節約する', - 'ai_content' => 'AI でより速く投稿を作成する', - 'plan_calendar' => 'カレンダーで投稿を計画する', - 'stay_on_brand' => 'すべての投稿をブランドに沿ったものにする', - 'grow_audience' => 'オーディエンスとエンゲージメントを増やす', - 'drive_sales' => 'トラフィックと売上を増やす', - 'manage_clients' => '複数のブランドやクライアントを管理する', - 'team_collaboration' => 'チームで作業する', - 'automate_api' => 'API、MCP、コードで投稿を自動化する', - 'track_performance' => '投稿のパフォーマンスを確認する', - 'just_exploring' => '今はまだ様子を見ている', - 'other' => 'その他', + 'mcp' => [ + 'title' => 'AIアシスタントを接続', + 'description' => 'TryPostをMCPサーバーとして追加すると、アシスタントがSNS投稿の作成・管理を行えます。', + 'copy_step' => 'TryPostサーバーURLをコピー', + 'open_step' => 'AIアシスタントを開く', + 'copy' => 'URLをコピー', + 'copied' => 'MCP URLをコピーしました。', + 'connect' => ':clientで接続', + 'clients' => [ + 'claude' => 'Settings → Connectors を開き、カスタムコネクタを追加して上のURLを貼り付けます。', + 'chatgpt' => 'Settings → Apps & Connectors を開き、カスタムコネクタを作成して上のURLを貼り付けます。', + ], ], - 'referral_source_title' => 'どこで私たちを知りましたか?', - 'referral_source_description' => 'これは、人々がどのように TryPost を見つけるかを理解するのに役立ちます。', - 'referral_source' => [ - 'google' => 'Google または検索', - 'x' => 'X (Twitter)', - 'linkedin' => 'LinkedIn', - 'youtube' => 'YouTube', - 'tiktok' => 'TikTok', - 'instagram' => 'Instagram または Threads', - 'reddit' => 'Reddit', - 'product_hunt' => 'Product Hunt', - 'ai_assistant' => 'AI アシスタント(ChatGPT、Claude など)', - 'friend' => '友人または同僚', - 'blog' => 'ブログ、ニュースレター、記事', - 'other' => 'その他', + 'social' => [ + 'title' => 'SNSアカウントを接続', + 'description' => 'TryPostが投稿できるネットワークを少なくとも1つ選びます。', ], - 'connect' => [ - 'title' => '最初のネットワークを接続', - 'description' => 'スケジュールを始めるには、少なくとも 1 つのソーシャルアカウントを連携してください。後からいつでも追加できます。', - 'must_connect' => '続けるには、少なくとも 1 つのネットワークを接続してください。', + 'first_post' => [ + 'title' => '最初の投稿を作成', + 'description' => '接続したアシスタントでこのスタータープロンプトを試すか、TryPostで直接投稿を作成します。', + 'prompt_label' => 'サンプルプロンプト', + 'sample_prompt' => 'ブランドを紹介する親しみやすいSNS投稿を作成し、接続済みの各ネットワーク向けに最適化してください。', + 'copy_prompt' => 'プロンプトをコピー', + 'copied' => 'サンプルプロンプトをコピーしました。', + 'create_button' => '最初の投稿を作成', + 'or' => 'または', + ], + 'ready' => [ + 'title' => '公開の準備ができました', + 'description' => '設定完了です。TryPostへ進み、コンテンツの計画を始めましょう。', ], ]; diff --git a/lang/ja/settings.php b/lang/ja/settings.php index 80eae74b4..6d56f7c73 100644 --- a/lang/ja/settings.php +++ b/lang/ja/settings.php @@ -129,6 +129,7 @@ 'brand' => 'ブランド', 'users' => 'メンバー', 'api_keys' => 'API キー', + 'mcp' => 'MCP', ], 'title' => 'ワークスペース設定', 'logo_heading' => 'ワークスペースのロゴ', diff --git a/lang/ja/sidebar.php b/lang/ja/sidebar.php index 09ff7c59d..403c08162 100644 --- a/lang/ja/sidebar.php +++ b/lang/ja/sidebar.php @@ -8,6 +8,8 @@ 'create_workspace' => 'ワークスペースを作成', 'create_post' => '投稿を作成', 'profile' => 'プロフィール', + 'my_account' => 'マイアカウント', + 'account_settings' => 'アカウントと請求', 'log_out' => 'ログアウト', 'workspace' => 'ワークスペース: :name', @@ -30,6 +32,8 @@ 'analytics' => 'アナリティクス', 'automations' => 'オートメーション', 'settings' => '設定', + 'onboarding' => 'はじめに', + 'onboarding_hint' => 'セットアップを完了', 'posts' => [ 'calendar' => 'カレンダー', @@ -44,7 +48,9 @@ 'signatures' => '署名', 'labels' => 'ラベル', 'assets' => 'アセット', + 'settings' => '設定', 'api_keys' => 'API キー', + 'mcp' => 'MCP', ], 'notifications' => '通知', diff --git a/lang/ja/welcome.php b/lang/ja/welcome.php new file mode 100644 index 000000000..f42134166 --- /dev/null +++ b/lang/ja/welcome.php @@ -0,0 +1,52 @@ + 'あなたに一番近いのは?', + 'description' => '近いものを選ぶと、体験を最適化します。', + 'continue' => '続ける', + 'checkout_owner_only' => 'アカウントのオーナーにチェックアウトを完了してサブスクリプションを開始するよう依頼してください。', + 'progress' => 'ようこそ進捗', + 'go_to_step' => 'ステップ :step へ', + 'personas' => [ + 'creator' => 'コンテンツクリエイター', + 'freelancer' => 'フリーランス', + 'developer' => '開発者', + 'startup' => 'スタートアップ', + 'agency' => '代理店', + 'small_business' => '中小企業', + 'marketer' => 'マーケター', + 'online_store' => 'オンラインストア', + 'other' => 'その他', + ], + 'goals_title' => '目標は何ですか?', + 'goals_description' => '当てはまるものをすべて選んでください。TryPost をあなた向けに設定します。', + 'goals' => [ + 'save_time' => 'すべての場所へ一度に投稿して時間を節約する', + 'ai_content' => 'AI でより速く投稿を作成する', + 'plan_calendar' => 'カレンダーで投稿を計画する', + 'stay_on_brand' => 'すべての投稿をブランドに沿ったものにする', + 'grow_audience' => 'オーディエンスとエンゲージメントを増やす', + 'drive_sales' => 'トラフィックと売上を増やす', + 'manage_clients' => '複数のブランドやクライアントを管理する', + 'just_exploring' => '今はまだ様子を見ている', + 'other' => 'その他', + ], + 'referral_source_title' => 'どこで私たちを知りましたか?', + 'referral_source_description' => 'これは、人々がどのように TryPost を見つけるかを理解するのに役立ちます。', + 'referral_source' => [ + 'google' => 'Google または検索', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram または Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'AI アシスタント(ChatGPT、Claude など)', + 'friend' => '友人または同僚', + 'blog' => 'ブログ、ニュースレター、記事', + 'other' => 'その他', + ], +]; diff --git a/lang/ko/mcp.php b/lang/ko/mcp.php new file mode 100644 index 000000000..1048d656b --- /dev/null +++ b/lang/ko/mcp.php @@ -0,0 +1,41 @@ + 'MCP', + 'subtitle' => 'TryPost 계정으로 게시물을 만들고 관리할 수 있도록 AI 어시스턴트를 연결하세요.', + 'step_add' => '아래 이름, URL 또는 설정을 앱에 붙여넣으세요. 처음 연결할 때 브라우저에서 로그인이 열립니다.', + 'name_label' => '이름', + 'url_label' => '서버 URL', + 'config_label' => '설정', + 'connected_title' => '연결된 앱', + 'connected_description' => '이 계정의 누구나 로그인한 어시스턴트입니다. 본인 연결만 해제할 수 있습니다.', + 'connected_empty' => '아직 연결된 앱이 없습니다. 위의 Claude, ChatGPT 또는 다른 클라이언트를 사용하세요.', + 'connected_by' => ':name님이 연결함', + 'disconnect' => '연결 해제', + 'disconnect_title' => '앱 연결 해제', + 'disconnect_confirm' => 'TryPost에서 앱 로그인을 해제합니다. MCP를 다시 쓰려면 다시 연결해야 합니다.', + 'disconnected' => '앱 연결이 해제되었습니다.', + 'copied' => '복사됨', + 'last_used' => '최근 사용', + 'never' => '없음', + 'documentation_title' => '문서', + 'documentation_description' => '클라이언트별 설정 가이드, 사용 가능한 도구, 문제 해결.', + 'view_docs' => '문서 보기', + 'connector_name' => 'TryPost', + + 'other_clients_title' => '다른 앱', + 'other_clients_description' => 'Cursor, VS Code, Claude Code 및 MCP를 지원하는 모든 앱.', + + 'clients' => [ + 'cursor' => 'Cursor에서 TryPost를 원격 MCP 서버로 추가하세요.', + 'cursor_name' => 'Cursor', + 'vscode' => '아래 설정을 VS Code MCP 설정에 붙여넣으세요.', + 'vscode_name' => 'VS Code', + 'claude_code' => '아래 설정을 Claude Code MCP 설정에 붙여넣으세요.', + 'claude_code_name' => 'Claude Code', + 'other' => 'mcpServers 설정을 읽는 모든 클라이언트에서 동작합니다.', + 'other_name' => '기타', + ], +]; diff --git a/lang/ko/onboarding.php b/lang/ko/onboarding.php index fc954799f..57c8428f2 100644 --- a/lang/ko/onboarding.php +++ b/lang/ko/onboarding.php @@ -3,55 +3,45 @@ declare(strict_types=1); return [ - 'title' => 'TryPost에 오신 것을 환영합니다', - 'description' => '회원님이나 비즈니스를 가장 잘 설명하는 항목을 알려주시면 맞춤형 경험을 제공해 드립니다.', - 'continue' => '계속', - 'personas' => [ - 'creator' => '콘텐츠 크리에이터', - 'freelancer' => '프리랜서', - 'developer' => '개발자', - 'startup' => '스타트업', - 'agency' => '에이전시', - 'small_business' => '소상공인', - 'marketer' => '마케터', - 'online_store' => '온라인 스토어', - 'other' => '기타', + 'title' => '시작하기', + 'welcome' => 'TryPost에 오신 걸 환영해요, :name', + 'welcome_anonymous' => 'TryPost에 오신 걸 환영해요', + 'description' => '아래 단계를 따라 TryPost 사용법을 확인하고 첫 게시물을 발행하세요.', + 'skip' => '지금은 건너뛰기', + 'continue' => 'TryPost로 계속', + 'status' => [ + 'complete' => '완료', + 'todo' => '할 일', ], - 'goals_title' => 'TryPost로 이루려는 목표는 무엇인가요?', - 'goals_description' => '해당되는 항목을 모두 선택하면 TryPost를 맞춤 설정해 드립니다.', - 'goals' => [ - 'save_time' => '한 번에 여러 곳에 게시하여 시간 절약', - 'ai_content' => 'AI로 더 빠르게 게시물 작성', - 'plan_calendar' => '캘린더에서 게시물 계획', - 'stay_on_brand' => '모든 게시물을 브랜드에 맞게 유지', - 'grow_audience' => '팔로워와 참여 늘리기', - 'drive_sales' => '더 많은 트래픽과 판매 유도', - 'manage_clients' => '여러 브랜드 또는 클라이언트 관리', - 'team_collaboration' => '팀과 협업', - 'automate_api' => 'API, MCP 또는 코드로 게시 자동화', - 'track_performance' => '게시물 성과 확인', - 'just_exploring' => '지금은 둘러보는 중', - 'other' => '다른 것', + 'mcp' => [ + 'title' => 'AI 어시스턴트 연결', + 'description' => 'TryPost를 MCP 서버로 추가하면 어시스턴트가 소셜 게시물을 만들고 관리할 수 있어요.', + 'copy_step' => 'TryPost 서버 URL 복사', + 'open_step' => 'AI 어시스턴트 열기', + 'copy' => 'URL 복사', + 'copied' => 'MCP URL이 복사되었어요.', + 'connect' => ':client로 연결', + 'clients' => [ + 'claude' => 'Settings → Connectors를 열고 커스텀 커넥터를 추가한 뒤 위 URL을 붙여넣으세요.', + 'chatgpt' => 'Settings → Apps & Connectors를 열고 커스텀 커넥터를 만든 뒤 위 URL을 붙여넣으세요.', + ], ], - 'referral_source_title' => '저희를 어떻게 알게 되셨나요?', - 'referral_source_description' => '사람들이 TryPost를 어떻게 발견하는지 파악하는 데 도움이 됩니다.', - 'referral_source' => [ - 'google' => 'Google 또는 검색', - 'x' => 'X (Twitter)', - 'linkedin' => 'LinkedIn', - 'youtube' => 'YouTube', - 'tiktok' => 'TikTok', - 'instagram' => 'Instagram 또는 Threads', - 'reddit' => 'Reddit', - 'product_hunt' => 'Product Hunt', - 'ai_assistant' => 'AI 어시스턴트 (ChatGPT, Claude 등)', - 'friend' => '친구 또는 동료', - 'blog' => '블로그, 뉴스레터 또는 기사', - 'other' => '기타', + 'social' => [ + 'title' => '소셜 계정 연결', + 'description' => 'TryPost가 콘텐츠를 발행할 네트워크를 하나 이상 선택하세요.', ], - 'connect' => [ - 'title' => '첫 네트워크 연결', - 'description' => '예약을 시작하려면 소셜 계정을 하나 이상 연결하세요. 언제든지 추가할 수 있습니다.', - 'must_connect' => '계속하려면 네트워크를 하나 이상 연결하세요.', + 'first_post' => [ + 'title' => '첫 게시물 만들기', + 'description' => '연결된 어시스턴트에서 이 시작 프롬프트를 써 보거나, TryPost에서 바로 게시물을 만드세요.', + 'prompt_label' => '샘플 프롬프트', + 'sample_prompt' => '내 브랜드를 소개하는 친근한 소셜 게시물을 만들고 연결된 각 네트워크에 맞게 조정해 주세요.', + 'copy_prompt' => '프롬프트 복사', + 'copied' => '샘플 프롬프트가 복사되었어요.', + 'create_button' => '첫 게시물 만들기', + 'or' => '또는', + ], + 'ready' => [ + 'title' => '발행할 준비가 됐어요', + 'description' => '설정이 끝났어요. TryPost로 가서 콘텐츠 계획을 시작하세요.', ], ]; diff --git a/lang/ko/settings.php b/lang/ko/settings.php index feb6bf182..d00474184 100644 --- a/lang/ko/settings.php +++ b/lang/ko/settings.php @@ -129,6 +129,7 @@ 'brand' => '브랜드', 'users' => '멤버', 'api_keys' => 'API 키', + 'mcp' => 'MCP', ], 'title' => '워크스페이스 설정', 'logo_heading' => '워크스페이스 로고', diff --git a/lang/ko/sidebar.php b/lang/ko/sidebar.php index 3d47a2bfe..9563b592d 100644 --- a/lang/ko/sidebar.php +++ b/lang/ko/sidebar.php @@ -8,6 +8,8 @@ 'create_workspace' => '워크스페이스 만들기', 'create_post' => '게시물 만들기', 'profile' => '프로필', + 'my_account' => '내 계정', + 'account_settings' => '계정 및 결제', 'log_out' => '로그아웃', 'workspace' => '워크스페이스: :name', @@ -30,6 +32,8 @@ 'analytics' => '분석', 'automations' => '자동화', 'settings' => '설정', + 'onboarding' => '시작하기', + 'onboarding_hint' => '설정 마치기', 'posts' => [ 'calendar' => '캘린더', @@ -44,7 +48,9 @@ 'signatures' => '서명', 'labels' => '라벨', 'assets' => '에셋', + 'settings' => '설정', 'api_keys' => 'API 키', + 'mcp' => 'MCP', ], 'notifications' => '알림', diff --git a/lang/ko/welcome.php b/lang/ko/welcome.php new file mode 100644 index 000000000..b12dd89ec --- /dev/null +++ b/lang/ko/welcome.php @@ -0,0 +1,52 @@ + '무엇을 가장 잘 설명하나요?', + 'description' => '가장 가까운 항목을 선택하면 맞춤 경험을 제공해 드립니다.', + 'continue' => '계속', + 'checkout_owner_only' => '계정 소유자에게 결제와 구독 시작을 요청하세요.', + 'progress' => '환영 진행률', + 'go_to_step' => ':step단계로 이동', + 'personas' => [ + 'creator' => '콘텐츠 크리에이터', + 'freelancer' => '프리랜서', + 'developer' => '개발자', + 'startup' => '스타트업', + 'agency' => '에이전시', + 'small_business' => '소상공인', + 'marketer' => '마케터', + 'online_store' => '온라인 스토어', + 'other' => '기타', + ], + 'goals_title' => '목표가 무엇인가요?', + 'goals_description' => '해당되는 항목을 모두 선택하면 TryPost를 맞춤 설정해 드립니다.', + 'goals' => [ + 'save_time' => '한 번에 여러 곳에 게시하여 시간 절약', + 'ai_content' => 'AI로 더 빠르게 게시물 작성', + 'plan_calendar' => '캘린더에서 게시물 계획', + 'stay_on_brand' => '모든 게시물을 브랜드에 맞게 유지', + 'grow_audience' => '팔로워와 참여 늘리기', + 'drive_sales' => '더 많은 트래픽과 판매 유도', + 'manage_clients' => '여러 브랜드 또는 클라이언트 관리', + 'just_exploring' => '지금은 둘러보는 중', + 'other' => '다른 것', + ], + 'referral_source_title' => '저희를 어떻게 알게 되셨나요?', + 'referral_source_description' => '사람들이 TryPost를 어떻게 발견하는지 파악하는 데 도움이 됩니다.', + 'referral_source' => [ + 'google' => 'Google 또는 검색', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram 또는 Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'AI 어시스턴트 (ChatGPT, Claude 등)', + 'friend' => '친구 또는 동료', + 'blog' => '블로그, 뉴스레터 또는 기사', + 'other' => '기타', + ], +]; diff --git a/lang/nl/mcp.php b/lang/nl/mcp.php new file mode 100644 index 000000000..f77435348 --- /dev/null +++ b/lang/nl/mcp.php @@ -0,0 +1,41 @@ + 'MCP', + 'subtitle' => 'Koppel AI-assistenten zodat ze posts kunnen maken en beheren met je TryPost-account.', + 'step_add' => 'Plak de naam, URL of config hieronder in je app. Inloggen opent in de browser bij de eerste verbinding.', + 'name_label' => 'Naam', + 'url_label' => 'Server-URL', + 'config_label' => 'Config', + 'connected_title' => 'Gekoppelde apps', + 'connected_description' => 'Assistenten waarmee iemand op dit account is ingelogd. Je kunt alleen je eigen apps ontkoppelen.', + 'connected_empty' => 'Nog niets gekoppeld. Gebruik Claude, ChatGPT of een andere client hierboven.', + 'connected_by' => 'Verbonden door :name', + 'disconnect' => 'Ontkoppelen', + 'disconnect_title' => 'App ontkoppelen', + 'disconnect_confirm' => 'Dit logt de app uit bij TryPost. Hij moet opnieuw verbinden om MCP weer te gebruiken.', + 'disconnected' => 'App ontkoppeld.', + 'copied' => 'Gekopieerd', + 'last_used' => 'Laatst gebruikt', + 'never' => 'Nooit', + 'documentation_title' => 'Documentatie', + 'documentation_description' => 'Handleidingen per client, beschikbare tools en probleemoplossing.', + 'view_docs' => 'Documentatie bekijken', + 'connector_name' => 'TryPost', + + 'other_clients_title' => 'Andere apps', + 'other_clients_description' => 'Cursor, VS Code, Claude Code en alles wat MCP spreekt.', + + 'clients' => [ + 'cursor' => 'Voeg TryPost toe als remote MCP-server in Cursor.', + 'cursor_name' => 'Cursor', + 'vscode' => 'Plak de config hieronder in de MCP-instellingen van VS Code.', + 'vscode_name' => 'VS Code', + 'claude_code' => 'Plak de config hieronder in de MCP-instellingen van Claude Code.', + 'claude_code_name' => 'Claude Code', + 'other' => 'Werkt met elke client die een mcpServers-config leest.', + 'other_name' => 'Overig', + ], +]; diff --git a/lang/nl/onboarding.php b/lang/nl/onboarding.php index 424cfe421..3d6ccaba7 100644 --- a/lang/nl/onboarding.php +++ b/lang/nl/onboarding.php @@ -3,55 +3,45 @@ declare(strict_types=1); return [ - 'title' => 'Welkom bij TryPost', - 'description' => 'Vertel ons wat jou of je bedrijf het beste omschrijft, zodat we je ervaring kunnen afstemmen.', - 'continue' => 'Doorgaan', - 'personas' => [ - 'creator' => 'Contentmaker', - 'freelancer' => 'Freelancer', - 'developer' => 'Ontwikkelaar', - 'startup' => 'Startup', - 'agency' => 'Bureau', - 'small_business' => 'Klein bedrijf', - 'marketer' => 'Marketeer', - 'online_store' => 'Webshop', - 'other' => 'Anders', + 'title' => 'Aan de slag', + 'welcome' => 'Welkom bij TryPost, :name', + 'welcome_anonymous' => 'Welkom bij TryPost', + 'description' => 'Volg de stappen hieronder om te zien hoe TryPost werkt en je eerste post te publiceren.', + 'skip' => 'Voor nu overslaan', + 'continue' => 'Doorgaan naar TryPost', + 'status' => [ + 'complete' => 'Voltooid', + 'todo' => 'Te doen', ], - 'goals_title' => 'Wat is je doel met TryPost?', - 'goals_description' => 'Kies alles wat past en we stellen TryPost voor je in.', - 'goals' => [ - 'save_time' => 'Tijd besparen door overal tegelijk te posten', - 'ai_content' => 'Sneller posts maken met AI', - 'plan_calendar' => 'Mijn posts plannen op een kalender', - 'stay_on_brand' => 'Elke post in lijn met mijn merk houden', - 'grow_audience' => 'Mijn publiek en betrokkenheid laten groeien', - 'drive_sales' => 'Meer verkeer en verkopen krijgen', - 'manage_clients' => 'Meerdere merken of klanten beheren', - 'team_collaboration' => 'Samenwerken met mijn team', - 'automate_api' => 'Posten automatiseren met de API, MCP of code', - 'track_performance' => 'Zien hoe mijn posts presteren', - 'just_exploring' => 'Voorlopig gewoon aan het verkennen', - 'other' => 'Iets anders', + 'mcp' => [ + 'title' => 'Koppel je AI-assistent', + 'description' => 'Voeg TryPost toe als MCP-server zodat je assistent social posts voor je kan maken en beheren.', + 'copy_step' => 'Kopieer je TryPost-server-URL', + 'open_step' => 'Open je AI-assistent', + 'copy' => 'URL kopiëren', + 'copied' => 'MCP-URL gekopieerd.', + 'connect' => 'Verbinden met :client', + 'clients' => [ + 'claude' => 'Open Settings → Connectors, voeg een aangepaste connector toe en plak de URL hierboven.', + 'chatgpt' => 'Open Settings → Apps & Connectors, maak een aangepaste connector aan en plak de URL hierboven.', + ], ], - 'referral_source_title' => 'Hoe heb je ons gevonden?', - 'referral_source_description' => 'Dit helpt ons te begrijpen hoe mensen TryPost ontdekken.', - 'referral_source' => [ - 'google' => 'Google of zoekmachine', - 'x' => 'X (Twitter)', - 'linkedin' => 'LinkedIn', - 'youtube' => 'YouTube', - 'tiktok' => 'TikTok', - 'instagram' => 'Instagram of Threads', - 'reddit' => 'Reddit', - 'product_hunt' => 'Product Hunt', - 'ai_assistant' => 'AI-assistent (ChatGPT, Claude…)', - 'friend' => 'Vriend of collega', - 'blog' => 'Blog, nieuwsbrief of artikel', - 'other' => 'Iets anders', + 'social' => [ + 'title' => 'Koppel een social account', + 'description' => 'Kies minstens één netwerk waar TryPost je content kan publiceren.', ], - 'connect' => [ - 'title' => 'Koppel je eerste netwerk', - 'description' => 'Koppel ten minste één social account om te beginnen met plannen. Je kunt er altijd meer toevoegen.', - 'must_connect' => 'Koppel ten minste één netwerk om door te gaan.', + 'first_post' => [ + 'title' => 'Maak je eerste post', + 'description' => 'Probeer deze startprompt met je gekoppelde assistent, of maak de post direct in TryPost.', + 'prompt_label' => 'Voorbeeldprompt', + 'sample_prompt' => 'Maak een vriendelijke social post die mijn merk introduceert en pas die aan voor elk gekoppeld netwerk.', + 'copy_prompt' => 'Prompt kopiëren', + 'copied' => 'Voorbeeldprompt gekopieerd.', + 'create_button' => 'Je eerste post maken', + 'or' => 'of', + ], + 'ready' => [ + 'title' => 'Je bent klaar om te publiceren', + 'description' => 'Alles staat. Ga door naar TryPost en begin je content te plannen.', ], ]; diff --git a/lang/nl/settings.php b/lang/nl/settings.php index 6a5b42a8f..5c1359a29 100644 --- a/lang/nl/settings.php +++ b/lang/nl/settings.php @@ -129,6 +129,7 @@ 'brand' => 'Merk', 'users' => 'Leden', 'api_keys' => 'API-sleutels', + 'mcp' => 'MCP', ], 'title' => 'Workspace-instellingen', 'logo_heading' => 'Workspace-logo', diff --git a/lang/nl/sidebar.php b/lang/nl/sidebar.php index daca60f21..9390f1ef8 100644 --- a/lang/nl/sidebar.php +++ b/lang/nl/sidebar.php @@ -8,6 +8,8 @@ 'create_workspace' => 'Workspace aanmaken', 'create_post' => 'Post aanmaken', 'profile' => 'Profiel', + 'my_account' => 'Mijn account', + 'account_settings' => 'Account en facturatie', 'log_out' => 'Uitloggen', 'workspace' => 'Workspace: :name', @@ -30,6 +32,8 @@ 'analytics' => 'Statistieken', 'automations' => 'Automatiseringen', 'settings' => 'Instellingen', + 'onboarding' => 'Aan de slag', + 'onboarding_hint' => 'Setup afronden', 'posts' => [ 'calendar' => 'Kalender', @@ -44,7 +48,9 @@ 'signatures' => 'Handtekeningen', 'labels' => 'Labels', 'assets' => 'Assets', + 'settings' => 'Instellingen', 'api_keys' => 'API-sleutels', + 'mcp' => 'MCP', ], 'notifications' => 'Meldingen', diff --git a/lang/nl/welcome.php b/lang/nl/welcome.php new file mode 100644 index 000000000..4b32ea0c8 --- /dev/null +++ b/lang/nl/welcome.php @@ -0,0 +1,52 @@ + 'Wat omschrijft jou het beste?', + 'description' => 'Kies de dichtstbijzijnde optie, dan stemmen we je ervaring af.', + 'continue' => 'Doorgaan', + 'checkout_owner_only' => 'Vraag de accounteigenaar om de checkout af te ronden en het abonnement te starten.', + 'progress' => 'Welkomstvoortgang', + 'go_to_step' => 'Ga naar stap :step', + 'personas' => [ + 'creator' => 'Contentmaker', + 'freelancer' => 'Freelancer', + 'developer' => 'Ontwikkelaar', + 'startup' => 'Startup', + 'agency' => 'Bureau', + 'small_business' => 'Klein bedrijf', + 'marketer' => 'Marketeer', + 'online_store' => 'Webshop', + 'other' => 'Anders', + ], + 'goals_title' => 'Wat is je doel?', + 'goals_description' => 'Kies alles wat past en we stellen TryPost voor je in.', + 'goals' => [ + 'save_time' => 'Tijd besparen door overal tegelijk te posten', + 'ai_content' => 'Sneller posts maken met AI', + 'plan_calendar' => 'Mijn posts plannen op een kalender', + 'stay_on_brand' => 'Elke post in lijn met mijn merk houden', + 'grow_audience' => 'Mijn publiek en betrokkenheid laten groeien', + 'drive_sales' => 'Meer verkeer en verkopen krijgen', + 'manage_clients' => 'Meerdere merken of klanten beheren', + 'just_exploring' => 'Voorlopig gewoon aan het verkennen', + 'other' => 'Iets anders', + ], + 'referral_source_title' => 'Hoe heb je ons gevonden?', + 'referral_source_description' => 'Dit helpt ons te begrijpen hoe mensen TryPost ontdekken.', + 'referral_source' => [ + 'google' => 'Google of zoekmachine', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram of Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'AI-assistent (ChatGPT, Claude…)', + 'friend' => 'Vriend of collega', + 'blog' => 'Blog, nieuwsbrief of artikel', + 'other' => 'Iets anders', + ], +]; diff --git a/lang/pl/mcp.php b/lang/pl/mcp.php new file mode 100644 index 000000000..3c24c5d05 --- /dev/null +++ b/lang/pl/mcp.php @@ -0,0 +1,41 @@ + 'MCP', + 'subtitle' => 'Połącz asystentów AI, aby tworzyli i zarządzali postami na koncie TryPost.', + 'step_add' => 'Wklej nazwę, URL lub config poniżej do swojej aplikacji. Logowanie otworzy się w przeglądarce przy pierwszym połączeniu.', + 'name_label' => 'Nazwa', + 'url_label' => 'URL serwera', + 'config_label' => 'Config', + 'connected_title' => 'Połączone aplikacje', + 'connected_description' => 'Asystenci zalogowani przez kogokolwiek na tym koncie. Możesz rozłączyć tylko własne.', + 'connected_empty' => 'Nic jeszcze nie połączono. Użyj Claude, ChatGPT lub innego klienta powyżej.', + 'connected_by' => 'Połączono przez :name', + 'disconnect' => 'Rozłącz', + 'disconnect_title' => 'Rozłącz aplikację', + 'disconnect_confirm' => 'To wyloguje aplikację z TryPost. Musi połączyć się ponownie, zanim znów użyje MCP.', + 'disconnected' => 'Aplikacja rozłączona.', + 'copied' => 'Skopiowano', + 'last_used' => 'Ostatnie użycie', + 'never' => 'Nigdy', + 'documentation_title' => 'Dokumentacja', + 'documentation_description' => 'Przewodniki per klient, dostępne tools i rozwiązywanie problemów.', + 'view_docs' => 'Zobacz dokumentację', + 'connector_name' => 'TryPost', + + 'other_clients_title' => 'Inne aplikacje', + 'other_clients_description' => 'Cursor, VS Code, Claude Code i wszystko, co mówi MCP.', + + 'clients' => [ + 'cursor' => 'Dodaj TryPost jako zdalny serwer MCP w Cursorze.', + 'cursor_name' => 'Cursor', + 'vscode' => 'Wklej poniższą konfigurację w ustawieniach MCP VS Code.', + 'vscode_name' => 'VS Code', + 'claude_code' => 'Wklej poniższą konfigurację w ustawieniach MCP Claude Code.', + 'claude_code_name' => 'Claude Code', + 'other' => 'Działa z każdym klientem, który czyta config mcpServers.', + 'other_name' => 'Inne', + ], +]; diff --git a/lang/pl/onboarding.php b/lang/pl/onboarding.php index 91092398d..571ca8775 100644 --- a/lang/pl/onboarding.php +++ b/lang/pl/onboarding.php @@ -3,55 +3,45 @@ declare(strict_types=1); return [ - 'title' => 'Witamy w TryPost', - 'description' => 'Powiedz nam, co najlepiej opisuje Ciebie lub Twoją firmę, abyśmy mogli dopasować Twoje doświadczenie.', - 'continue' => 'Kontynuuj', - 'personas' => [ - 'creator' => 'Twórca treści', - 'freelancer' => 'Freelancer', - 'developer' => 'Programista', - 'startup' => 'Startup', - 'agency' => 'Agencja', - 'small_business' => 'Mała firma', - 'marketer' => 'Marketingowiec', - 'online_store' => 'Sklep internetowy', - 'other' => 'Inne', + 'title' => 'Pierwsze kroki', + 'welcome' => 'Witaj w TryPost, :name', + 'welcome_anonymous' => 'Witaj w TryPost', + 'description' => 'Wykonaj poniższe kroki, aby zobaczyć, jak działa TryPost, i opublikować pierwszy post.', + 'skip' => 'Pomiń na razie', + 'continue' => 'Przejdź do TryPost', + 'status' => [ + 'complete' => 'Ukończone', + 'todo' => 'Do zrobienia', ], - 'goals_title' => 'Jaki jest Twój cel z TryPost?', - 'goals_description' => 'Wybierz wszystko, co pasuje, a my skonfigurujemy TryPost dla Ciebie.', - 'goals' => [ - 'save_time' => 'Oszczędzaj czas, publikując wszędzie naraz', - 'ai_content' => 'Twórz posty szybciej dzięki AI', - 'plan_calendar' => 'Planuj posty w kalendarzu', - 'stay_on_brand' => 'Utrzymuj każdy post spójny z marką', - 'grow_audience' => 'Powiększaj grono odbiorców i zaangażowanie', - 'drive_sales' => 'Zdobywaj więcej ruchu i sprzedaży', - 'manage_clients' => 'Zarządzaj wieloma markami lub klientami', - 'team_collaboration' => 'Pracuj z moim zespołem', - 'automate_api' => 'Automatyzuj publikowanie za pomocą API, MCP lub kodu', - 'track_performance' => 'Sprawdzaj, jak radzą sobie moje posty', - 'just_exploring' => 'Na razie tylko się rozglądam', - 'other' => 'Coś innego', + 'mcp' => [ + 'title' => 'Połącz asystenta AI', + 'description' => 'Dodaj TryPost jako serwer MCP, aby asystent mógł tworzyć i zarządzać postami społecznościowymi za Ciebie.', + 'copy_step' => 'Skopiuj URL serwera TryPost', + 'open_step' => 'Otwórz asystenta AI', + 'copy' => 'Kopiuj URL', + 'copied' => 'URL MCP skopiowany.', + 'connect' => 'Połącz z :client', + 'clients' => [ + 'claude' => 'Otwórz Settings → Connectors, dodaj niestandardowy connector, a następnie wklej powyższy URL.', + 'chatgpt' => 'Otwórz Settings → Apps & Connectors, utwórz niestandardowy connector, a następnie wklej powyższy URL.', + ], ], - 'referral_source_title' => 'Jak nas znalazłeś?', - 'referral_source_description' => 'To pomaga nam zrozumieć, jak ludzie odkrywają TryPost.', - 'referral_source' => [ - 'google' => 'Google lub wyszukiwarka', - 'x' => 'X (Twitter)', - 'linkedin' => 'LinkedIn', - 'youtube' => 'YouTube', - 'tiktok' => 'TikTok', - 'instagram' => 'Instagram lub Threads', - 'reddit' => 'Reddit', - 'product_hunt' => 'Product Hunt', - 'ai_assistant' => 'Asystent AI (ChatGPT, Claude…)', - 'friend' => 'Znajomy lub współpracownik', - 'blog' => 'Blog, newsletter lub artykuł', - 'other' => 'Coś innego', + 'social' => [ + 'title' => 'Połącz konto społecznościowe', + 'description' => 'Wybierz co najmniej jedną sieć, na której TryPost może publikować Twoje treści.', ], - 'connect' => [ - 'title' => 'Połącz swoją pierwszą sieć', - 'description' => 'Połącz co najmniej jedno konto społecznościowe, aby zacząć planować. Więcej możesz dodać w dowolnym momencie.', - 'must_connect' => 'Połącz co najmniej jedną sieć, aby kontynuować.', + 'first_post' => [ + 'title' => 'Utwórz pierwszy post', + 'description' => 'Wypróbuj ten prompt startowy z podłączonym asystentem albo utwórz post bezpośrednio w TryPost.', + 'prompt_label' => 'Przykładowy prompt', + 'sample_prompt' => 'Utwórz przyjazny post społecznościowy przedstawiający moją markę i dostosuj go do każdej podłączonej sieci.', + 'copy_prompt' => 'Kopiuj prompt', + 'copied' => 'Przykładowy prompt skopiowany.', + 'create_button' => 'Utwórz pierwszy post', + 'or' => 'lub', + ], + 'ready' => [ + 'title' => 'Możesz już publikować', + 'description' => 'Wszystko gotowe. Przejdź do TryPost i zacznij planować treści.', ], ]; diff --git a/lang/pl/settings.php b/lang/pl/settings.php index bfd2db6c4..7a3d6af94 100644 --- a/lang/pl/settings.php +++ b/lang/pl/settings.php @@ -129,6 +129,7 @@ 'brand' => 'Marka', 'users' => 'Członkowie', 'api_keys' => 'Klucze API', + 'mcp' => 'MCP', ], 'title' => 'Ustawienia przestrzeni roboczej', 'logo_heading' => 'Logo przestrzeni roboczej', diff --git a/lang/pl/sidebar.php b/lang/pl/sidebar.php index 9e83547d1..0b9862b18 100644 --- a/lang/pl/sidebar.php +++ b/lang/pl/sidebar.php @@ -8,6 +8,8 @@ 'create_workspace' => 'Utwórz przestrzeń roboczą', 'create_post' => 'Utwórz post', 'profile' => 'Profil', + 'my_account' => 'Moje konto', + 'account_settings' => 'Konto i płatności', 'log_out' => 'Wyloguj się', 'workspace' => 'Przestrzeń robocza: :name', @@ -30,6 +32,8 @@ 'analytics' => 'Analityka', 'automations' => 'Automatyzacje', 'settings' => 'Ustawienia', + 'onboarding' => 'Pierwsze kroki', + 'onboarding_hint' => 'Dokończ konfigurację', 'posts' => [ 'calendar' => 'Kalendarz', @@ -44,7 +48,9 @@ 'signatures' => 'Sygnatury', 'labels' => 'Etykiety', 'assets' => 'Zasoby', + 'settings' => 'Ustawienia', 'api_keys' => 'Klucze API', + 'mcp' => 'MCP', ], 'notifications' => 'Powiadomienia', diff --git a/lang/pl/welcome.php b/lang/pl/welcome.php new file mode 100644 index 000000000..b8ad58a1b --- /dev/null +++ b/lang/pl/welcome.php @@ -0,0 +1,52 @@ + 'Co najlepiej Cię opisuje?', + 'description' => 'Wybierz najbliższą opcję, a my dopasujemy Twoje doświadczenie.', + 'continue' => 'Kontynuuj', + 'checkout_owner_only' => 'Poproś właściciela konta o dokończenie płatności i rozpoczęcie subskrypcji.', + 'progress' => 'Postęp powitalny', + 'go_to_step' => 'Przejdź do kroku :step', + 'personas' => [ + 'creator' => 'Twórca treści', + 'freelancer' => 'Freelancer', + 'developer' => 'Programista', + 'startup' => 'Startup', + 'agency' => 'Agencja', + 'small_business' => 'Mała firma', + 'marketer' => 'Marketingowiec', + 'online_store' => 'Sklep internetowy', + 'other' => 'Inne', + ], + 'goals_title' => 'Jaki jest Twój cel?', + 'goals_description' => 'Wybierz wszystko, co pasuje, a my skonfigurujemy TryPost dla Ciebie.', + 'goals' => [ + 'save_time' => 'Oszczędzaj czas, publikując wszędzie naraz', + 'ai_content' => 'Twórz posty szybciej dzięki AI', + 'plan_calendar' => 'Planuj posty w kalendarzu', + 'stay_on_brand' => 'Utrzymuj każdy post spójny z marką', + 'grow_audience' => 'Powiększaj grono odbiorców i zaangażowanie', + 'drive_sales' => 'Zdobywaj więcej ruchu i sprzedaży', + 'manage_clients' => 'Zarządzaj wieloma markami lub klientami', + 'just_exploring' => 'Na razie tylko się rozglądam', + 'other' => 'Coś innego', + ], + 'referral_source_title' => 'Jak nas znalazłeś?', + 'referral_source_description' => 'To pomaga nam zrozumieć, jak ludzie odkrywają TryPost.', + 'referral_source' => [ + 'google' => 'Google lub wyszukiwarka', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram lub Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'Asystent AI (ChatGPT, Claude…)', + 'friend' => 'Znajomy lub współpracownik', + 'blog' => 'Blog, newsletter lub artykuł', + 'other' => 'Coś innego', + ], +]; diff --git a/lang/pt-BR/mcp.php b/lang/pt-BR/mcp.php new file mode 100644 index 000000000..8818a660c --- /dev/null +++ b/lang/pt-BR/mcp.php @@ -0,0 +1,41 @@ + 'MCP', + 'subtitle' => 'Conecte assistentes de IA pra criarem e gerenciarem posts com sua conta TryPost.', + 'step_add' => 'Cole o nome, a URL ou o config abaixo no seu app. O login abre no navegador na primeira conexão.', + 'name_label' => 'Nome', + 'url_label' => 'URL do servidor', + 'config_label' => 'Config', + 'connected_title' => 'Apps conectados', + 'connected_description' => 'Assistentes com login de qualquer pessoa nesta conta. Você só desconecta os seus.', + 'connected_empty' => 'Nada conectado ainda. Use Claude, ChatGPT ou outro cliente acima.', + 'connected_by' => 'Conectado por :name', + 'disconnect' => 'Desconectar', + 'disconnect_title' => 'Desconectar app', + 'disconnect_confirm' => 'Isso desconecta o app do TryPost. Ele precisa reconectar pra usar o MCP de novo.', + 'disconnected' => 'App desconectado.', + 'copied' => 'Copiado', + 'last_used' => 'Último uso', + 'never' => 'Nunca', + 'documentation_title' => 'Documentação', + 'documentation_description' => 'Guias por cliente, tools disponíveis e solução de problemas.', + 'view_docs' => 'Ver documentação', + 'connector_name' => 'TryPost', + + 'other_clients_title' => 'Outros apps', + 'other_clients_description' => 'Cursor, VS Code, Claude Code e qualquer app que fale MCP.', + + 'clients' => [ + 'cursor' => 'Adicione o TryPost como servidor MCP remoto no Cursor.', + 'cursor_name' => 'Cursor', + 'vscode' => 'Cole o config abaixo nas configurações MCP do VS Code.', + 'vscode_name' => 'VS Code', + 'claude_code' => 'Cole o config abaixo nas configurações MCP do Claude Code.', + 'claude_code_name' => 'Claude Code', + 'other' => 'Funciona com qualquer cliente que leia um config mcpServers.', + 'other_name' => 'Outros', + ], +]; diff --git a/lang/pt-BR/onboarding.php b/lang/pt-BR/onboarding.php index 12ad79629..bb3813ce3 100644 --- a/lang/pt-BR/onboarding.php +++ b/lang/pt-BR/onboarding.php @@ -3,55 +3,45 @@ declare(strict_types=1); return [ - 'title' => 'Bem-vindo ao TryPost', - 'description' => 'Conte o que melhor descreve você ou seu negócio para personalizarmos sua experiência.', - 'continue' => 'Continuar', - 'personas' => [ - 'creator' => 'Criador de conteúdo', - 'freelancer' => 'Freelancer', - 'developer' => 'Desenvolvedor', - 'startup' => 'Startup', - 'agency' => 'Agência', - 'small_business' => 'Pequena empresa', - 'marketer' => 'Profissional de marketing', - 'online_store' => 'Loja online', - 'other' => 'Outro', + 'title' => 'Primeiros passos', + 'welcome' => 'Boas-vindas ao TryPost, :name', + 'welcome_anonymous' => 'Boas-vindas ao TryPost', + 'description' => 'Siga os passos abaixo pra ver como o TryPost funciona e publicar seu primeiro post.', + 'skip' => 'Pular por agora', + 'continue' => 'Continuar no TryPost', + 'status' => [ + 'complete' => 'Concluído', + 'todo' => 'Pendente', ], - 'goals_title' => 'Qual o seu objetivo com o TryPost?', - 'goals_description' => 'Marque tudo que faz sentido e a gente ajusta o TryPost pra você.', - 'goals' => [ - 'save_time' => 'Economizar tempo postando em todas as redes de uma vez', - 'ai_content' => 'Criar posts mais rápido com IA', - 'plan_calendar' => 'Planejar meus posts num calendário', - 'stay_on_brand' => 'Manter a consistência da minha marca', - 'grow_audience' => 'Crescer minha audiência e engajamento', - 'drive_sales' => 'Conseguir mais tráfego e vendas', - 'manage_clients' => 'Gerenciar várias marcas ou clientes', - 'team_collaboration' => 'Trabalhar com meu time', - 'automate_api' => 'Automatizar publicações com a API, MCP ou código', - 'track_performance' => 'Ver o desempenho dos meus posts', - 'just_exploring' => 'Só dando uma olhada por enquanto', - 'other' => 'Outra coisa', + 'mcp' => [ + 'title' => 'Conecte seu assistente de IA', + 'description' => 'Adicione o TryPost como servidor MCP para o assistente criar e gerenciar posts por você.', + 'copy_step' => 'Copie a URL do servidor TryPost', + 'open_step' => 'Abra seu assistente de IA', + 'copy' => 'Copiar URL', + 'copied' => 'URL do MCP copiada.', + 'connect' => 'Conectar com :client', + 'clients' => [ + 'claude' => 'Abra Settings → Connectors, adicione um connector customizado e cole a URL acima.', + 'chatgpt' => 'Abra Settings → Apps & Connectors, crie um connector customizado e cole a URL acima.', + ], ], - 'referral_source_title' => 'Como você nos encontrou?', - 'referral_source_description' => 'Isso nos ajuda a entender como as pessoas descobrem o TryPost.', - 'referral_source' => [ - 'google' => 'Google ou busca', - 'x' => 'X (Twitter)', - 'linkedin' => 'LinkedIn', - 'youtube' => 'YouTube', - 'tiktok' => 'TikTok', - 'instagram' => 'Instagram ou Threads', - 'reddit' => 'Reddit', - 'product_hunt' => 'Product Hunt', - 'ai_assistant' => 'Assistente de IA (ChatGPT, Claude…)', - 'friend' => 'Amigo ou colega', - 'blog' => 'Blog, newsletter ou artigo', - 'other' => 'Outra coisa', + 'social' => [ + 'title' => 'Conecte uma rede social', + 'description' => 'Escolha pelo menos uma rede onde o TryPost possa publicar seu conteúdo.', ], - 'connect' => [ - 'title' => 'Conecte sua primeira rede', - 'description' => 'Vincule pelo menos uma conta social para começar a agendar. Você pode adicionar mais quando quiser.', - 'must_connect' => 'Conecte pelo menos uma rede para continuar.', + 'first_post' => [ + 'title' => 'Crie seu primeiro post', + 'description' => 'Use este prompt no seu assistente, ou crie o post direto no TryPost.', + 'prompt_label' => 'Prompt de exemplo', + 'sample_prompt' => 'Crie um post social amigável apresentando minha marca e adapte para cada rede conectada.', + 'copy_prompt' => 'Copiar prompt', + 'copied' => 'Prompt de exemplo copiado.', + 'create_button' => 'Criar seu primeiro post', + 'or' => 'ou', + ], + 'ready' => [ + 'title' => 'Tudo pronto pra publicar', + 'description' => 'Você já pode seguir. Continue no TryPost e comece a planejar seu conteúdo.', ], ]; diff --git a/lang/pt-BR/settings.php b/lang/pt-BR/settings.php index 7ad405879..1aa1a28ce 100644 --- a/lang/pt-BR/settings.php +++ b/lang/pt-BR/settings.php @@ -129,6 +129,7 @@ 'brand' => 'Marca', 'users' => 'Membros', 'api_keys' => 'API Keys', + 'mcp' => 'MCP', ], 'title' => 'Configurações do workspace', 'logo_heading' => 'Logo do workspace', diff --git a/lang/pt-BR/sidebar.php b/lang/pt-BR/sidebar.php index 0717d6215..3b2a4a0f4 100644 --- a/lang/pt-BR/sidebar.php +++ b/lang/pt-BR/sidebar.php @@ -8,6 +8,8 @@ 'create_workspace' => 'Criar workspace', 'create_post' => 'Novo post', 'profile' => 'Perfil', + 'my_account' => 'Minha conta', + 'account_settings' => 'Conta e cobrança', 'log_out' => 'Sair', 'workspace' => 'Workspace: :name', @@ -30,6 +32,8 @@ 'analytics' => 'Analytics', 'automations' => 'Automações', 'settings' => 'Configurações', + 'onboarding' => 'Primeiros passos', + 'onboarding_hint' => 'Complete a configuração', 'posts' => [ 'calendar' => 'Calendário', @@ -44,7 +48,9 @@ 'signatures' => 'Assinaturas', 'labels' => 'Etiquetas', 'assets' => 'Mídias', + 'settings' => 'Configurações', 'api_keys' => 'API Keys', + 'mcp' => 'MCP', ], 'notifications' => 'Notificações', diff --git a/lang/pt-BR/welcome.php b/lang/pt-BR/welcome.php new file mode 100644 index 000000000..26159246e --- /dev/null +++ b/lang/pt-BR/welcome.php @@ -0,0 +1,52 @@ + 'O que melhor descreve você?', + 'description' => 'Escolha a opção mais próxima e a gente personaliza sua experiência.', + 'continue' => 'Continuar', + 'checkout_owner_only' => 'Peça ao dono da conta para concluir o checkout e iniciar a assinatura.', + 'progress' => 'Progresso do onboarding', + 'go_to_step' => 'Ir para a etapa :step', + 'personas' => [ + 'creator' => 'Criador de conteúdo', + 'freelancer' => 'Freelancer', + 'developer' => 'Desenvolvedor', + 'startup' => 'Startup', + 'agency' => 'Agência', + 'small_business' => 'Pequena empresa', + 'marketer' => 'Profissional de marketing', + 'online_store' => 'Loja online', + 'other' => 'Outro', + ], + 'goals_title' => 'Qual o seu objetivo?', + 'goals_description' => 'Marque tudo que faz sentido e a gente ajusta o TryPost pra você.', + 'goals' => [ + 'save_time' => 'Economizar tempo postando em todas as redes de uma vez', + 'ai_content' => 'Criar posts mais rápido com IA', + 'plan_calendar' => 'Planejar meus posts num calendário', + 'stay_on_brand' => 'Manter a consistência da minha marca', + 'grow_audience' => 'Crescer minha audiência e engajamento', + 'drive_sales' => 'Conseguir mais tráfego e vendas', + 'manage_clients' => 'Gerenciar várias marcas ou clientes', + 'just_exploring' => 'Só dando uma olhada por enquanto', + 'other' => 'Outra coisa', + ], + 'referral_source_title' => 'Como você nos encontrou?', + 'referral_source_description' => 'Isso nos ajuda a entender como as pessoas descobrem o TryPost.', + 'referral_source' => [ + 'google' => 'Google ou busca', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram ou Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'Assistente de IA (ChatGPT, Claude…)', + 'friend' => 'Amigo ou colega', + 'blog' => 'Blog, newsletter ou artigo', + 'other' => 'Outra coisa', + ], +]; diff --git a/lang/ru/mcp.php b/lang/ru/mcp.php new file mode 100644 index 000000000..4f8acfde4 --- /dev/null +++ b/lang/ru/mcp.php @@ -0,0 +1,41 @@ + 'MCP', + 'subtitle' => 'Подключите ИИ-ассистентов, чтобы они создавали и управляли постами в вашем аккаунте TryPost.', + 'step_add' => 'Вставьте имя, URL или config ниже в своё приложение. Вход откроется в браузере при первом подключении.', + 'name_label' => 'Имя', + 'url_label' => 'URL сервера', + 'config_label' => 'Config', + 'connected_title' => 'Подключённые приложения', + 'connected_description' => 'Ассистенты, вошедшие от имени кого угодно в этом аккаунте. Отключить можно только свои.', + 'connected_empty' => 'Пока ничего не подключено. Используйте Claude, ChatGPT или другого клиента выше.', + 'connected_by' => 'Подключил :name', + 'disconnect' => 'Отключить', + 'disconnect_title' => 'Отключить приложение', + 'disconnect_confirm' => 'Это выйдет из аккаунта TryPost в приложении. Нужно будет подключиться снова, чтобы снова использовать MCP.', + 'disconnected' => 'Приложение отключено.', + 'copied' => 'Скопировано', + 'last_used' => 'Последнее использование', + 'never' => 'Никогда', + 'documentation_title' => 'Документация', + 'documentation_description' => 'Гайды по клиентам, доступные tools и решение проблем.', + 'view_docs' => 'Открыть документацию', + 'connector_name' => 'TryPost', + + 'other_clients_title' => 'Другие приложения', + 'other_clients_description' => 'Cursor, VS Code, Claude Code и всё, что говорит на MCP.', + + 'clients' => [ + 'cursor' => 'Добавьте TryPost как удалённый MCP-сервер в Cursor.', + 'cursor_name' => 'Cursor', + 'vscode' => 'Вставьте конфиг ниже в настройки MCP VS Code.', + 'vscode_name' => 'VS Code', + 'claude_code' => 'Вставьте конфиг ниже в настройки MCP Claude Code.', + 'claude_code_name' => 'Claude Code', + 'other' => 'Работает с любым клиентом, который читает config mcpServers.', + 'other_name' => 'Другие', + ], +]; diff --git a/lang/ru/onboarding.php b/lang/ru/onboarding.php index d7b64adde..cf2f0a085 100644 --- a/lang/ru/onboarding.php +++ b/lang/ru/onboarding.php @@ -3,55 +3,45 @@ declare(strict_types=1); return [ - 'title' => 'Добро пожаловать в TryPost', - 'description' => 'Расскажите, что лучше всего описывает вас или ваш бизнес, чтобы мы могли настроить работу под вас.', - 'continue' => 'Продолжить', - 'personas' => [ - 'creator' => 'Автор контента', - 'freelancer' => 'Фрилансер', - 'developer' => 'Разработчик', - 'startup' => 'Стартап', - 'agency' => 'Агентство', - 'small_business' => 'Малый бизнес', - 'marketer' => 'Маркетолог', - 'online_store' => 'Интернет-магазин', - 'other' => 'Другое', + 'title' => 'Начало работы', + 'welcome' => 'Добро пожаловать в TryPost, :name', + 'welcome_anonymous' => 'Добро пожаловать в TryPost', + 'description' => 'Выполните шаги ниже, чтобы увидеть, как работает TryPost, и опубликовать первый пост.', + 'skip' => 'Пропустить пока', + 'continue' => 'Перейти в TryPost', + 'status' => [ + 'complete' => 'Готово', + 'todo' => 'Сделать', ], - 'goals_title' => 'Какова ваша цель с TryPost?', - 'goals_description' => 'Выберите всё, что подходит, и мы настроим TryPost для вас.', - 'goals' => [ - 'save_time' => 'Экономить время, публикуя всюду сразу', - 'ai_content' => 'Создавать посты быстрее с помощью ИИ', - 'plan_calendar' => 'Планировать посты в календаре', - 'stay_on_brand' => 'Держать каждый пост в стиле бренда', - 'grow_audience' => 'Наращивать аудиторию и вовлечённость', - 'drive_sales' => 'Получать больше трафика и продаж', - 'manage_clients' => 'Управлять несколькими брендами или клиентами', - 'team_collaboration' => 'Работать с командой', - 'automate_api' => 'Автоматизировать публикацию с помощью API, MCP или кода', - 'track_performance' => 'Отслеживать эффективность постов', - 'just_exploring' => 'Пока просто знакомлюсь', - 'other' => 'Что-то ещё', + 'mcp' => [ + 'title' => 'Подключите ИИ-ассистента', + 'description' => 'Добавьте TryPost как MCP-сервер, чтобы ассистент мог создавать и вести соцпосты за вас.', + 'copy_step' => 'Скопируйте URL сервера TryPost', + 'open_step' => 'Откройте ИИ-ассистента', + 'copy' => 'Копировать URL', + 'copied' => 'URL MCP скопирован.', + 'connect' => 'Подключить через :client', + 'clients' => [ + 'claude' => 'Откройте Settings → Connectors, добавьте свой connector и вставьте URL выше.', + 'chatgpt' => 'Откройте Settings → Apps & Connectors, создайте свой connector и вставьте URL выше.', + ], ], - 'referral_source_title' => 'Как вы нас нашли?', - 'referral_source_description' => 'Это помогает нам понять, как люди узнают о TryPost.', - 'referral_source' => [ - 'google' => 'Google или поиск', - 'x' => 'X (Twitter)', - 'linkedin' => 'LinkedIn', - 'youtube' => 'YouTube', - 'tiktok' => 'TikTok', - 'instagram' => 'Instagram или Threads', - 'reddit' => 'Reddit', - 'product_hunt' => 'Product Hunt', - 'ai_assistant' => 'ИИ-ассистент (ChatGPT, Claude…)', - 'friend' => 'Друг или коллега', - 'blog' => 'Блог, рассылка или статья', - 'other' => 'Что-то другое', + 'social' => [ + 'title' => 'Подключите соцсеть', + 'description' => 'Выберите хотя бы одну сеть, где TryPost сможет публиковать ваш контент.', ], - 'connect' => [ - 'title' => 'Подключите первую сеть', - 'description' => 'Привяжите хотя бы один социальный аккаунт, чтобы начать планировать. Вы можете добавить другие в любой момент.', - 'must_connect' => 'Подключите хотя бы одну сеть, чтобы продолжить.', + 'first_post' => [ + 'title' => 'Создайте первый пост', + 'description' => 'Попробуйте этот стартовый промпт с подключённым ассистентом или создайте пост прямо в TryPost.', + 'prompt_label' => 'Пример промпта', + 'sample_prompt' => 'Создай дружелюбный соцпост с представлением моего бренда и адаптируй его для каждой подключённой сети.', + 'copy_prompt' => 'Копировать промпт', + 'copied' => 'Пример промпта скопирован.', + 'create_button' => 'Создать первый пост', + 'or' => 'или', + ], + 'ready' => [ + 'title' => 'Вы готовы публиковать', + 'description' => 'Всё настроено. Перейдите в TryPost и начните планировать контент.', ], ]; diff --git a/lang/ru/settings.php b/lang/ru/settings.php index adfeeefdd..72a90f123 100644 --- a/lang/ru/settings.php +++ b/lang/ru/settings.php @@ -129,6 +129,7 @@ 'brand' => 'Бренд', 'users' => 'Участники', 'api_keys' => 'API-ключи', + 'mcp' => 'MCP', ], 'title' => 'Настройки рабочего пространства', 'logo_heading' => 'Логотип рабочего пространства', diff --git a/lang/ru/sidebar.php b/lang/ru/sidebar.php index 4ebffe4e7..bf18d1bb2 100644 --- a/lang/ru/sidebar.php +++ b/lang/ru/sidebar.php @@ -8,6 +8,8 @@ 'create_workspace' => 'Создать рабочее пространство', 'create_post' => 'Создать пост', 'profile' => 'Профиль', + 'my_account' => 'Мой аккаунт', + 'account_settings' => 'Аккаунт и оплата', 'log_out' => 'Выйти', 'workspace' => 'Рабочее пространство: :name', @@ -30,6 +32,8 @@ 'analytics' => 'Аналитика', 'automations' => 'Автоматизации', 'settings' => 'Настройки', + 'onboarding' => 'Начало работы', + 'onboarding_hint' => 'Завершите настройку', 'posts' => [ 'calendar' => 'Календарь', @@ -44,7 +48,9 @@ 'signatures' => 'Подписи', 'labels' => 'Метки', 'assets' => 'Медиафайлы', + 'settings' => 'Настройки', 'api_keys' => 'API-ключи', + 'mcp' => 'MCP', ], 'notifications' => 'Уведомления', diff --git a/lang/ru/welcome.php b/lang/ru/welcome.php new file mode 100644 index 000000000..f84ae4d43 --- /dev/null +++ b/lang/ru/welcome.php @@ -0,0 +1,52 @@ + 'Что лучше всего вас описывает?', + 'description' => 'Выберите ближайший вариант — мы настроим опыт под вас.', + 'continue' => 'Продолжить', + 'checkout_owner_only' => 'Попросите владельца аккаунта завершить оплату и оформить подписку.', + 'progress' => 'Прогресс приветствия', + 'go_to_step' => 'Перейти к шагу :step', + 'personas' => [ + 'creator' => 'Автор контента', + 'freelancer' => 'Фрилансер', + 'developer' => 'Разработчик', + 'startup' => 'Стартап', + 'agency' => 'Агентство', + 'small_business' => 'Малый бизнес', + 'marketer' => 'Маркетолог', + 'online_store' => 'Интернет-магазин', + 'other' => 'Другое', + ], + 'goals_title' => 'Какова ваша цель?', + 'goals_description' => 'Выберите всё, что подходит, и мы настроим TryPost для вас.', + 'goals' => [ + 'save_time' => 'Экономить время, публикуя всюду сразу', + 'ai_content' => 'Создавать посты быстрее с помощью ИИ', + 'plan_calendar' => 'Планировать посты в календаре', + 'stay_on_brand' => 'Держать каждый пост в стиле бренда', + 'grow_audience' => 'Наращивать аудиторию и вовлечённость', + 'drive_sales' => 'Получать больше трафика и продаж', + 'manage_clients' => 'Управлять несколькими брендами или клиентами', + 'just_exploring' => 'Пока просто знакомлюсь', + 'other' => 'Что-то ещё', + ], + 'referral_source_title' => 'Как вы нас нашли?', + 'referral_source_description' => 'Это помогает нам понять, как люди узнают о TryPost.', + 'referral_source' => [ + 'google' => 'Google или поиск', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram или Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'ИИ-ассистент (ChatGPT, Claude…)', + 'friend' => 'Друг или коллега', + 'blog' => 'Блог, рассылка или статья', + 'other' => 'Что-то другое', + ], +]; diff --git a/lang/tr/mcp.php b/lang/tr/mcp.php new file mode 100644 index 000000000..c39fb668b --- /dev/null +++ b/lang/tr/mcp.php @@ -0,0 +1,41 @@ + 'MCP', + 'subtitle' => 'TryPost hesabınızla gönderi oluşturup yönetmeleri için yapay zeka asistanlarını bağlayın.', + 'step_add' => 'Adı, URL’yi veya config’i aşağıdaki gibi uygulamanıza yapıştırın. İlk bağlantıda oturum açma tarayıcıda açılır.', + 'name_label' => 'Ad', + 'url_label' => 'Sunucu URL’si', + 'config_label' => 'Config', + 'connected_title' => 'Bağlı uygulamalar', + 'connected_description' => 'Bu hesaptaki herhangi birinin giriş yaptığı asistanlar. Yalnızca kendininkileri bağlantıyı kesebilirsiniz.', + 'connected_empty' => 'Henüz bağlı bir şey yok. Yukarıdan Claude, ChatGPT veya başka bir istemci kullanın.', + 'connected_by' => ':name tarafından bağlandı', + 'disconnect' => 'Bağlantıyı kes', + 'disconnect_title' => 'Uygulama bağlantısını kes', + 'disconnect_confirm' => 'Bu, uygulamayı TryPost’tan çıkarır. MCP’yi yeniden kullanmak için tekrar bağlanması gerekir.', + 'disconnected' => 'Uygulama bağlantısı kesildi.', + 'copied' => 'Kopyalandı', + 'last_used' => 'Son kullanım', + 'never' => 'Hiç', + 'documentation_title' => 'Dokümantasyon', + 'documentation_description' => 'İstemci kurulum rehberleri, kullanılabilir tools ve sorun giderme.', + 'view_docs' => 'Dokümantasyonu görüntüle', + 'connector_name' => 'TryPost', + + 'other_clients_title' => 'Diğer uygulamalar', + 'other_clients_description' => 'Cursor, VS Code, Claude Code ve MCP konuşan diğer her şey.', + + 'clients' => [ + 'cursor' => 'Cursor’da TryPost’u uzak MCP sunucusu olarak ekleyin.', + 'cursor_name' => 'Cursor', + 'vscode' => 'Aşağıdaki yapılandırmayı VS Code\'un MCP ayarlarına yapıştırın.', + 'vscode_name' => 'VS Code', + 'claude_code' => 'Aşağıdaki yapılandırmayı Claude Code\'un MCP ayarlarına yapıştırın.', + 'claude_code_name' => 'Claude Code', + 'other' => 'mcpServers config okuyan her istemciyle çalışır.', + 'other_name' => 'Diğer', + ], +]; diff --git a/lang/tr/onboarding.php b/lang/tr/onboarding.php index b54630a4c..9a2d58b34 100644 --- a/lang/tr/onboarding.php +++ b/lang/tr/onboarding.php @@ -3,55 +3,45 @@ declare(strict_types=1); return [ - 'title' => 'TryPost\'a hoş geldiniz', - 'description' => 'Deneyiminizi kişiselleştirebilmemiz için sizi veya işinizi en iyi tanımlayan seçeneği belirtin.', - 'continue' => 'Devam et', - 'personas' => [ - 'creator' => 'İçerik üreticisi', - 'freelancer' => 'Serbest çalışan', - 'developer' => 'Geliştirici', - 'startup' => 'Girişim', - 'agency' => 'Ajans', - 'small_business' => 'Küçük işletme', - 'marketer' => 'Pazarlamacı', - 'online_store' => 'Çevrimiçi mağaza', - 'other' => 'Diğer', + 'title' => 'Başlarken', + 'welcome' => 'TryPost’a hoş geldin, :name', + 'welcome_anonymous' => 'TryPost’a hoş geldin', + 'description' => 'TryPost’un nasıl çalıştığını görmek ve ilk gönderini yayınlamak için aşağıdaki adımları izle.', + 'skip' => 'Şimdilik atla', + 'continue' => 'TryPost’a devam et', + 'status' => [ + 'complete' => 'Tamamlandı', + 'todo' => 'Yapılacak', ], - 'goals_title' => 'TryPost ile hedefiniz nedir?', - 'goals_description' => 'Size uyan her şeyi seçin, biz de TryPost\'u sizin için ayarlayalım.', - 'goals' => [ - 'save_time' => 'Her yere aynı anda paylaşarak zaman kazanmak', - 'ai_content' => 'AI ile daha hızlı gönderi oluşturmak', - 'plan_calendar' => 'Gönderilerimi bir takvimde planlamak', - 'stay_on_brand' => 'Her gönderiyi marka çizgisinde tutmak', - 'grow_audience' => 'Kitlemi ve etkileşimimi büyütmek', - 'drive_sales' => 'Daha fazla trafik ve satış elde etmek', - 'manage_clients' => 'Birden fazla marka veya müşteri yönetmek', - 'team_collaboration' => 'Ekibimle çalışmak', - 'automate_api' => 'API, MCP veya kodla paylaşımı otomatikleştirmek', - 'track_performance' => 'Gönderilerimin performansını görmek', - 'just_exploring' => 'Şimdilik sadece keşfetmek', - 'other' => 'Başka bir şey', + 'mcp' => [ + 'title' => 'AI asistanını bağla', + 'description' => 'Asistanının senin için sosyal gönderiler oluşturup yönetebilmesi için TryPost’u MCP sunucusu olarak ekle.', + 'copy_step' => 'TryPost sunucu URL’ini kopyala', + 'open_step' => 'AI asistanını aç', + 'copy' => 'URL’yi kopyala', + 'copied' => 'MCP URL’si kopyalandı.', + 'connect' => ':client ile bağlan', + 'clients' => [ + 'claude' => 'Settings → Connectors’ı aç, özel bir connector ekle ve yukarıdaki URL’yi yapıştır.', + 'chatgpt' => 'Settings → Apps & Connectors’ı aç, özel bir connector oluştur ve yukarıdaki URL’yi yapıştır.', + ], ], - 'referral_source_title' => 'Bizi nasıl buldunuz?', - 'referral_source_description' => 'İnsanların TryPost\'u nasıl keşfettiğini anlamamıza yardımcı olur.', - 'referral_source' => [ - 'google' => 'Google veya arama', - 'x' => 'X (Twitter)', - 'linkedin' => 'LinkedIn', - 'youtube' => 'YouTube', - 'tiktok' => 'TikTok', - 'instagram' => 'Instagram veya Threads', - 'reddit' => 'Reddit', - 'product_hunt' => 'Product Hunt', - 'ai_assistant' => 'Yapay zeka asistanı (ChatGPT, Claude…)', - 'friend' => 'Arkadaş veya meslektaş', - 'blog' => 'Blog, bülten veya makale', - 'other' => 'Başka bir şey', + 'social' => [ + 'title' => 'Bir sosyal hesap bağla', + 'description' => 'TryPost’un içeriğini yayınlayabileceği en az bir ağ seç.', ], - 'connect' => [ - 'title' => 'İlk ağınızı bağlayın', - 'description' => 'Zamanlamaya başlamak için en az bir sosyal hesap bağlayın. İstediğiniz zaman daha fazlasını ekleyebilirsiniz.', - 'must_connect' => 'Devam etmek için en az bir ağ bağlayın.', + 'first_post' => [ + 'title' => 'İlk gönderini oluştur', + 'description' => 'Bu başlangıç prompt’unu bağlı asistanınla dene veya gönderiyi doğrudan TryPost’ta oluştur.', + 'prompt_label' => 'Örnek prompt', + 'sample_prompt' => 'Markamı tanıtan samimi bir sosyal gönderi oluştur ve bağlı her ağ için uyarla.', + 'copy_prompt' => 'Prompt’u kopyala', + 'copied' => 'Örnek prompt kopyalandı.', + 'create_button' => 'İlk gönderini oluştur', + 'or' => 'veya', + ], + 'ready' => [ + 'title' => 'Yayınlamaya hazırsın', + 'description' => 'Her şey hazır. TryPost’a devam et ve içeriğini planlamaya başla.', ], ]; diff --git a/lang/tr/settings.php b/lang/tr/settings.php index 148bb834b..4c6d93f50 100644 --- a/lang/tr/settings.php +++ b/lang/tr/settings.php @@ -131,6 +131,7 @@ 'brand' => 'Marka', 'users' => 'Üyeler', 'api_keys' => 'API Anahtarları', + 'mcp' => 'MCP', ], 'title' => 'Çalışma alanı ayarları', 'logo_heading' => 'Çalışma alanı logosu', diff --git a/lang/tr/sidebar.php b/lang/tr/sidebar.php index c7936fc96..393710fce 100644 --- a/lang/tr/sidebar.php +++ b/lang/tr/sidebar.php @@ -8,6 +8,8 @@ 'create_workspace' => 'Çalışma alanı oluştur', 'create_post' => 'Gönderi oluştur', 'profile' => 'Profil', + 'my_account' => 'Hesabım', + 'account_settings' => 'Hesap ve faturalandırma', 'log_out' => 'Çıkış yap', 'workspace' => 'Çalışma alanı: :name', @@ -30,6 +32,8 @@ 'analytics' => 'Analitik', 'automations' => 'Otomasyonlar', 'settings' => 'Ayarlar', + 'onboarding' => 'Başlarken', + 'onboarding_hint' => 'Kurulumu bitir', 'posts' => [ 'calendar' => 'Takvim', @@ -44,7 +48,9 @@ 'signatures' => 'İmzalar', 'labels' => 'Etiketler', 'assets' => 'Varlıklar', + 'settings' => 'Ayarlar', 'api_keys' => 'API Anahtarları', + 'mcp' => 'MCP', ], 'notifications' => 'Bildirimler', diff --git a/lang/tr/welcome.php b/lang/tr/welcome.php new file mode 100644 index 000000000..e11329092 --- /dev/null +++ b/lang/tr/welcome.php @@ -0,0 +1,52 @@ + 'Sizi en iyi ne tanımlar?', + 'description' => 'En yakın seçeneği seçin, deneyiminizi kişiselleştirelim.', + 'continue' => 'Devam et', + 'checkout_owner_only' => 'Hesap sahibinden ödemeyi tamamlayıp aboneliği başlatmasını isteyin.', + 'progress' => 'Karşılama ilerlemesi', + 'go_to_step' => ':step. adıma git', + 'personas' => [ + 'creator' => 'İçerik üreticisi', + 'freelancer' => 'Serbest çalışan', + 'developer' => 'Geliştirici', + 'startup' => 'Girişim', + 'agency' => 'Ajans', + 'small_business' => 'Küçük işletme', + 'marketer' => 'Pazarlamacı', + 'online_store' => 'Çevrimiçi mağaza', + 'other' => 'Diğer', + ], + 'goals_title' => 'Hedefiniz nedir?', + 'goals_description' => 'Size uyan her şeyi seçin, biz de TryPost\'u sizin için ayarlayalım.', + 'goals' => [ + 'save_time' => 'Her yere aynı anda paylaşarak zaman kazanmak', + 'ai_content' => 'AI ile daha hızlı gönderi oluşturmak', + 'plan_calendar' => 'Gönderilerimi bir takvimde planlamak', + 'stay_on_brand' => 'Her gönderiyi marka çizgisinde tutmak', + 'grow_audience' => 'Kitlemi ve etkileşimimi büyütmek', + 'drive_sales' => 'Daha fazla trafik ve satış elde etmek', + 'manage_clients' => 'Birden fazla marka veya müşteri yönetmek', + 'just_exploring' => 'Şimdilik sadece keşfetmek', + 'other' => 'Başka bir şey', + ], + 'referral_source_title' => 'Bizi nasıl buldunuz?', + 'referral_source_description' => 'İnsanların TryPost\'u nasıl keşfettiğini anlamamıza yardımcı olur.', + 'referral_source' => [ + 'google' => 'Google veya arama', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram veya Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'Yapay zeka asistanı (ChatGPT, Claude…)', + 'friend' => 'Arkadaş veya meslektaş', + 'blog' => 'Blog, bülten veya makale', + 'other' => 'Başka bir şey', + ], +]; diff --git a/lang/zh/mcp.php b/lang/zh/mcp.php new file mode 100644 index 000000000..66addb6e4 --- /dev/null +++ b/lang/zh/mcp.php @@ -0,0 +1,41 @@ + 'MCP', + 'subtitle' => '连接 AI 助手,让它们用你的 TryPost 账户创建和管理帖子。', + 'step_add' => '将下方的名称、URL 或配置粘贴到你的应用中。首次连接时会在浏览器中打开登录。', + 'name_label' => '名称', + 'url_label' => '服务器 URL', + 'config_label' => '配置', + 'connected_title' => '已连接的应用', + 'connected_description' => '此账户中任何人已登录的助手。你只能断开自己的连接。', + 'connected_empty' => '还没有连接。请使用上方的 Claude、ChatGPT 或其他客户端。', + 'connected_by' => '由 :name 连接', + 'disconnect' => '断开连接', + 'disconnect_title' => '断开应用', + 'disconnect_confirm' => '这将使应用退出 TryPost。再次使用 MCP 前需要重新连接。', + 'disconnected' => '应用已断开连接。', + 'copied' => '已复制', + 'last_used' => '最近使用', + 'never' => '从未', + 'documentation_title' => '文档', + 'documentation_description' => '各客户端设置指南、可用工具和问题排查。', + 'view_docs' => '查看文档', + 'connector_name' => 'TryPost', + + 'other_clients_title' => '其他应用', + 'other_clients_description' => 'Cursor、VS Code、Claude Code 以及任何支持 MCP 的应用。', + + 'clients' => [ + 'cursor' => '在 Cursor 中将 TryPost 添加为远程 MCP 服务器。', + 'cursor_name' => 'Cursor', + 'vscode' => '将下方配置粘贴到 VS Code 的 MCP 设置中。', + 'vscode_name' => 'VS Code', + 'claude_code' => '将下方配置粘贴到 Claude Code 的 MCP 设置中。', + 'claude_code_name' => 'Claude Code', + 'other' => '适用于任何读取 mcpServers 配置的客户端。', + 'other_name' => '其他', + ], +]; diff --git a/lang/zh/onboarding.php b/lang/zh/onboarding.php index 9761ce99a..5558ea170 100644 --- a/lang/zh/onboarding.php +++ b/lang/zh/onboarding.php @@ -3,55 +3,45 @@ declare(strict_types=1); return [ - 'title' => '欢迎使用 TryPost', - 'description' => '告诉我们最能描述你或你业务的选项,以便我们为你定制体验。', - 'continue' => '继续', - 'personas' => [ - 'creator' => '内容创作者', - 'freelancer' => '自由职业者', - 'developer' => '开发者', - 'startup' => '初创公司', - 'agency' => '代理机构', - 'small_business' => '小型企业', - 'marketer' => '营销人员', - 'online_store' => '网店', - 'other' => '其他', + 'title' => '开始使用', + 'welcome' => '欢迎使用 TryPost,:name', + 'welcome_anonymous' => '欢迎使用 TryPost', + 'description' => '按下面的步骤了解 TryPost 的用法,并发布你的第一条内容。', + 'skip' => '暂时跳过', + 'continue' => '继续前往 TryPost', + 'status' => [ + 'complete' => '已完成', + 'todo' => '待完成', ], - 'goals_title' => '你使用 TryPost 的目标是什么?', - 'goals_description' => '选择所有符合的选项,我们会为你配置好 TryPost。', - 'goals' => [ - 'save_time' => '一次发布到所有平台,节省时间', - 'ai_content' => '借助 AI 更快地创建帖子', - 'plan_calendar' => '在日历上规划我的帖子', - 'stay_on_brand' => '让每一条帖子都符合品牌调性', - 'grow_audience' => '增长我的受众和互动', - 'drive_sales' => '获得更多流量和销量', - 'manage_clients' => '管理多个品牌或客户', - 'team_collaboration' => '与我的团队协作', - 'automate_api' => '通过 API、MCP 或代码自动发帖', - 'track_performance' => '查看我的帖子表现', - 'just_exploring' => '目前只是随便看看', - 'other' => '其他需求', + 'mcp' => [ + 'title' => '连接你的 AI 助手', + 'description' => '将 TryPost 添加为 MCP 服务器,让助手帮你创建和管理社交内容。', + 'copy_step' => '复制你的 TryPost 服务器 URL', + 'open_step' => '打开你的 AI 助手', + 'copy' => '复制 URL', + 'copied' => '已复制 MCP URL。', + 'connect' => '使用 :client 连接', + 'clients' => [ + 'claude' => '打开 Settings → Connectors,添加自定义连接器,然后粘贴上方 URL。', + 'chatgpt' => '打开 Settings → Apps & Connectors,创建自定义连接器,然后粘贴上方 URL。', + ], ], - 'referral_source_title' => '您是如何找到我们的?', - 'referral_source_description' => '这有助于我们了解人们是如何发现 TryPost 的。', - 'referral_source' => [ - 'google' => 'Google 或搜索', - 'x' => 'X (Twitter)', - 'linkedin' => 'LinkedIn', - 'youtube' => 'YouTube', - 'tiktok' => 'TikTok', - 'instagram' => 'Instagram 或 Threads', - 'reddit' => 'Reddit', - 'product_hunt' => 'Product Hunt', - 'ai_assistant' => 'AI 助手(ChatGPT、Claude 等)', - 'friend' => '朋友或同事', - 'blog' => '博客、新闻通讯或文章', - 'other' => '其他', + 'social' => [ + 'title' => '连接社交账号', + 'description' => '至少选择一个网络,让 TryPost 可以发布你的内容。', ], - 'connect' => [ - 'title' => '连接你的第一个平台', - 'description' => '至少关联一个社交账号即可开始排期。你可以随时添加更多。', - 'must_connect' => '请至少连接一个平台以继续。', + 'first_post' => [ + 'title' => '创建第一条内容', + 'description' => '用已连接的助手试试这个入门提示词,或直接在 TryPost 中创建内容。', + 'prompt_label' => '示例提示词', + 'sample_prompt' => '写一条友好的社交内容介绍我的品牌,并为每个已连接的网络做适配。', + 'copy_prompt' => '复制提示词', + 'copied' => '已复制示例提示词。', + 'create_button' => '创建第一条内容', + 'or' => '或', + ], + 'ready' => [ + 'title' => '可以开始发布了', + 'description' => '一切就绪。继续前往 TryPost,开始规划你的内容。', ], ]; diff --git a/lang/zh/settings.php b/lang/zh/settings.php index eff7e82ca..590fde641 100644 --- a/lang/zh/settings.php +++ b/lang/zh/settings.php @@ -129,6 +129,7 @@ 'brand' => '品牌', 'users' => '成员', 'api_keys' => 'API 密钥', + 'mcp' => 'MCP', ], 'title' => '工作区设置', 'logo_heading' => '工作区徽标', diff --git a/lang/zh/sidebar.php b/lang/zh/sidebar.php index 46a3eda6f..3a9389b81 100644 --- a/lang/zh/sidebar.php +++ b/lang/zh/sidebar.php @@ -8,6 +8,8 @@ 'create_workspace' => '创建工作区', 'create_post' => '创建帖子', 'profile' => '个人资料', + 'my_account' => '我的账户', + 'account_settings' => '账户与账单', 'log_out' => '退出登录', 'workspace' => '工作区::name', @@ -30,6 +32,8 @@ 'analytics' => '分析', 'automations' => '自动化', 'settings' => '设置', + 'onboarding' => '开始使用', + 'onboarding_hint' => '完成设置', 'posts' => [ 'calendar' => '日历', @@ -44,7 +48,9 @@ 'signatures' => '签名', 'labels' => '标签', 'assets' => '素材库', + 'settings' => '设置', 'api_keys' => 'API 密钥', + 'mcp' => 'MCP', ], 'notifications' => '通知', diff --git a/lang/zh/welcome.php b/lang/zh/welcome.php new file mode 100644 index 000000000..51ba40153 --- /dev/null +++ b/lang/zh/welcome.php @@ -0,0 +1,52 @@ + '哪项最能描述你?', + 'description' => '选择最接近的一项,我们会为你定制体验。', + 'continue' => '继续', + 'checkout_owner_only' => '请让账户所有者完成结账并开始订阅。', + 'progress' => '欢迎进度', + 'go_to_step' => '前往第 :step 步', + 'personas' => [ + 'creator' => '内容创作者', + 'freelancer' => '自由职业者', + 'developer' => '开发者', + 'startup' => '初创公司', + 'agency' => '代理机构', + 'small_business' => '小型企业', + 'marketer' => '营销人员', + 'online_store' => '网店', + 'other' => '其他', + ], + 'goals_title' => '你的目标是什么?', + 'goals_description' => '选择所有符合的选项,我们会为你配置好 TryPost。', + 'goals' => [ + 'save_time' => '一次发布到所有平台,节省时间', + 'ai_content' => '借助 AI 更快地创建帖子', + 'plan_calendar' => '在日历上规划我的帖子', + 'stay_on_brand' => '让每一条帖子都符合品牌调性', + 'grow_audience' => '增长我的受众和互动', + 'drive_sales' => '获得更多流量和销量', + 'manage_clients' => '管理多个品牌或客户', + 'just_exploring' => '目前只是随便看看', + 'other' => '其他需求', + ], + 'referral_source_title' => '您是如何找到我们的?', + 'referral_source_description' => '这有助于我们了解人们是如何发现 TryPost 的。', + 'referral_source' => [ + 'google' => 'Google 或搜索', + 'x' => 'X (Twitter)', + 'linkedin' => 'LinkedIn', + 'youtube' => 'YouTube', + 'tiktok' => 'TikTok', + 'instagram' => 'Instagram 或 Threads', + 'reddit' => 'Reddit', + 'product_hunt' => 'Product Hunt', + 'ai_assistant' => 'AI 助手(ChatGPT、Claude 等)', + 'friend' => '朋友或同事', + 'blog' => '博客、新闻通讯或文章', + 'other' => '其他', + ], +]; diff --git a/public/images/ai/chatgpt-black.svg b/public/images/ai/chatgpt-black.svg new file mode 100644 index 000000000..9c80705e5 --- /dev/null +++ b/public/images/ai/chatgpt-black.svg @@ -0,0 +1 @@ +ChatGPT \ No newline at end of file diff --git a/public/images/ai/chatgpt-white.svg b/public/images/ai/chatgpt-white.svg new file mode 100644 index 000000000..fb4d0ac4d --- /dev/null +++ b/public/images/ai/chatgpt-white.svg @@ -0,0 +1 @@ +OpenAI \ No newline at end of file diff --git a/public/images/ai/claude.svg b/public/images/ai/claude.svg new file mode 100644 index 000000000..5d8d7461d --- /dev/null +++ b/public/images/ai/claude.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/js/components/AppSidebar.vue b/resources/js/components/AppSidebar.vue index 7aea78e49..019aec24b 100644 --- a/resources/js/components/AppSidebar.vue +++ b/resources/js/components/AppSidebar.vue @@ -1,5 +1,5 @@ \ No newline at end of file + diff --git a/resources/js/components/NavUser.vue b/resources/js/components/NavUser.vue deleted file mode 100644 index 8253dbf42..000000000 --- a/resources/js/components/NavUser.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - diff --git a/resources/js/components/NotificationBell.vue b/resources/js/components/NotificationBell.vue index 7fcd5aab1..d6db6e5d6 100644 --- a/resources/js/components/NotificationBell.vue +++ b/resources/js/components/NotificationBell.vue @@ -205,16 +205,16 @@ onBeforeUnmount(() => {
diff --git a/resources/js/components/UserMenuContent.vue b/resources/js/components/UserMenuContent.vue deleted file mode 100644 index 2fc9c54da..000000000 --- a/resources/js/components/UserMenuContent.vue +++ /dev/null @@ -1,115 +0,0 @@ - - - diff --git a/resources/js/components/WorkspaceMenuContent.vue b/resources/js/components/WorkspaceMenuContent.vue new file mode 100644 index 000000000..600640e38 --- /dev/null +++ b/resources/js/components/WorkspaceMenuContent.vue @@ -0,0 +1,210 @@ + + + diff --git a/resources/js/components/accounts/NetworkConnectGrid.vue b/resources/js/components/accounts/NetworkConnectGrid.vue index 0960a3552..ae7d62d2b 100644 --- a/resources/js/components/accounts/NetworkConnectGrid.vue +++ b/resources/js/components/accounts/NetworkConnectGrid.vue @@ -34,10 +34,13 @@ const props = withDefaults( platforms: AvailablePlatform[]; connectedAccounts?: ConnectedAccount[]; gridClass?: string; + /** When set, OAuth success reloads only these Inertia props (e.g. onboarding). */ + reloadOnly?: string[]; }>(), { connectedAccounts: () => [], gridClass: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5', + reloadOnly: undefined, }, ); @@ -157,7 +160,11 @@ const disconnectModal = ref | null>( const { openOAuthPopup } = useOAuthPopup((result) => { if (result.success) { toast.success(result.message); - router.reload(); + router.reload( + props.reloadOnly !== undefined && props.reloadOnly.length > 0 + ? { only: props.reloadOnly } + : undefined, + ); return; } diff --git a/resources/js/components/mcp/icons/ChatgptIcon.vue b/resources/js/components/mcp/icons/ChatgptIcon.vue new file mode 100644 index 000000000..350082a66 --- /dev/null +++ b/resources/js/components/mcp/icons/ChatgptIcon.vue @@ -0,0 +1,8 @@ + diff --git a/resources/js/components/mcp/icons/ClaudeIcon.vue b/resources/js/components/mcp/icons/ClaudeIcon.vue new file mode 100644 index 000000000..1c88e8c84 --- /dev/null +++ b/resources/js/components/mcp/icons/ClaudeIcon.vue @@ -0,0 +1,8 @@ + diff --git a/resources/js/components/mcp/icons/CursorIcon.vue b/resources/js/components/mcp/icons/CursorIcon.vue new file mode 100644 index 000000000..d903b3d1d --- /dev/null +++ b/resources/js/components/mcp/icons/CursorIcon.vue @@ -0,0 +1,8 @@ + diff --git a/resources/js/components/mcp/icons/OtherClientsIcon.vue b/resources/js/components/mcp/icons/OtherClientsIcon.vue new file mode 100644 index 000000000..0fce179e8 --- /dev/null +++ b/resources/js/components/mcp/icons/OtherClientsIcon.vue @@ -0,0 +1,8 @@ + diff --git a/resources/js/components/mcp/icons/VscodeIcon.vue b/resources/js/components/mcp/icons/VscodeIcon.vue new file mode 100644 index 000000000..290939998 --- /dev/null +++ b/resources/js/components/mcp/icons/VscodeIcon.vue @@ -0,0 +1,8 @@ + diff --git a/resources/js/components/onboarding/OnboardingStepCard.vue b/resources/js/components/onboarding/OnboardingStepCard.vue new file mode 100644 index 000000000..694b4e79e --- /dev/null +++ b/resources/js/components/onboarding/OnboardingStepCard.vue @@ -0,0 +1,66 @@ + + + diff --git a/resources/js/components/ui/accordion/Accordion.vue b/resources/js/components/ui/accordion/Accordion.vue new file mode 100644 index 000000000..58ad491e6 --- /dev/null +++ b/resources/js/components/ui/accordion/Accordion.vue @@ -0,0 +1,18 @@ + + + diff --git a/resources/js/components/ui/accordion/AccordionContent.vue b/resources/js/components/ui/accordion/AccordionContent.vue new file mode 100644 index 000000000..91c8dccd3 --- /dev/null +++ b/resources/js/components/ui/accordion/AccordionContent.vue @@ -0,0 +1,23 @@ + + + diff --git a/resources/js/components/ui/accordion/AccordionItem.vue b/resources/js/components/ui/accordion/AccordionItem.vue new file mode 100644 index 000000000..d890a2b11 --- /dev/null +++ b/resources/js/components/ui/accordion/AccordionItem.vue @@ -0,0 +1,24 @@ + + + diff --git a/resources/js/components/ui/accordion/AccordionTrigger.vue b/resources/js/components/ui/accordion/AccordionTrigger.vue new file mode 100644 index 000000000..c20ffba37 --- /dev/null +++ b/resources/js/components/ui/accordion/AccordionTrigger.vue @@ -0,0 +1,37 @@ + + + diff --git a/resources/js/components/ui/accordion/index.ts b/resources/js/components/ui/accordion/index.ts new file mode 100644 index 000000000..b18018b56 --- /dev/null +++ b/resources/js/components/ui/accordion/index.ts @@ -0,0 +1,4 @@ +export { default as Accordion } from "./Accordion.vue" +export { default as AccordionContent } from "./AccordionContent.vue" +export { default as AccordionItem } from "./AccordionItem.vue" +export { default as AccordionTrigger } from "./AccordionTrigger.vue" diff --git a/resources/js/composables/echo/useOnboardingStatusEcho.ts b/resources/js/composables/echo/useOnboardingStatusEcho.ts new file mode 100644 index 000000000..66009f5a0 --- /dev/null +++ b/resources/js/composables/echo/useOnboardingStatusEcho.ts @@ -0,0 +1,70 @@ +import { router, usePage, usePoll } from '@inertiajs/vue3'; +import { watch } from 'vue'; + +import { useWorkspaceEcho } from '@/composables/echo/useWorkspaceEcho'; + +/** + * Refresh the given Inertia props when onboarding checklist progress changes. + */ +export const useOnboardingStatusEcho = (only: string[]): void => { + useWorkspaceEcho('.onboarding.status.updated', () => { + router.reload({ only }); + }); +}; + +/** + * Keep the sidebar residual banner in sync while it is visible. + * Skips on the onboarding page — that page already reloads residual with status. + * + * Echo is the fast path; a slow poll covers Reverb outages. Polling only runs + * while the banner is visible so completed/dismissed owners are not hit. + * + * When residual is false we stop listening/polling. That is effectively terminal + * in practice (completed, dismissed, non-owner, no app access, or another + * workspace already finished activation). Observers/syncProgress stamp + * completion for the cross-workspace case, so false rarely flips back to a + * progress object without a full navigation. + */ +export const useOnboardingResidualEcho = (): void => { + const page = usePage(); + + useWorkspaceEcho('.onboarding.status.updated', () => { + if (page.props.onboardingResidual === false) { + return; + } + + if (page.component === 'onboarding/Index') { + return; + } + + router.reload({ only: ['onboardingResidual'] }); + }); + + const { start, stop } = usePoll( + 10000, + { + only: ['onboardingResidual'], + }, + { + autoStart: false, + }, + ); + + watch( + () => + [ + page.props.onboardingResidual, + page.component, + ] as const, + ([residual, component]) => { + if (residual === false || component === 'onboarding/Index') { + stop(); + + return; + } + + start(); + }, + { immediate: true }, + ); +}; diff --git a/resources/js/composables/useWorkspaceSettingsTabs.ts b/resources/js/composables/useWorkspaceSettingsTabs.ts new file mode 100644 index 000000000..c645affa8 --- /dev/null +++ b/resources/js/composables/useWorkspaceSettingsTabs.ts @@ -0,0 +1,36 @@ +import { computed } from 'vue'; +import { trans } from 'laravel-vue-i18n'; + +import { members as membersRoute } from '@/routes/app'; +import { index as apiKeysRoute } from '@/routes/app/api-keys'; +import { index as mcpRoute } from '@/routes/app/mcp'; +import { brand as brandRoute, settings as workspaceSettings } from '@/routes/app/workspace'; + +export const useWorkspaceSettingsTabs = () => + computed(() => [ + { + name: 'workspace', + label: trans('settings.workspace.tabs.workspace'), + href: workspaceSettings.url(), + }, + { + name: 'brand', + label: trans('settings.workspace.tabs.brand'), + href: brandRoute.url(), + }, + { + name: 'members', + label: trans('settings.workspace.tabs.users'), + href: membersRoute.url(), + }, + { + name: 'api-keys', + label: trans('settings.workspace.tabs.api_keys'), + href: apiKeysRoute.url(), + }, + { + name: 'mcp', + label: trans('settings.workspace.tabs.mcp'), + href: mcpRoute.url(), + }, + ]); diff --git a/resources/js/layouts/OnboardingLayout.vue b/resources/js/layouts/OnboardingLayout.vue deleted file mode 100644 index 2f859c55a..000000000 --- a/resources/js/layouts/OnboardingLayout.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - diff --git a/resources/js/layouts/WelcomeLayout.vue b/resources/js/layouts/WelcomeLayout.vue new file mode 100644 index 000000000..46c166052 --- /dev/null +++ b/resources/js/layouts/WelcomeLayout.vue @@ -0,0 +1,119 @@ + + + diff --git a/resources/js/layouts/app/AppSidebarLayout.vue b/resources/js/layouts/app/AppSidebarLayout.vue index f0ac4b825..f5421d09b 100644 --- a/resources/js/layouts/app/AppSidebarLayout.vue +++ b/resources/js/layouts/app/AppSidebarLayout.vue @@ -5,7 +5,11 @@ import { onBeforeUnmount, onMounted } from 'vue'; import AppHeader from '@/components/AppHeader.vue'; import AppSidebar from '@/components/AppSidebar.vue'; import Toast from '@/components/Toast.vue'; -import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'; +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from '@/components/ui/sidebar'; import { heartbeat as heartbeatRoute } from '@/routes/app/presence'; const page = usePage(); @@ -52,7 +56,7 @@ onBeforeUnmount(() => {
(); @@ -34,7 +35,8 @@ const { trackPurchase } = useTracking(); const finishing = ref(false); let redirectTimer: ReturnType | null = null; -const goToAccounts = () => router.visit(accounts.url()); +const goNext = () => + router.visit(props.redirectToOnboarding ? onboarding.url() : calendar.url()); // Fires `checkout.completed` exactly once for a real checkout. A trial-with-card // subscription is already `subscribed()` (status `trialing`) by the time the @@ -61,7 +63,7 @@ const completePurchase = () => { // Always hold for the same window before navigating, so PostHog and the ad // pixels (Google/Meta via dataLayer → GTM) reliably flush. - redirectTimer = setTimeout(goToAccounts, REDIRECT_DELAY_MS); + redirectTimer = setTimeout(goNext, REDIRECT_DELAY_MS); }; watch( diff --git a/resources/js/pages/onboarding/Connect.vue b/resources/js/pages/onboarding/Connect.vue deleted file mode 100644 index 80ae728ab..000000000 --- a/resources/js/pages/onboarding/Connect.vue +++ /dev/null @@ -1,83 +0,0 @@ - - - diff --git a/resources/js/pages/onboarding/Goals.vue b/resources/js/pages/onboarding/Goals.vue deleted file mode 100644 index a5e7db0d1..000000000 --- a/resources/js/pages/onboarding/Goals.vue +++ /dev/null @@ -1,147 +0,0 @@ - - - diff --git a/resources/js/pages/onboarding/Index.vue b/resources/js/pages/onboarding/Index.vue index 376c9d09c..2b1550942 100644 --- a/resources/js/pages/onboarding/Index.vue +++ b/resources/js/pages/onboarding/Index.vue @@ -1,127 +1,389 @@ diff --git a/resources/js/pages/onboarding/ReferralSource.vue b/resources/js/pages/onboarding/ReferralSource.vue deleted file mode 100644 index 76afe58b4..000000000 --- a/resources/js/pages/onboarding/ReferralSource.vue +++ /dev/null @@ -1,135 +0,0 @@ - - - diff --git a/resources/js/pages/settings/workspace/ApiKeys.vue b/resources/js/pages/settings/workspace/ApiKeys.vue index b12444f20..6e2827a85 100644 --- a/resources/js/pages/settings/workspace/ApiKeys.vue +++ b/resources/js/pages/settings/workspace/ApiKeys.vue @@ -30,9 +30,8 @@ import { import date from '@/date'; import AppLayout from '@/layouts/AppLayout.vue'; import { copyToClipboard } from '@/lib/utils'; -import { members as membersRoute } from '@/routes/app'; -import { index as apiKeysRoute } from '@/routes/app/api-keys'; -import { brand as brandRoute, settings as workspaceSettings } from '@/routes/app/workspace'; +import { useWorkspaceSettingsTabs } from '@/composables/useWorkspaceSettingsTabs'; + interface ApiToken { id: string; name: string; @@ -53,12 +52,7 @@ const newToken = computed(() => (page.props.flash as Record)?.p const createDialogOpen = ref(false); const confirmDeleteModal = ref | null>(null); -const tabs = computed(() => [ - { name: 'workspace', label: trans('settings.workspace.tabs.workspace'), href: workspaceSettings.url() }, - { name: 'brand', label: trans('settings.workspace.tabs.brand'), href: brandRoute.url() }, - { name: 'members', label: trans('settings.workspace.tabs.users'), href: membersRoute.url() }, - { name: 'api-keys', label: trans('settings.workspace.tabs.api_keys'), href: apiKeysRoute.url() }, -]); +const tabs = useWorkspaceSettingsTabs();