diff --git a/.planning/STATE.md b/.planning/STATE.md index 31164dd..5848c62 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,8 +4,8 @@ milestone: v2.3 milestone_name: Global & Social Ready status: executing stopped_at: Phase 34 context gathered -last_updated: "2026-04-13T16:24:18.387Z" -last_activity: 2026-04-13 +last_updated: "2026-04-13T16:27:56.612Z" +last_activity: 2026-04-13 -- Phase 34 execution started progress: total_phases: 16 completed_phases: 6 @@ -21,14 +21,14 @@ progress: See: .planning/PROJECT.md (updated 2026-04-09) **Core value:** Help people make better gear decisions — discover what others use, compare real-world data, and see how a potential buy affects your setup before committing. -**Current focus:** Phase 34 — i18n Foundation +**Current focus:** Phase 34 — i18n-foundation ## Current Position -Phase: 999.1 -Plan: Not started +Phase: 34 (i18n-foundation) — EXECUTING +Plan: 1 of 5 Status: Executing Phase 34 -Last activity: 2026-04-13 +Last activity: 2026-04-13 -- Phase 34 execution started Progress: [░░░░░░░░░░] 0% diff --git a/.planning/phases/32-setup-sharing-system/32-UAT.md b/.planning/phases/32-setup-sharing-system/32-UAT.md new file mode 100644 index 0000000..733e005 --- /dev/null +++ b/.planning/phases/32-setup-sharing-system/32-UAT.md @@ -0,0 +1,69 @@ +--- +status: testing +phase: 32-setup-sharing-system +source: [32-01-SUMMARY.md, 32-02-SUMMARY.md, 32-03-SUMMARY.md, 32-04-SUMMARY.md] +started: 2026-04-13T18:00:00.000Z +updated: 2026-04-13T18:00:00.000Z +--- + +## Current Test + +number: 1 +name: Visibility badge on setup cards +expected: | + On the setups list page, each setup card shows a visibility indicator. Public setups show a green globe icon, link-shared show a blue link icon, and private show a gray lock icon. +awaiting: user response + +## Tests + +### 1. Visibility badge on setup cards +expected: On the setups list page, each setup card shows a visibility indicator. Public setups show a green globe icon, link-shared show a blue link icon, and private show a gray lock icon. +result: [pending] + +### 2. Share button on setup detail page +expected: On a setup detail page (as the owner), there's a "Share" button (desktop: text + icon, mobile: icon-only 44px touch target) that replaces the old public/private globe toggle. The icon reflects the current visibility state. +result: [pending] + +### 3. Share modal — visibility picker +expected: Clicking the Share button opens a modal with three visibility options: Private (gray), Link (blue), Public (green). Selecting one immediately updates the setup's visibility via API call. Current state is highlighted. +result: [pending] + +### 4. Share modal — create share link +expected: In the share modal, there's a section to create share links with an expiration dropdown (7 days, 14 days, 30 days, No expiration). Creating a link generates a URL and shows it in the active links list. +result: [pending] + +### 5. Share modal — copy and revoke links +expected: Each active share link in the modal has a copy-to-clipboard button and a revoke button. Copying puts the URL in the clipboard. Revoking removes the link from the active list. +result: [pending] + +### 6. Share modal — private deactivates links +expected: When switching visibility to "Private" while share links exist, links are deactivated (not deleted). Switching back to "Link" reactivates them. +result: [pending] + +### 7. Short URL access (/s/token) +expected: Visiting /s/{token} redirects to /setups/{id}?share={token}. The setup loads correctly showing its items and totals. +result: [pending] + +### 8. Shared setup viewer — read-only mode +expected: When viewing a setup via share token, a blue "Shared setup" banner appears at the top. All owner controls are hidden: no Add Items, no Share button, no Delete, no item removal, no classification cycling. +result: [pending] + +### 9. Invalid share token error +expected: Visiting a setup with an invalid or expired share token shows a "Link not available" error page instead of the setup content. +result: [pending] + +### 10. Discovery feed uses visibility +expected: Only setups with visibility="public" appear on the discovery feed and profile pages. Link-shared and private setups do not appear. +result: [pending] + +## Summary + +total: 10 +passed: 0 +issues: 0 +pending: 10 +skipped: 0 + +## Gaps + +[none yet] diff --git a/src/client/components/BottomTabBar.tsx b/src/client/components/BottomTabBar.tsx index 918d0d8..998f930 100644 --- a/src/client/components/BottomTabBar.tsx +++ b/src/client/components/BottomTabBar.tsx @@ -48,7 +48,11 @@ export function BottomTabBar() {
{/* Home tab — always a Link */} - + {/* Collection tab — Link if authenticated, button if anonymous */} @@ -73,17 +77,29 @@ export function BottomTabBar() { {/* Setups tab — Link if authenticated, button if anonymous */} {isAuthenticated ? ( - + ) : ( )} {/* Search tab — always a button, opens CatalogSearchOverlay */}
diff --git a/src/client/components/ExternalLinkDialog.tsx b/src/client/components/ExternalLinkDialog.tsx index de92c76..c167f37 100644 --- a/src/client/components/ExternalLinkDialog.tsx +++ b/src/client/components/ExternalLinkDialog.tsx @@ -39,7 +39,9 @@ export function ExternalLinkDialog() {

{t("externalLink.title")}

-

{t("externalLink.redirectMessage")}

+

+ {t("externalLink.redirectMessage")} +

{externalLinkUrl}

diff --git a/src/client/components/onboarding/OnboardingDone.tsx b/src/client/components/onboarding/OnboardingDone.tsx index 4331730..da34e76 100644 --- a/src/client/components/onboarding/OnboardingDone.tsx +++ b/src/client/components/onboarding/OnboardingDone.tsx @@ -24,9 +24,7 @@ export function OnboardingDone({

{t("done.title")}

-

- {t("done.subtitle")} -

+

{t("done.subtitle")}

@@ -321,7 +321,9 @@ function ResolveDialog({ disabled={resolveThread.isPending} className="px-4 py-2 text-sm font-medium text-white bg-amber-600 hover:bg-amber-700 disabled:opacity-50 rounded-lg transition-colors" > - {resolveThread.isPending ? t("actions.saving") : t("confirm.pickWinner")} + {resolveThread.isPending + ? t("actions.saving") + : t("confirm.pickWinner")} diff --git a/src/client/routes/global-items/$globalItemId.tsx b/src/client/routes/global-items/$globalItemId.tsx index 7dc9f1c..2eae2a9 100644 --- a/src/client/routes/global-items/$globalItemId.tsx +++ b/src/client/routes/global-items/$globalItemId.tsx @@ -3,14 +3,13 @@ import { useState } from "react"; import { GearImage, imageContainerBg } from "../../components/GearImage"; import { useAuth } from "../../hooks/useAuth"; import { useCurrency } from "../../hooks/useCurrency"; -import { useExchangeRates } from "../../hooks/useExchangeRates"; import { useFormatters } from "../../hooks/useFormatters"; import { useGlobalItem, useGlobalItemCommunityStats, useGlobalItemPrices, } from "../../hooks/useGlobalItems"; -import { formatPrice, type Currency } from "../../lib/formatters"; +import { type Currency, formatPrice } from "../../lib/formatters"; import { LucideIcon } from "../../lib/iconData"; import { useUIStore } from "../../stores/uiStore"; @@ -263,11 +262,8 @@ function GlobalItemDetail() { ); } -function MarketPricesSection({ - globalItemId, -}: { globalItemId: number }) { +function MarketPricesSection({ globalItemId }: { globalItemId: number }) { const { market: userMarket } = useCurrency(); - const { price } = useFormatters(); const { data: pricesData } = useGlobalItemPrices(globalItemId); const { data: communityStats } = useGlobalItemCommunityStats(globalItemId); const [showOtherMarkets, setShowOtherMarkets] = useState(false); @@ -279,9 +275,7 @@ function MarketPricesSection({ if (marketPrices.length === 0 && stats.length === 0) return null; const userMarketPrice = marketPrices.find((p) => p.market === userMarket); - const otherMarketPrices = marketPrices.filter( - (p) => p.market !== userMarket, - ); + const otherMarketPrices = marketPrices.filter((p) => p.market !== userMarket); const userMarketStats = stats.filter((s) => s.market === userMarket); const otherMarketStats = stats.filter((s) => s.market !== userMarket); @@ -293,7 +287,10 @@ function MarketPricesSection({ {userMarketPrice && (
- {formatPrice(userMarketPrice.priceCents, userMarketPrice.currency as Currency)} + {formatPrice( + userMarketPrice.priceCents, + userMarketPrice.currency as Currency, + )} MSRP ({userMarketPrice.market}) @@ -303,7 +300,10 @@ function MarketPricesSection({ {/* Community stats for user's market */} {userMarketStats.map((stat) => ( -

+

Community ({stat.market}):{" "} {formatPrice(stat.medianPrice, stat.currency as Currency)} median{" "} diff --git a/src/client/routes/settings.tsx b/src/client/routes/settings.tsx index c6d610c..c009c72 100644 --- a/src/client/routes/settings.tsx +++ b/src/client/routes/settings.tsx @@ -51,10 +51,10 @@ function ApiKeySection() { return (

-

{t("apiKeys.title")}

-

- {t("apiKeys.description")} -

+

+ {t("apiKeys.title")} +

+

{t("apiKeys.description")}

{newKey && (
@@ -151,10 +151,10 @@ function ImportExportSection() { return (
-

{t("importExport.title")}

-

- {t("importExport.description")} -

+

+ {t("importExport.title")} +

+

{t("importExport.description")}