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
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { AlertTriangle, X } from 'lucide-react'
import { useT } from '../i18n/useT'
import styles from './ConfirmDeleteModal.module.css'
interface ConfirmDeleteModalProps {
@@ -20,6 +21,7 @@ export function ConfirmDeleteModal({
onConfirm,
onCancel,
}: ConfirmDeleteModalProps) {
const t = useT()
const [mounted, setMounted] = useState(open)
const [closing, setClosing] = useState(false)
@@ -71,14 +73,14 @@ export function ConfirmDeleteModal({
<div className={styles.actions}>
<button type="button" className={styles.cancelBtn} onClick={onCancel}>
Cancel
{t('common.cancel')}
</button>
<button type="button" className={styles.deleteBtn} onClick={onConfirm}>
Delete
{t('common.delete')}
</button>
</div>
<button className={styles.closeBtn} onClick={onCancel} aria-label="Close">
<button className={styles.closeBtn} onClick={onCancel} aria-label={t('common.close')}>
<X size={18} />
</button>
</div>
@@ -51,6 +51,13 @@
word-break: break-all;
}
.hint {
margin: 6px 0 0;
font-size: 12px;
line-height: 1.4;
color: var(--text-muted);
}
.error {
margin: 0;
font-size: 12px;
@@ -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}
@@ -115,6 +115,24 @@
text-align: left;
}
.categoryChip {
display: inline-block;
align-self: flex-start;
max-width: 100%;
padding: 3px 10px;
font-family: var(--font-ui);
font-size: 11px;
font-weight: 500;
line-height: 1.35;
color: var(--text-secondary);
background: rgba(148, 163, 184, 0.15);
border-radius: 50px;
unicode-bidi: isolate;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.meta {
margin: 0;
font-size: 12px;
@@ -122,6 +140,6 @@
color: var(--text-muted);
}
.metaDot {
margin: 0 6px;
.metaDate {
unicode-bidi: isolate;
}
@@ -94,14 +94,15 @@ export function PortfolioCard({
</p>
)
) : null}
{portfolio.categoryName ? (
<span className={cardStyles.categoryChip} dir="auto">
{portfolio.categoryName}
</span>
) : null}
<p className={cardStyles.meta}>
{portfolio.categoryName ? (
<>
<span>{portfolio.categoryName}</span>
<span className={cardStyles.metaDot}>·</span>
</>
) : null}
<span>{publishDate}</span>
<span className={cardStyles.metaDate} dir="auto">
{publishDate}
</span>
</p>
</div>
</Link>