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-content.test.ts # Format tests; also parses every real recipe (npm test)
|
||||||
recipe-card.tsx # Card layout (measured with satori) and PNG rendering
|
recipe-card.tsx # Card layout (measured with satori) and PNG rendering
|
||||||
recipe-urls.ts # Client-safe helper that maps ./assets/ paths to served URLs
|
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)
|
recipes/ # ALL recipe content lives here (markdown + images together)
|
||||||
[category]/
|
[category]/
|
||||||
@ -69,5 +70,5 @@ public/
|
|||||||
- **Content first**: recipe pages are minimal — no sidebar, just the recipe
|
- **Content first**: recipe pages are minimal — no sidebar, just the recipe
|
||||||
- **Server components by default**: only add `'use client'` when interactivity is needed
|
- **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
|
- **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
|
- **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';
|
'use client';
|
||||||
|
|
||||||
import type { FilterState } from '@/lib/types';
|
import type { FilterState } from '@/lib/types';
|
||||||
|
import { capitalize } from '@/lib/format';
|
||||||
|
|
||||||
interface ActiveFiltersProps {
|
interface ActiveFiltersProps {
|
||||||
filters: FilterState;
|
filters: FilterState;
|
||||||
@ -19,11 +20,11 @@ export default function ActiveFilters({ filters, onFilterChange }: ActiveFilters
|
|||||||
? [{ key: 'search', label: `“${filters.search}”`, remove: { ...filters, search: '' } }]
|
? [{ key: 'search', label: `“${filters.search}”`, remove: { ...filters, search: '' } }]
|
||||||
: []),
|
: []),
|
||||||
...(filters.category
|
...(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) => ({
|
...filters.selectedTags.map((tag) => ({
|
||||||
key: `tag-${tag}`,
|
key: `tag-${tag}`,
|
||||||
label: tag,
|
label: capitalize(tag),
|
||||||
remove: { ...filters, selectedTags: filters.selectedTags.filter((t) => t !== 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 Image from 'next/image';
|
||||||
import type { RecipeMetadata } from '@/lib/recipes';
|
import type { RecipeMetadata } from '@/lib/recipes';
|
||||||
import { resolveRecipeAssetUrl } from '@/lib/recipe-urls';
|
import { resolveRecipeAssetUrl } from '@/lib/recipe-urls';
|
||||||
|
import { capitalize } from '@/lib/format';
|
||||||
|
|
||||||
interface RecipeCardProps {
|
interface RecipeCardProps {
|
||||||
recipe: RecipeMetadata;
|
recipe: RecipeMetadata;
|
||||||
@ -57,7 +58,7 @@ export default function RecipeGridCard({ recipe }: RecipeCardProps) {
|
|||||||
role="listitem"
|
role="listitem"
|
||||||
className="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded"
|
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>
|
</span>
|
||||||
))}
|
))}
|
||||||
{recipe.tags.length > 3 && (
|
{recipe.tags.length > 3 && (
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import type { Recipe } from '@/lib/recipes';
|
import type { Recipe } from '@/lib/recipes';
|
||||||
|
import { capitalize } from '@/lib/format';
|
||||||
|
|
||||||
interface RecipePageLayoutProps {
|
interface RecipePageLayoutProps {
|
||||||
recipe: Recipe;
|
recipe: Recipe;
|
||||||
@ -83,7 +84,7 @@ export default function RecipePageLayout({ recipe, children }: RecipePageLayoutP
|
|||||||
role="listitem"
|
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"
|
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>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import FacetGroup from './FacetGroup';
|
import FacetGroup from './FacetGroup';
|
||||||
import type { FacetCounts, FilterState } from '@/lib/types';
|
import type { FacetCounts, FilterState } from '@/lib/types';
|
||||||
|
import { capitalize } from '@/lib/format';
|
||||||
|
|
||||||
interface RecipesSidebarProps {
|
interface RecipesSidebarProps {
|
||||||
categories: string[];
|
categories: string[];
|
||||||
@ -15,10 +16,6 @@ interface RecipesSidebarProps {
|
|||||||
const COLLAPSED_CATEGORY_COUNT = 8;
|
const COLLAPSED_CATEGORY_COUNT = 8;
|
||||||
const COLLAPSED_TAG_COUNT = 10;
|
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 }) {
|
function ShowMoreButton({ expanded, hiddenCount, onClick }: { expanded: boolean; hiddenCount: number; onClick: () => void }) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@ -199,7 +196,7 @@ export default function RecipesSidebar({ categories, tags, facetCounts, filters,
|
|||||||
onChange={() => handleTagToggle(tag)}
|
onChange={() => handleTagToggle(tag)}
|
||||||
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 accent-blue-600"
|
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>
|
<span className="text-xs tabular-nums text-gray-500 dark:text-gray-400">{count}</span>
|
||||||
</label>
|
</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",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"test": "node --import tsx --test lib/recipe-content.test.ts"
|
"test": "node --import tsx --test lib/*.test.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "^5.3.0",
|
"@fontsource/inter": "^5.3.0",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user