Public Access
feat(ui): close 6 P1 audit findings + 1 bonus mobile fix (Sprint 2)
- Dashboard MealCard: title truncate -> line-clamp-2, image shrinks to 40x40 on <md to give the title room (B6). - MealDetail: hero reworked to normal flow with stronger gradient; description runs through new cleanDescription() helper that strips 14 spoonacular SEO patterns and trims to the last full sentence. Raw description moved to a 'Notes from source' disclosure (B7). - Pantry: free-text aisle/unit replaced with <Select> populated from the new PANTRY_AISLES canonical enum; ingredient name field marked required. New PANTRY_AISLES export + PantryAisle type in types (B8). - backend: alembic 0015_normalize_pantry_aisles maps free-text ingredient.aisle and grocery_item.aisle to canonical labels in a single transaction; downgrade raises (restore from snapshot). backend/scripts/dry_run_aisle_migration.sql is the read-only preview helper. - ShoppingList: human-readable AISLE_LABEL map replaces raw snake_case aisle keys; 3-col stat grid with compact mobile sizing (B9 + S3.3). - Pantry table: role/aria-label region and a right-edge white gradient hint at mobile horizontal overflow (B10). - Recipes: pending/applied filter split, Apply and Reset buttons, active-count chip on the Filters button, role=region + aria-label on the panel (B11). - Review/sprint2-verification.md and fix-ui-audit.md updated. Build: npm run build (tsc + vite) green. tsc emits 0 errors. Co-located audit + plan docs kept in sync: Review/ui-nielsen-audit.md gains a Sprint 2 status block; fix-ui-audit.md has implementation notes for each Sprint 2 task.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
const SEO_PATTERNS: RegExp[] = [
|
||||
/\bFeatured In Group[^.!?]*[.!?]?/gi,
|
||||
/\busers? who liked this recipe also liked[^.!?]*[.!?]?/gi,
|
||||
/\bSimilar recipes (include|are)[^.!?]*[.!?]?/gi,
|
||||
/\b\d+ people (found this recipe|made this recipe|have made this recipe)[^.!?]*[.!?]?/gi,
|
||||
/\bOverall,? this recipe earns[^.!?]*[.!?]?/gi,
|
||||
/\b\d+ people have made this recipe and would make it again\.?/gi,
|
||||
/\b\d+ person has tried and liked this recipe\.?/gi,
|
||||
/\bFor \$[\d.]+ per serving,? this recipe covers[^.!?]*[.!?]?/gi,
|
||||
/\bThis recipe serves \d+\.?\s?/gi,
|
||||
/\bIt is brought to you by [^.!?]+[.!?]?/gi,
|
||||
/\bFrom preparation to the plate,? this recipe takes[^.!?]*[.!?]?/gi,
|
||||
/\bIt works well as [^.!?]+[.!?]?/gi,
|
||||
/\bIf you have [^,.]+(,\s*[^,.]+){0,5},? you can make it\.?/gi,
|
||||
/\bOne serving contains [^.!?]+[.!?]?/gi,
|
||||
/\bFor \d+ cents per serving,? this recipe covers[^.!?]*[.!?]?/gi,
|
||||
];
|
||||
|
||||
export function cleanDescription(input: string | undefined | null, maxLen = 280): string {
|
||||
if (!input) return '';
|
||||
let s = input;
|
||||
for (const re of SEO_PATTERNS) s = s.replace(re, '');
|
||||
s = s.replace(/\s{2,}/g, ' ').replace(/\.\s*\./g, '.').trim();
|
||||
if (s.length > maxLen) {
|
||||
const cut = s.slice(0, maxLen);
|
||||
const lastDot = cut.lastIndexOf('.');
|
||||
s = (lastDot > 80 ? cut.slice(0, lastDot + 1) : cut.trimEnd() + '…');
|
||||
}
|
||||
return s;
|
||||
}
|
||||
Reference in New Issue
Block a user