diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 8765161..6a9bbd5 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -220,8 +220,7 @@ async function measureScroll(page: Page): Promise { const root = document.querySelector(selector) ?? document.documentElement; const maxScroll = Math.max(0, root.scrollHeight - root.clientHeight); - const scrollY = - root === document.documentElement ? window.scrollY : root.scrollTop; + const scrollY = root === document.documentElement ? window.scrollY : root.scrollTop; const intersectionRatio = await new Promise((resolve) => { const io = new IntersectionObserver( diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 3b87fa3..6cd77f0 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -24,7 +24,11 @@ export function Layout() { -
+
-
- - + patch({ + visibility: visibility as PrFiltersState["visibility"], + }) + } + options={visibilityOptions} + /> +
+ )}
-
-
- {t("pr.visibility")} - - {isPublic ? t("pr.visibilityPublicHint") : t("pr.visibilityPrivateHint")} - + {FEATURES.visibility && ( +
+
+ {t("pr.visibility")} + + {isPublic ? t("pr.visibilityPublicHint") : t("pr.visibilityPrivateHint")} + +
+
- -
+ )} {error &&

{error}

} diff --git a/src/config/features.ts b/src/config/features.ts new file mode 100644 index 0000000..2e98861 --- /dev/null +++ b/src/config/features.ts @@ -0,0 +1,7 @@ +export const FEATURES = { + /** + * Public/private sharing is experimental. + * When disabled, all UI and behavior related to visibility is hidden and writes are forced private. + */ + visibility: import.meta.env.VITE_ENABLE_VISIBILITY === "true", +} as const; diff --git a/src/lib/prFilters.test.ts b/src/lib/prFilters.test.ts index be765d4..ebcb982 100644 --- a/src/lib/prFilters.test.ts +++ b/src/lib/prFilters.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { FEATURES } from "@/config/features"; import { applyPrFilters, countActiveFilters, DEFAULT_PR_FILTERS } from "./prFilters"; import type { PrRecord } from "@/types/pr"; @@ -67,16 +68,14 @@ describe("applyPrFilters", () => { }); it("filters public and with comments", () => { + // Visibility filtering is gated behind a feature flag. + // When disabled, "public/private" behaves like "all". const result = applyPrFilters( records, - { - ...DEFAULT_PR_FILTERS, - visibility: "public", - withCommentsOnly: true, - }, + { ...DEFAULT_PR_FILTERS, visibility: "public", withCommentsOnly: true }, bestIds ); - expect(result).toHaveLength(0); + expect(result).toHaveLength(FEATURES.visibility ? 0 : 1); const withNotes = applyPrFilters( records, diff --git a/src/lib/prFilters.ts b/src/lib/prFilters.ts index c77442a..24b6303 100644 --- a/src/lib/prFilters.ts +++ b/src/lib/prFilters.ts @@ -1,4 +1,5 @@ import { EXERCISE_GROUPS } from "@/lib/statistics"; +import { FEATURES } from "@/config/features"; import type { PrRecord } from "@/types/pr"; export type VisibilityFilter = "all" | "public" | "private"; @@ -23,6 +24,12 @@ export const DEFAULT_PR_FILTERS: PrFiltersState = { withCommentsOnly: false, }; +/** Normalize filters for UI when visibility feature is disabled. */ +export function uiFilters(filters: PrFiltersState): PrFiltersState { + if (FEATURES.visibility) return filters; + return { ...filters, visibility: "all" }; +} + export function hasActiveFilters(filters: PrFiltersState): boolean { return countActiveFilters(filters) > 0; } @@ -34,7 +41,7 @@ export function countActiveFilters(filters: PrFiltersState): number { if (filters.dateFrom !== "") count++; if (filters.dateTo !== "") count++; if (filters.onlyHighest) count++; - if (filters.visibility !== "all") count++; + if (FEATURES.visibility && filters.visibility !== "all") count++; if (filters.withCommentsOnly) count++; return count; } @@ -57,8 +64,10 @@ export function applyPrFilters( if (filters.dateFrom && record.date < filters.dateFrom) return false; if (filters.dateTo && record.date > filters.dateTo) return false; if (filters.onlyHighest && !bestPrIds.has(record.id)) return false; - if (filters.visibility === "public" && !record.isPublic) return false; - if (filters.visibility === "private" && record.isPublic) return false; + if (FEATURES.visibility) { + if (filters.visibility === "public" && !record.isPublic) return false; + if (filters.visibility === "private" && record.isPublic) return false; + } if (filters.withCommentsOnly && !record.notes?.trim()) return false; return true; }); diff --git a/src/pages/Statistics/StatisticsPage.tsx b/src/pages/Statistics/StatisticsPage.tsx index 9b62cba..844203e 100644 --- a/src/pages/Statistics/StatisticsPage.tsx +++ b/src/pages/Statistics/StatisticsPage.tsx @@ -126,7 +126,11 @@ export function StatisticsPage() { {groupStats.map((g) => { const best = g.best; return ( -
  • +
  • {t(g.labelKey)} diff --git a/src/services/prService.ts b/src/services/prService.ts index f1ce1d0..a31f3b2 100644 --- a/src/services/prService.ts +++ b/src/services/prService.ts @@ -10,6 +10,7 @@ import { updateDoc, type Unsubscribe, } from "firebase/firestore"; +import { FEATURES } from "@/config/features"; import { db } from "@/lib/firebase"; import { validatePrInput, type ValidationErrorCode } from "@/lib/validation"; import type { PrInput, PrRecord } from "@/types/pr"; @@ -25,7 +26,8 @@ export class PrValidationError extends Error { } function assertValidInput(input: PrInput): PrInput { - const result = validatePrInput(input); + const gated: PrInput = FEATURES.visibility ? input : { ...input, isPublic: false }; + const result = validatePrInput(gated); if (!result.ok) throw new PrValidationError(result.code); return result.value; } diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index bcf8228..a7ac783 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -5,6 +5,7 @@ declare const __APP_VERSION__: string; interface ImportMetaEnv { readonly VITE_E2E_MOCK?: string; + readonly VITE_ENABLE_VISIBILITY?: string; readonly VITE_FIREBASE_API_KEY: string; readonly VITE_FIREBASE_AUTH_DOMAIN: string; readonly VITE_FIREBASE_PROJECT_ID: string;