mirror of
https://github.com/runyanjake/cooking.git
synced 2026-11-13 00:24:51 -08:00
Merge pull request #8 from runyanjake/agent/swe-worker/4e283abf1548
PWS-15: Capitalize tags
This commit is contained in:
commit
50c6fcad36
@ -50,6 +50,7 @@ lib/
|
||||
recipe-content.test.ts # Format tests; also parses every real recipe (npm test)
|
||||
recipe-card.tsx # Card layout (measured with satori) and PNG rendering
|
||||
recipe-urls.ts # Client-safe helper that maps ./assets/ paths to served URLs
|
||||
format.ts # Client-safe display formatting (capitalize categories/tags)
|
||||
|
||||
recipes/ # ALL recipe content lives here (markdown + images together)
|
||||
[category]/
|
||||
@ -69,5 +70,5 @@ public/
|
||||
- **Content first**: recipe pages are minimal — no sidebar, just the recipe
|
||||
- **Server components by default**: only add `'use client'` when interactivity is needed
|
||||
- **The markdown is the recipe**: tabs, their titles and order, and what the card prints are all written in the `.md` file. The renderer only turns what's written into the page; never add hardcoded tab names, ordering, or fallbacks
|
||||
- **No taxonomy file**: categories and tags are derived directly from frontmatter across all recipes — no external registry to keep in sync
|
||||
- **No taxonomy file**: categories and tags are derived directly from frontmatter across all recipes — no external registry to keep in sync. They stay lowercase in frontmatter and URLs; display them through `capitalize` from `lib/format.ts`
|
||||
- **Content is top level**: markdown and images are colocated in `recipes/`; images are served by a statically prerendered route handler, so there's no copy step. `public/` is only for site branding and data
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import type { FilterState } from '@/lib/types';
|
||||
import { capitalize } from '@/lib/format';
|
||||
|
||||
interface ActiveFiltersProps {
|
||||
filters: FilterState;
|
||||
@ -19,11 +20,11 @@ export default function ActiveFilters({ filters, onFilterChange }: ActiveFilters
|
||||
? [{ key: 'search', label: `“${filters.search}”`, remove: { ...filters, search: '' } }]
|
||||
: []),
|
||||
...(filters.category
|
||||
? [{ key: 'category', label: filters.category.charAt(0).toUpperCase() + filters.category.slice(1), remove: { ...filters, category: '' } }]
|
||||
? [{ key: 'category', label: capitalize(filters.category), remove: { ...filters, category: '' } }]
|
||||
: []),
|
||||
...filters.selectedTags.map((tag) => ({
|
||||
key: `tag-${tag}`,
|
||||
label: tag,
|
||||
label: capitalize(tag),
|
||||
remove: { ...filters, selectedTags: filters.selectedTags.filter((t) => t !== tag) },
|
||||
})),
|
||||
];
|
||||
|
||||
@ -2,6 +2,7 @@ import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import type { RecipeMetadata } from '@/lib/recipes';
|
||||
import { resolveRecipeAssetUrl } from '@/lib/recipe-urls';
|
||||
import { capitalize } from '@/lib/format';
|
||||
|
||||
interface RecipeCardProps {
|
||||
recipe: RecipeMetadata;
|
||||
@ -57,7 +58,7 @@ export default function RecipeGridCard({ recipe }: RecipeCardProps) {
|
||||
role="listitem"
|
||||
className="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded"
|
||||
>
|
||||
{tag}
|
||||
{capitalize(tag)}
|
||||
</span>
|
||||
))}
|
||||
{recipe.tags.length > 3 && (
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import Link from 'next/link';
|
||||
import type { Recipe } from '@/lib/recipes';
|
||||
import { capitalize } from '@/lib/format';
|
||||
|
||||
interface RecipePageLayoutProps {
|
||||
recipe: Recipe;
|
||||
@ -83,7 +84,7 @@ export default function RecipePageLayout({ recipe, children }: RecipePageLayoutP
|
||||
role="listitem"
|
||||
className="px-3 py-1 text-sm bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
{tag}
|
||||
{capitalize(tag)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import FacetGroup from './FacetGroup';
|
||||
import type { FacetCounts, FilterState } from '@/lib/types';
|
||||
import { capitalize } from '@/lib/format';
|
||||
|
||||
interface RecipesSidebarProps {
|
||||
categories: string[];
|
||||
@ -15,10 +16,6 @@ interface RecipesSidebarProps {
|
||||
const COLLAPSED_CATEGORY_COUNT = 8;
|
||||
const COLLAPSED_TAG_COUNT = 10;
|
||||
|
||||
function capitalize(value: string): string {
|
||||
return value.charAt(0).toUpperCase() + value.slice(1);
|
||||
}
|
||||
|
||||
function ShowMoreButton({ expanded, hiddenCount, onClick }: { expanded: boolean; hiddenCount: number; onClick: () => void }) {
|
||||
return (
|
||||
<button
|
||||
@ -199,7 +196,7 @@ export default function RecipesSidebar({ categories, tags, facetCounts, filters,
|
||||
onChange={() => handleTagToggle(tag)}
|
||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 accent-blue-600"
|
||||
/>
|
||||
<span className="flex-1 truncate">{tag}</span>
|
||||
<span className="flex-1 truncate">{capitalize(tag)}</span>
|
||||
<span className="text-xs tabular-nums text-gray-500 dark:text-gray-400">{count}</span>
|
||||
</label>
|
||||
);
|
||||
|
||||
10
lib/format.test.ts
Normal file
10
lib/format.test.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { test } from 'node:test';
|
||||
import { capitalize } from './format';
|
||||
|
||||
test('capitalize uppercases the first letter only', () => {
|
||||
assert.equal(capitalize('italian'), 'Italian');
|
||||
assert.equal(capitalize('ice cream'), 'Ice cream');
|
||||
assert.equal(capitalize('BBQ'), 'BBQ');
|
||||
assert.equal(capitalize(''), '');
|
||||
});
|
||||
4
lib/format.ts
Normal file
4
lib/format.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// Display formatting for frontmatter values (categories, tags). Client-safe.
|
||||
export function capitalize(value: string): string {
|
||||
return value.charAt(0).toUpperCase() + value.slice(1);
|
||||
}
|
||||
@ -7,7 +7,7 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "node --import tsx --test lib/recipe-content.test.ts"
|
||||
"test": "node --import tsx --test lib/*.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.3.0",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user