mirror of
https://github.com/Afilmory/afilmory
synced 2026-04-25 07:15:36 +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
|
author: FeaturedGalleryAuthor | null
|
||||||
photoCount: number
|
photoCount: number
|
||||||
tags: string[]
|
tags: string[]
|
||||||
createdAt: string
|
lastUpload: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FeaturedGalleriesResponse {
|
interface FeaturedGalleriesResponse {
|
||||||
@@ -241,7 +241,7 @@ export const GalleryShowcase = () => {
|
|||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-xs text-white/40">
|
<div className="text-xs text-white/40">
|
||||||
{formatDate(gallery.createdAt)}
|
{t('lastUpload')} {formatDate(gallery.lastUpload)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-white/30 transition group-hover:text-white/60">
|
<div className="text-white/30 transition group-hover:text-white/60">
|
||||||
<i className="i-lucide-external-link size-4" />
|
<i className="i-lucide-external-link size-4" />
|
||||||
|
|||||||
@@ -184,6 +184,7 @@
|
|||||||
"title": "Explore registered visual spaces",
|
"title": "Explore registered visual spaces",
|
||||||
"description": "Discover amazing photography archives created by other photographers and curators, experience different visual storytelling styles.",
|
"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.",
|
"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": "探索已注册的影像空间",
|
"title": "探索已注册的影像空间",
|
||||||
"description": "发现其他摄影师和策展人创建的精彩影像档案馆,感受不同的视觉叙事风格。",
|
"description": "发现其他摄影师和策展人创建的精彩影像档案馆,感受不同的视觉叙事风格。",
|
||||||
"error": "加载画展列表时出错,请稍后重试。",
|
"error": "加载画展列表时出错,请稍后重试。",
|
||||||
"empty": "暂无已注册的画展。"
|
"empty": "暂无已注册的画展。",
|
||||||
|
"lastUpload": "最近上传"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ export class FeaturedGalleriesService {
|
|||||||
const finalTenantIds = validTenants.map((t) => t.id)
|
const finalTenantIds = validTenants.map((t) => t.id)
|
||||||
|
|
||||||
// Step 3: Fetch all related data in parallel
|
// 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
|
// Site settings
|
||||||
db
|
db
|
||||||
.select()
|
.select()
|
||||||
@@ -137,6 +137,17 @@ export class FeaturedGalleriesService {
|
|||||||
})
|
})
|
||||||
.from(tenantDomains)
|
.from(tenantDomains)
|
||||||
.where(and(inArray(tenantDomains.tenantId, finalTenantIds), eq(tenantDomains.status, 'verified'))),
|
.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)
|
// Step 4: Fetch popular tags for top tenants (batch query)
|
||||||
@@ -188,12 +199,17 @@ export class FeaturedGalleriesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const domainMap = new Map<string, string>()
|
const domainMap = new Map<string, string>()
|
||||||
|
const lastUpdatedMap = new Map<string, Date | null>()
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
if (!domainMap.has(domain.tenantId)) {
|
if (!domainMap.has(domain.tenantId)) {
|
||||||
domainMap.set(domain.tenantId, domain.domain)
|
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
|
// Step 6: Build response sorted by quality score
|
||||||
const featuredGalleries = validTenants
|
const featuredGalleries = validTenants
|
||||||
.map((tenant) => {
|
.map((tenant) => {
|
||||||
@@ -217,7 +233,11 @@ export class FeaturedGalleriesService {
|
|||||||
: null,
|
: null,
|
||||||
photoCount: score?.photoCount ?? 0,
|
photoCount: score?.photoCount ?? 0,
|
||||||
tags,
|
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)
|
.filter((gallery) => gallery.photoCount > 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user