mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
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 <tukon479@gmail.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.<action>() -> 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.<method>()` 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.
|
||||
@@ -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.
|
||||
@@ -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 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs text-white/40">
|
||||
{formatDate(gallery.createdAt)}
|
||||
{t('lastUpload')} {formatDate(gallery.lastUpload)}
|
||||
</div>
|
||||
<div className="text-white/30 transition group-hover:text-white/60">
|
||||
<i className="i-lucide-external-link size-4" />
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,6 +184,7 @@
|
||||
"title": "探索已注册的影像空间",
|
||||
"description": "发现其他摄影师和策展人创建的精彩影像档案馆,感受不同的视觉叙事风格。",
|
||||
"error": "加载画展列表时出错,请稍后重试。",
|
||||
"empty": "暂无已注册的画展。"
|
||||
"empty": "暂无已注册的画展。",
|
||||
"lastUpload": "最近上传"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Date>`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<string, string>()
|
||||
const lastUpdatedMap = new Map<string, Date | null>()
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user