Fix portfolio list sorting UX and add name/category filters.

Newest portfolios show first with corrected move-up, category no longer mixes into the date, and website link generation can filter by tags.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-23 15:01:53 +03:30
co-authored by Cursor
parent 2477936bce
commit 1e9f6481b0
8 changed files with 324 additions and 49 deletions
@@ -13,14 +13,17 @@ import type { ProductCategory } from '../types/category'
import {
WEBSITE_LINK_KINDS,
buildWebsiteCategoryUrl,
buildWebsiteTagsUrl,
type WebsiteLinkFilter,
type WebsiteLinkKind,
} from '../utils/websiteLinks'
import { CategoryTreeSelect } from './CategoryTreeSelect'
import { TagInput } from './TagInput'
import modalStyles from './CategoryModal.module.css'
import styles from './GenerateWebsiteLinkModal.module.css'
export type { WebsiteLinkKind }
export { buildWebsiteCategoryUrl }
export { buildWebsiteCategoryUrl, buildWebsiteTagsUrl }
const KIND_LABEL: Record<WebsiteLinkKind, BusinessMessageKey> = {
product: 'website.linkGenerator.types.product',
@@ -91,8 +94,10 @@ export function GenerateWebsiteLinkModal({ open, onClose, onPick }: GenerateWebs
const [mounted, setMounted] = useState(open)
const [closing, setClosing] = useState(false)
const [kind, setKind] = useState<WebsiteLinkKind>('product')
const [filterBy, setFilterBy] = useState<WebsiteLinkFilter>('category')
const [categories, setCategories] = useState<ProductCategory[]>([])
const [categoryId, setCategoryId] = useState('')
const [tags, setTags] = useState<string[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
@@ -101,8 +106,10 @@ export function GenerateWebsiteLinkModal({ open, onClose, onPick }: GenerateWebs
setMounted(true)
setClosing(false)
setKind('product')
setFilterBy('category')
setCategories([])
setCategoryId('')
setTags([])
setError('')
} else if (mounted) {
setClosing(true)
@@ -127,7 +134,7 @@ export function GenerateWebsiteLinkModal({ open, onClose, onPick }: GenerateWebs
}, [mounted, closing, onClose])
useEffect(() => {
if (!open) return
if (!open || filterBy !== 'category') return
const controller = new AbortController()
setLoading(true)
@@ -152,7 +159,7 @@ export function GenerateWebsiteLinkModal({ open, onClose, onPick }: GenerateWebs
})
return () => controller.abort()
}, [open, kind, t])
}, [open, kind, filterBy, t])
const uiCategories = useMemo(
() => categories.map(mapProductCategoryToUi),
@@ -160,7 +167,12 @@ export function GenerateWebsiteLinkModal({ open, onClose, onPick }: GenerateWebs
)
const selected = categories.find((category) => category.id === categoryId)
const preview = selected ? buildWebsiteCategoryUrl(kind, selected) : ''
const preview =
filterBy === 'category'
? selected
? buildWebsiteCategoryUrl(kind, selected)
: ''
: buildWebsiteTagsUrl(kind, tags)
if (!mounted) return null
@@ -171,6 +183,16 @@ export function GenerateWebsiteLinkModal({ open, onClose, onPick }: GenerateWebs
onPick(preview)
}
function handleFilterChange(next: WebsiteLinkFilter) {
setFilterBy(next)
setError('')
if (next === 'tag') {
setCategoryId('')
} else {
setTags([])
}
}
return createPortal(
<div
className={`${modalStyles.overlay} ${modalStyles.overlayStacked} ${closing ? modalStyles.overlayOut : modalStyles.overlayIn}`}
@@ -222,23 +244,47 @@ export function GenerateWebsiteLinkModal({ open, onClose, onPick }: GenerateWebs
</select>
</div>
<div className={`${modalStyles.field} ${styles.treeField}`}>
<label htmlFor="generate-link-category">{t('website.linkGenerator.category')}</label>
{loading ? (
<p className={styles.status}>{t('website.linkGenerator.loading')}</p>
) : uiCategories.length === 0 ? (
<p className={styles.status}>{t('website.linkGenerator.empty')}</p>
) : (
<CategoryTreeSelect
id="generate-link-category"
categories={uiCategories}
value={categoryId}
onChange={setCategoryId}
placeholder={t('website.linkGenerator.pickCategory')}
/>
)}
<div className={modalStyles.field}>
<label htmlFor="generate-link-filter">{t('website.linkGenerator.filterBy')}</label>
<select
id="generate-link-filter"
value={filterBy}
onChange={(e) => handleFilterChange(e.target.value as WebsiteLinkFilter)}
>
<option value="category">{t('website.linkGenerator.category')}</option>
<option value="tag">{t('website.linkGenerator.tag')}</option>
</select>
</div>
{filterBy === 'category' ? (
<div className={`${modalStyles.field} ${styles.treeField}`}>
<label htmlFor="generate-link-category">{t('website.linkGenerator.category')}</label>
{loading ? (
<p className={styles.status}>{t('website.linkGenerator.loading')}</p>
) : uiCategories.length === 0 ? (
<p className={styles.status}>{t('website.linkGenerator.empty')}</p>
) : (
<CategoryTreeSelect
id="generate-link-category"
categories={uiCategories}
value={categoryId}
onChange={setCategoryId}
placeholder={t('website.linkGenerator.pickCategory')}
/>
)}
</div>
) : (
<div className={modalStyles.field}>
<label htmlFor="generate-link-tags">{t('website.linkGenerator.tags')}</label>
<TagInput
tags={tags}
onChange={setTags}
placeholder={t('website.linkGenerator.tagsPlaceholder')}
/>
<p className={styles.hint}>{t('website.linkGenerator.tagsHint')}</p>
</div>
)}
{preview ? (
<p className={styles.preview} dir="ltr">
{preview}