From 7d70361717a393628e42d503276ce1cd5c4b2b5a Mon Sep 17 00:00:00 2001 From: Innei Date: Sat, 6 Dec 2025 22:20:03 +0800 Subject: [PATCH] refactor: remove outdated documentation and update gallery showcase with last upload feature - Deleted obsolete documentation files related to color system, discover modal architecture, and store-actions pattern. - Updated the GalleryShowcase component to replace 'createdAt' with 'lastUpload' for better clarity on photo upload timing. - Enhanced localization files to include a new key for 'lastUpload' in both English and Chinese. - Modified the FeaturedGalleriesService to fetch and include the last updated timestamp for galleries. Signed-off-by: Innei --- apps/landing/ai-docs/color-system.md | 92 ------------------- .../ai-docs/discover-modal-architecture.md | 70 -------------- apps/landing/ai-docs/store-actions-pattern.md | 46 ---------- .../components/landing/GalleryShowcase.tsx | 4 +- apps/landing/src/locales/en.json | 5 +- apps/landing/src/locales/zh-CN.json | 5 +- .../featured-galleries.service.ts | 24 ++++- 7 files changed, 30 insertions(+), 216 deletions(-) delete mode 100644 apps/landing/ai-docs/color-system.md delete mode 100644 apps/landing/ai-docs/discover-modal-architecture.md delete mode 100644 apps/landing/ai-docs/store-actions-pattern.md diff --git a/apps/landing/ai-docs/color-system.md b/apps/landing/ai-docs/color-system.md deleted file mode 100644 index c3ca0d67..00000000 --- a/apps/landing/ai-docs/color-system.md +++ /dev/null @@ -1,92 +0,0 @@ -# Pastel Colors - -> A color library and sample site for design systems, offering three styles: Regular, Kawaii, and High Contrast. It covers application-level colors (accent/primary/secondary), semantic colors (text, background, border, separator, link, disabled), material transparency layers (ultraThick→opaque), and grayscale gradients (gray1→gray10). All colors are defined using OKLCH and offer light/dark variants to maintain consistent perceptual contrast and accessibility across themes and light/dark modes. - -This project includes a color package (`packages/colors`) and a demo/documentation site (`docs`). Colors are created using the `createColor` tool to ensure consistency and composability. Styles: -- Regular: Universal, neutral default style suitable for most products -- Kawaii: Softer, cute-style light-color contrast controls -- High Contrast: Extreme contrast for enhanced readability and accessibility (WCAG-friendly) - -Color Organization Structure and Recommendations: -- Application Colors: `accent`, `primary`, `secondary` (for branding, primary actions, secondary buttons, etc.) -- Semantic Colors: `text`, `placeholderText`, `border`, `separator`, `link`, `disabledControl`, `disabledText`; plus `background` and `fill` with progressive hierarchy (primary→quinary / primary→quaternary) -- Material Colors: `ultraThick`, `thick`, `medium`, `thin`, `ultraThin`, `opaque` (same color with varying opacity levels, used for frosted glass, card overlays, etc.) -- GrayScale: `gray1`→`gray10` (progresses and inverts under light/dark modes, supporting neutral interfaces and layering) -- Prioritize conveying intent through Application and Semantic categories before mapping specific hues; maintain semantic naming across light/dark modes, with variants automatically adjusting contrast. - -- Theme Overview: - - Regular: Default universal contrast with modern neutral palette, emphasizing balanced text/background gradation and soft colors. - - Kawaii: Soft pastel hues (pink, blue, purple, cyan, etc.) with low saturation; refined text/borders suited for cute/warm-toned interfaces. - - High Contrast: Enhanced text/background contrast, improved link/interaction visibility; used for accessibility-first or noisy environments. - -- Application (Purpose): - - accent: Brand accents, emphasis elements (logos, highlighted button borders/hover states, selected indicators) - - primary: Primary actions/visuals (main buttons, key links, progress/highlighting) - - secondary: Secondary actions/less prominent emphasis (secondary buttons, informational labels, auxiliary chart colors) - -- Semantic (Purpose): - - text - - primary: Body/heading main text - - secondary: Secondary descriptions, de-emphasized supplementary information - - tertiary: Placeholder or minor-level labels/meta information - - quaternary: Disabled state or extremely low-priority text - - placeholderText - - primary: Input field placeholder text - - border - - primary: Primary border for cards/inputs/popups - - secondary: Weak grouping/block separator border - - separator - - primary: List item/module separator - - link - - primary: Text links, clickable text - - disabledControl - - primary: Background/border for disabled controls - - disabledText - - primary: Disabled text color - - background (primary→quinary): Progressive background gradient for pages/cards/overlays/inner containers - - fill (primary→quaternary): Progressive fill gradient for icons/controls - - material (ultraThick→opaque): Opacity levels for frosted glass/overlay/card materials - -- Gray Scale (Purpose): - - gray1→gray10: Light-to-dark and dark-to-light grayscale gradients for background layering, strokes, and text softening; recommended with `separator` and `border` for stable hierarchy. - -- Hue Usage (Cross-Theme Recommendations): - - blue / indigo: Primary actions, links, interactive elements—emphasizing reliability and technological feel - - cyan / sky / teal: Information prompts, secondary highlights—emphasizing clarity and freshness - - green / emerald / lime: Success, approval, positive outcomes - - red / rose: Errors, danger, destructive actions - - orange / amber / yellow: Warnings, caution, in-progress states - - purple / violet / pink: Brand identity, creative/joyful tone, decorative emphasis - - brown: Neutral labels/backgrounds, secondary information emphasis - - slate / zinc / gray / neutral: Neutral text and surfaces, outlines, separators - - white / black: Baseline for very light/very dark surfaces and text - -- Regular Style: - - Primary Colors: blue, pink, purple, green, orange, yellow, sky, red, brown, gray, neutral, black, white, teal, cyan, indigo, violet, lime, emerald, amber, rose, slate, zinc - - Application Level (regularApplicationColors): accent (brand accents), primary (primary interactions), secondary (secondary interaction) - - Semantic Level (regularElementColors / regularBackgroundColors / regularFillColors / regularMaterialColors): - - text.*, border.*, separator.primary, link.primary, disabled*, background.* (primary→quinary), fill.* (primary→quaternary), material.* (ultraThick→opaque) -- Grayscale (regularGrayScale): gray1→gray10 supporting neutral surfaces and contrast levels - -- Kawaii Style: -- Distinctive features: More refined text/border contrast control on light color palettes; softer background/fill gradations - - Application Level (kawaiiApplicationColors): accent/primary/secondary follow Regular roles, with softer hue and luminance - - Semantic Level (kawaiiElementColors, etc.): links lean toward soft blue/cyan; disabled* aligns with overall color temperature - - Grayscale (kawaiiGrayScale): consistent gradation with Regular, adapted to overall style - -- High Contrast Style: - - Distinctive features: text is darker (light)/lighter (dark); links have higher saturation; background/fill spans a wider range; material elements exhibit stronger contrast - - Application Level (highContrastApplicationColors): accent/primary/secondary colors presented with higher contrast - - Semantic Level (highContrastElementColors, etc.): All semantic layers optimized under high-contrast constraints - - Grayscale (highContrastGrayScale): Same gradient as Regular but with overall stronger contrast - -## Docs - -- [Demo Documentation Site](https://pastel.innei.dev/): Interactive preview of all themes and colors - -## Optional - -- [packages/colors source code (theme definitions)](https://github.com/Innei/pastel/tree/main/packages/colors/src): Theme and color definitions, including Regular/Kawaii/High Contrast -- [docs site source code](https://github.com/Innei/pastel/tree/main/docs): Example and preview site -- [Tailwind Theme Export (packages/tailwindcss-colors)](https://github.com/Innei/pastel/tree/main/packages/tailwindcss-colors): Generator and theme CSS export - diff --git a/apps/landing/ai-docs/discover-modal-architecture.md b/apps/landing/ai-docs/discover-modal-architecture.md deleted file mode 100644 index 1e979cd1..00000000 --- a/apps/landing/ai-docs/discover-modal-architecture.md +++ /dev/null @@ -1,70 +0,0 @@ -# Discover Modal Architecture - -This module follows the shared [Store + Actions pattern](./store-actions-pattern.md) so the data flow stays predictable and easy to reason about. The notes below capture Discover-specific nuances; refer to the general guide for broader expectations. - -## High-level flow - -``` -UI component -> DiscoverModalActions.() -> DiscoverService/API -> store.ts state update -> UI selector re-render -``` - -- `store.ts` exposes a Zustand store that only holds raw state (provider metadata, form inputs, query results, selection, preview, importing flags). It **must not** contain business logic, async calls, or toast usage. -- `actions/index.ts` exports the `DiscoverModalActions` singleton. Feature-specific logic lives in `actions/slices/*`, while `actions/context.ts` manages shared helpers (tokens, persistence, helpers for history). Slices pull snapshots via `discoverModalStore.getState()`, call `DiscoverService` (or other modules like `TorrentActions`), then push state updates with `discoverModalStore.setState()`. Every async action should safeguard against race conditions with request tokens when applicable. -- UI components subscribe with selectors (`useDiscoverModalStore(state => state.xxx)`) for fine-grained updates and call `DiscoverModalActions.shared.()` on user events. - -## Store responsibilities - -`DiscoverModalState` should include only primitive state values: - -- Provider context: `activeProviderId`, `providerReady`, `pageSize`, filter definitions, default filters. -- Form + search state: `keyword`, `filters`, `committedSearch`, `items`, `total`, `hasMore`, `isSearching`, `searchError`. -- Selection & preview: `selectedIds`, `previewId`, `previewDetail`, `isPreviewLoading`, `previewError`. -- Import flag: `importing`. - -Setter helper methods (e.g. `setKeyword`) live outside the store, inside actions. The store should only expose `getState()`/`setState()` via the `discoverModalStore` helper. - -## Actions responsibilities - -Each action method should: - -1. Grab a snapshot (`discoverModalStore.getState()`). -2. Early-return with a typed `ActionResult` when a precondition fails (e.g. provider not ready). -3. Use `discoverModalStore.setState()` to apply optimistic updates before a request. -4. Call `DiscoverService`/`TorrentActions` and handle the response. -5. Guard against outdated promises with `this.searchToken`/`this.previewToken` style counters when fetching search or preview data. -6. Return `{ ok: boolean, error?: string }` so the caller decides how to render feedback (toast, UI message, etc.). - -Do **not** emit toast notifications inside actions; keep user-facing feedback in the UI layer. - -### Common actions - -- `configureProvider` resets the store when the provider changes and drops in default filters. -- `performSearch` and `goToPage` share the `fetchPage` helper and manage search concurrency tokens. -- `toggleSelection` and `selectAll` adjust the selection set and delegate preview management to `setPreview`. -- `setPreview` triggers preview fetching and handles stale responses with `previewToken`. -- `importSelected`/`importPreview` wrap download URL resolution and torrent submission. - -## UI guidelines - -- Always subscribe with selectors to avoid unnecessary re-renders (`useDiscoverModalStore(state => state.items)` instead of destructuring the full store). -- All user interactions should call an action; never mutate store state directly from a component. -- When actions return `error` codes, map them to localized messages in the component (e.g. `providerNotReady`, `requestFailed`, `selectionEmpty`). -- Use the existing i18n keys: `discover.messages.searchFailed`, `discover.messages.previewFailed`, `discover.messages.importFailed`, `discover.messages.importSuccess`, `discover.messages.providerNotReady`. - -## Extending the modal - -When adding new provider-specific filters or data fields: - -1. Update the provider implementation and its `getFilterDefinitions` method. -2. Ensure `configureProvider` receives the new defaults. -3. Extend the store state/interface if you need additional persisted values. -4. Add actions or extend existing ones for the new behavior. -5. Update UI components to read from selectors and surface the new data. - -## Testing checklist - -- Run `pnpm typecheck:turbo` after any change. -- Manually verify: provider switching, initial filter defaults, searching, pagination, selection, preview loading, single/bulk import. -- Confirm optimistic state transitions (e.g. clearing selection after import) behave as expected without relying on a toast to hide issues. - -Following this pattern keeps the Discover modal consistent with the rest of the app and makes it easier to share logic with automation agents. diff --git a/apps/landing/ai-docs/store-actions-pattern.md b/apps/landing/ai-docs/store-actions-pattern.md deleted file mode 100644 index d35e175a..00000000 --- a/apps/landing/ai-docs/store-actions-pattern.md +++ /dev/null @@ -1,46 +0,0 @@ -# Store + Actions Pattern - -Use this pattern for any complex UI module (modals, dashboards, wizards, etc.) that needs shared state, async side-effects, and reusable logic. The Discover modal is the reference implementation—see `docs/discover-modal-architecture.md` for a concrete example. - -## Principles - -1. **Single source of truth** — A Zustand store (`store.ts`) holds only serializable state. It defines the initial shape and exposes `getState`/`setState`. Do not put derived data, async calls, or business rules into the store definition. -2. **Global singleton actions** — A `*Actions` class (e.g. `DiscoverModalActions`) encapsulates every side-effect: service calls, optimistic updates, selection logic, and concurrency guards. Export a single instance (usually `shared`) so the same logic works from UI, background tasks, or agents. -3. **Selectors in UI** — Components subscribe with selectors (`useStore(state => state.slice)`) for fine-grained re-renders, then invoke actions directly on user events. Components never mutate the store themselves. -4. **Result-based feedback** — Actions return `{ ok, data?, error? }`. UI surfaces messages (toast, inline banner) based on the `error` code, keeping rendering concerns out of the actions. - -## Implementation checklist - -- `store.ts` - - Export `useXStore` via `createWithEqualityFn + subscribeWithSelector + immer`. - - Provide a helper (`xStore`) exposing `getState` / `setState` / `reset` for actions to use. - - Store only raw values: inputs, fetched results, flags. Derive display values in selectors or components. - -- `actions/` - - `index.ts` stitches together feature-specific slices and exposes the singleton. - - `slices/*` group related side-effects (provider, form, search, history, selection, preview, importing). - - Shared helpers live in `actions/context.ts` (tokens, persistence) and `actions/utils.ts` (pure utilities). - - Use request tokens when loading paginated or preview data to avoid stale updates. - - Keep user-facing strings out of slices; return symbolic `error` keys instead. - - Expose imperative helpers (`configure`, `search`, `goToPage`, `toggleSelection`, `importSelected`, etc.). - -- UI components - - Select minimal slices from the store. - - Call actions inside handlers (`onClick={() => DiscoverModalActions.shared.search()}`). - - Map returned `error` keys to localized messages. - - Avoid prop drilling; each component subscribes to the pieces it needs. - -## When to apply - -Adopt this pattern whenever a feature needs: - -- Shared state across multiple components. -- Async workflows (search, preview, import, etc.). -- Logic reuse outside React (automation, background tasks). -- Deterministic control over optimistic updates and race conditions. - -For simple components, local `useState` is fine. Once the feature spans multiple files or requires cross-component coordination, migrate to this pattern. - -## Further reading - -- `docs/discover-modal-architecture.md` — Full reference implementation with search, pagination, preview, and import flows. diff --git a/apps/landing/src/components/landing/GalleryShowcase.tsx b/apps/landing/src/components/landing/GalleryShowcase.tsx index 3eb4285e..223f033f 100644 --- a/apps/landing/src/components/landing/GalleryShowcase.tsx +++ b/apps/landing/src/components/landing/GalleryShowcase.tsx @@ -21,7 +21,7 @@ interface FeaturedGallery { author: FeaturedGalleryAuthor | null photoCount: number tags: string[] - createdAt: string + lastUpload: string } interface FeaturedGalleriesResponse { @@ -241,7 +241,7 @@ export const GalleryShowcase = () => { {/* Footer */}
- {formatDate(gallery.createdAt)} + {t('lastUpload')} {formatDate(gallery.lastUpload)}
diff --git a/apps/landing/src/locales/en.json b/apps/landing/src/locales/en.json index d965d4bd..1a0aced8 100644 --- a/apps/landing/src/locales/en.json +++ b/apps/landing/src/locales/en.json @@ -184,6 +184,7 @@ "title": "Explore registered visual spaces", "description": "Discover amazing photography archives created by other photographers and curators, experience different visual storytelling styles.", "error": "Failed to load gallery list, please try again later.", - "empty": "No registered galleries yet." + "empty": "No registered galleries yet.", + "lastUpload": "Last upload" } -} +} \ No newline at end of file diff --git a/apps/landing/src/locales/zh-CN.json b/apps/landing/src/locales/zh-CN.json index 2ad32647..13a8273c 100644 --- a/apps/landing/src/locales/zh-CN.json +++ b/apps/landing/src/locales/zh-CN.json @@ -184,6 +184,7 @@ "title": "探索已注册的影像空间", "description": "发现其他摄影师和策展人创建的精彩影像档案馆,感受不同的视觉叙事风格。", "error": "加载画展列表时出错,请稍后重试。", - "empty": "暂无已注册的画展。" + "empty": "暂无已注册的画展。", + "lastUpload": "最近上传" } -} +} \ No newline at end of file diff --git a/be/apps/core/src/modules/platform/featured-galleries/featured-galleries.service.ts b/be/apps/core/src/modules/platform/featured-galleries/featured-galleries.service.ts index 38ddb30c..150987cc 100644 --- a/be/apps/core/src/modules/platform/featured-galleries/featured-galleries.service.ts +++ b/be/apps/core/src/modules/platform/featured-galleries/featured-galleries.service.ts @@ -108,7 +108,7 @@ export class FeaturedGalleriesService { const finalTenantIds = validTenants.map((t) => t.id) // Step 3: Fetch all related data in parallel - const [siteSettings, authors, domains] = await Promise.all([ + const [siteSettings, authors, domains, lastUpdatedRows] = await Promise.all([ // Site settings db .select() @@ -137,6 +137,17 @@ export class FeaturedGalleriesService { }) .from(tenantDomains) .where(and(inArray(tenantDomains.tenantId, finalTenantIds), eq(tenantDomains.status, 'verified'))), + // Last photo library update time per tenant + db + .select({ + tenantId: photoAssets.tenantId, + lastUpdatedAt: sql`max(${photoAssets.updatedAt})`, + }) + .from(photoAssets) + .where( + and(inArray(photoAssets.tenantId, finalTenantIds), inArray(photoAssets.syncStatus, ['synced', 'conflict'])), + ) + .groupBy(photoAssets.tenantId), ]) // Step 4: Fetch popular tags for top tenants (batch query) @@ -188,12 +199,17 @@ export class FeaturedGalleriesService { } const domainMap = new Map() + const lastUpdatedMap = new Map() for (const domain of domains) { if (!domainMap.has(domain.tenantId)) { domainMap.set(domain.tenantId, domain.domain) } } + for (const row of lastUpdatedRows) { + lastUpdatedMap.set(row.tenantId, row.lastUpdatedAt) + } + // Step 6: Build response sorted by quality score const featuredGalleries = validTenants .map((tenant) => { @@ -217,7 +233,11 @@ export class FeaturedGalleriesService { : null, photoCount: score?.photoCount ?? 0, tags, - createdAt: normalizeDate(tenant.createdAt) ?? tenant.createdAt, + createdAt: normalizeDate(tenant.createdAt), + lastUpload: + normalizeDate(lastUpdatedMap.get(tenant.id) ?? undefined) ?? + lastUpdatedMap.get(tenant.id) ?? + normalizeDate(tenant.createdAt), } }) .filter((gallery) => gallery.photoCount > 0)