feat(newsletter): add newsletter generation utilities and pages #69

Merged
addison merged 15 commits from exe-dev-bot/market:feat/newsletter-generation into main 2026-02-06 04:40:10 -05:00
Contributor

Summary

Implements the Newsletter Generation section of spec-003 (Weekly Newsletter). This PR adds the core utilities and pages for generating, storing, and viewing newsletters.

Changes

New Files

  • src/lib/newsletter.ts - Newsletter content generation utility with generateNewsletterContent() function
  • src/lib/newsletter-email.ts - HTML email template renderer with renderNewsletterEmail() function
  • src/lib/service-client.ts - Isolated service role client (bypasses RLS, restricted by ESLint)
  • src/lib/format.ts - Shared formatting utilities (truncateText, formatDate, getInitials)
  • src/lib/theme-colors.generated.ts - Auto-generated email-safe hex colors from themes.css
  • scripts/generate-theme-colors.ts - Build script to regenerate theme colors
  • src/pages/my/newsletters/index.astro - Page listing user's newsletters (up to 4 most recent)
  • src/pages/my/newsletters/[id].astro - Newsletter detail view page

Modified Files

  • src/lib/auth.ts - Removed service client (moved to separate restricted file)
  • src/pages/dashboard.astro - Added "My Newsletters" link to Quick Access section
  • src/styles/themes.css - Added documentation about regenerating email colors
  • justfile - Added generate-theme-colors recipe
  • eslint.config.js - Added restriction on service-client imports (only allowed in src/worker.ts)
  • specs/003-weekly-newsletter.md - Marked Newsletter Generation tasks as complete

Theme Color Generation

Theme colors are defined in themes.css using OKLCH format. Since email clients don't support OKLCH or CSS variables, we generate hex equivalents:

just generate-theme-colors

Run this after modifying theme colors in themes.css.

Testing

Setup

  • Reset the database: just db-reset
  • Start the dev server: just dev
  • Log in as any test user (e.g., alice@example.com / password)
  • Navigate to Dashboard
  • Verify "My Newsletters" link appears in Quick Access section
  • Click the link - should show "No newsletters yet" message

Newsletter Detail View (Manual Insert)

  1. Open Supabase Studio: http://localhost:54323
  2. Go to Table Editor → newsletter
  3. Click Insert row and add:
{
  "user_id": "22222222-2222-2222-2222-222222222201",
  "content_json": {
    "announcements": [
      {
        "id": "11111111-1111-1111-1111-111111111101",
        "content_html": "<p>Welcome to our <strong>first newsletter</strong>! We're excited to have you.</p>",
        "publish_date": "2025-02-05"
      }
    ],
    "requests": [
      {
        "id": "44444444-4444-4444-4444-444444444405",
        "title": "Looking for Used Stove",
        "description": "Need a working gas or electric stove for my apartment. Must be in good condition.",
        "price_string": "Budget: $200",
        "category_name": "Resale",
        "first_image_url": null,
        "owner_display_name": "Carol Davis",
        "owner_avatar_url": "avatars/22222222-2222-2222-2222-222222222203/avatar.png"
      },
      {
        "id": "44444444-4444-4444-4444-444444444409",
        "title": "Custom Business Cards",
        "description": "Looking for someone to design and print custom business cards for my bakery. Need about 500 cards.",
        "price_string": "Budget: $75",
        "category_name": "New",
        "first_image_url": null,
        "owner_display_name": "Eve Martinez",
        "owner_avatar_url": "avatars/22222222-2222-2222-2222-222222222205/avatar.png"
      }
    ],
    "for_sale": [
      {
        "id": "44444444-4444-4444-4444-444444444401",
        "title": "Handmade Ceramic Vase",
        "description": "Beautiful hand-thrown vase with blue glaze. Perfect for fresh flowers or as a standalone piece. Approx 8\" tall.",
        "price_string": "$45",
        "category_name": "New",
        "first_image_url": "items/44444444-4444-4444-4444-444444444401/0.png",
        "owner_display_name": "Alice Johnson",
        "owner_avatar_url": "avatars/22222222-2222-2222-2222-222222222201/avatar.png"
      },
      {
        "id": "44444444-4444-4444-4444-444444444403",
        "title": "Custom 3D Printed Phone Stand",
        "description": "Adjustable phone stand, available in multiple colors. Specify your color preference when ordering.",
        "price_string": "$15",
        "category_name": "New",
        "first_image_url": "items/44444444-4444-4444-4444-444444444403/0.png",
        "owner_display_name": "Bob Smith",
        "owner_avatar_url": "avatars/22222222-2222-2222-2222-222222222202/avatar.png"
      },
      {
        "id": "44444444-4444-4444-4444-444444444410",
        "title": "Vintage Office Desk",
        "description": "Solid wood desk from the 1960s. Great condition with minor wear. Dimensions: 60\"x30\"x30\".",
        "price_string": "$175",
        "category_name": "Resale",
        "first_image_url": "items/44444444-4444-4444-4444-444444444410/0.png",
        "owner_display_name": "Eve Martinez",
        "owner_avatar_url": "avatars/22222222-2222-2222-2222-222222222205/avatar.png"
      }
    ],
    "featured_vendors": [
      {
        "id": "22222222-2222-2222-2222-222222222201",
        "display_name": "Alice Johnson",
        "vendor_id": "alice-pottery",
        "avatar_url": "avatars/22222222-2222-2222-2222-222222222201/avatar.png",
        "about": "Local artisan specializing in handmade pottery and ceramics. Based in Portland."
      },
      {
        "id": "22222222-2222-2222-2222-222222222202",
        "display_name": "Bob Smith",
        "vendor_id": "bob-tech",
        "avatar_url": "avatars/22222222-2222-2222-2222-222222222202/avatar.png",
        "about": "Freelance software developer and 3D printing enthusiast."
      },
      {
        "id": "22222222-2222-2222-2222-222222222204",
        "display_name": "David Lee",
        "vendor_id": "david-repairs",
        "avatar_url": "avatars/22222222-2222-2222-2222-222222222204/avatar.png",
        "about": "Professional handyman offering home repair and maintenance services."
      }
    ],
    "generated_at": "2025-02-05T12:00:00Z",
    "user_theme": "dusk"
  }
}
  1. Verification:
    • Go to /my/newsletters - should now show one newsletter
    • Click on the newsletter - should render all sections correctly
    • Verify announcements section shows HTML content
    • Verify requests, for sale, and vendors sections render with real data
    • Verify item images display correctly (signed URLs should work)
    • Try logging in as a different user (bob@example.com) - should NOT see Alice's newsletter

Notes

  • The newsletter generation utility is designed to be called by a cron job (implemented in Newsletter Delivery PR)
  • Email rendering will be tested via Mailpit when the cron job is implemented
  • RLS policies ensure users can only view their own newsletters
  • Service client is ESLint-restricted to prevent misuse (only src/worker.ts can import it)
## Summary Implements the Newsletter Generation section of spec-003 (Weekly Newsletter). This PR adds the core utilities and pages for generating, storing, and viewing newsletters. ## Changes ### New Files - `src/lib/newsletter.ts` - Newsletter content generation utility with `generateNewsletterContent()` function - `src/lib/newsletter-email.ts` - HTML email template renderer with `renderNewsletterEmail()` function - `src/lib/service-client.ts` - Isolated service role client (bypasses RLS, restricted by ESLint) - `src/lib/format.ts` - Shared formatting utilities (truncateText, formatDate, getInitials) - `src/lib/theme-colors.generated.ts` - Auto-generated email-safe hex colors from themes.css - `scripts/generate-theme-colors.ts` - Build script to regenerate theme colors - `src/pages/my/newsletters/index.astro` - Page listing user's newsletters (up to 4 most recent) - `src/pages/my/newsletters/[id].astro` - Newsletter detail view page ### Modified Files - `src/lib/auth.ts` - Removed service client (moved to separate restricted file) - `src/pages/dashboard.astro` - Added "My Newsletters" link to Quick Access section - `src/styles/themes.css` - Added documentation about regenerating email colors - `justfile` - Added `generate-theme-colors` recipe - `eslint.config.js` - Added restriction on service-client imports (only allowed in src/worker.ts) - `specs/003-weekly-newsletter.md` - Marked Newsletter Generation tasks as complete ## Theme Color Generation Theme colors are defined in `themes.css` using OKLCH format. Since email clients don't support OKLCH or CSS variables, we generate hex equivalents: ```bash just generate-theme-colors ``` Run this after modifying theme colors in `themes.css`. ## Testing ### Setup - [x] Reset the database: `just db-reset` - [x] Start the dev server: `just dev` ### Dashboard Link - [x] Log in as any test user (e.g., alice@example.com / password) - [x] Navigate to Dashboard - [x] Verify "My Newsletters" link appears in Quick Access section - [x] Click the link - should show "No newsletters yet" message ### Newsletter Detail View (Manual Insert) 1. Open Supabase Studio: http://localhost:54323 2. Go to **Table Editor → newsletter** 3. Click **Insert row** and add: ```json { "user_id": "22222222-2222-2222-2222-222222222201", "content_json": { "announcements": [ { "id": "11111111-1111-1111-1111-111111111101", "content_html": "<p>Welcome to our <strong>first newsletter</strong>! We're excited to have you.</p>", "publish_date": "2025-02-05" } ], "requests": [ { "id": "44444444-4444-4444-4444-444444444405", "title": "Looking for Used Stove", "description": "Need a working gas or electric stove for my apartment. Must be in good condition.", "price_string": "Budget: $200", "category_name": "Resale", "first_image_url": null, "owner_display_name": "Carol Davis", "owner_avatar_url": "avatars/22222222-2222-2222-2222-222222222203/avatar.png" }, { "id": "44444444-4444-4444-4444-444444444409", "title": "Custom Business Cards", "description": "Looking for someone to design and print custom business cards for my bakery. Need about 500 cards.", "price_string": "Budget: $75", "category_name": "New", "first_image_url": null, "owner_display_name": "Eve Martinez", "owner_avatar_url": "avatars/22222222-2222-2222-2222-222222222205/avatar.png" } ], "for_sale": [ { "id": "44444444-4444-4444-4444-444444444401", "title": "Handmade Ceramic Vase", "description": "Beautiful hand-thrown vase with blue glaze. Perfect for fresh flowers or as a standalone piece. Approx 8\" tall.", "price_string": "$45", "category_name": "New", "first_image_url": "items/44444444-4444-4444-4444-444444444401/0.png", "owner_display_name": "Alice Johnson", "owner_avatar_url": "avatars/22222222-2222-2222-2222-222222222201/avatar.png" }, { "id": "44444444-4444-4444-4444-444444444403", "title": "Custom 3D Printed Phone Stand", "description": "Adjustable phone stand, available in multiple colors. Specify your color preference when ordering.", "price_string": "$15", "category_name": "New", "first_image_url": "items/44444444-4444-4444-4444-444444444403/0.png", "owner_display_name": "Bob Smith", "owner_avatar_url": "avatars/22222222-2222-2222-2222-222222222202/avatar.png" }, { "id": "44444444-4444-4444-4444-444444444410", "title": "Vintage Office Desk", "description": "Solid wood desk from the 1960s. Great condition with minor wear. Dimensions: 60\"x30\"x30\".", "price_string": "$175", "category_name": "Resale", "first_image_url": "items/44444444-4444-4444-4444-444444444410/0.png", "owner_display_name": "Eve Martinez", "owner_avatar_url": "avatars/22222222-2222-2222-2222-222222222205/avatar.png" } ], "featured_vendors": [ { "id": "22222222-2222-2222-2222-222222222201", "display_name": "Alice Johnson", "vendor_id": "alice-pottery", "avatar_url": "avatars/22222222-2222-2222-2222-222222222201/avatar.png", "about": "Local artisan specializing in handmade pottery and ceramics. Based in Portland." }, { "id": "22222222-2222-2222-2222-222222222202", "display_name": "Bob Smith", "vendor_id": "bob-tech", "avatar_url": "avatars/22222222-2222-2222-2222-222222222202/avatar.png", "about": "Freelance software developer and 3D printing enthusiast." }, { "id": "22222222-2222-2222-2222-222222222204", "display_name": "David Lee", "vendor_id": "david-repairs", "avatar_url": "avatars/22222222-2222-2222-2222-222222222204/avatar.png", "about": "Professional handyman offering home repair and maintenance services." } ], "generated_at": "2025-02-05T12:00:00Z", "user_theme": "dusk" } } ``` 4. Verification: - [x] Go to `/my/newsletters` - should now show one newsletter - [x] Click on the newsletter - should render all sections correctly - [x] Verify announcements section shows HTML content - [x] Verify requests, for sale, and vendors sections render with real data - [x] Verify item images display correctly (signed URLs should work) - [x] Try logging in as a different user (bob@example.com) - should NOT see Alice's newsletter ## Notes - The newsletter generation utility is designed to be called by a cron job (implemented in Newsletter Delivery PR) - Email rendering will be tested via Mailpit when the cron job is implemented - RLS policies ensure users can only view their own newsletters - Service client is ESLint-restricted to prevent misuse (only `src/worker.ts` can import it)
Co-authored-by: Shelley <shelley@exe.dev>
Co-authored-by: Shelley <shelley@exe.dev>
Co-authored-by: Shelley <shelley@exe.dev>
Co-authored-by: Shelley <shelley@exe.dev>
Co-authored-by: Shelley <shelley@exe.dev>
feat(newsletter): add email template renderer
Some checks failed
CI / Lint, Type Check & Format (pull_request) Successful in 1m48s
CI / Unit Tests (pull_request) Successful in 1m47s
CI / E2E Tests (pull_request) Failing after 3m39s
c324d79ce6
Co-authored-by: Shelley <shelley@exe.dev>
Co-authored-by: Shelley <shelley@exe.dev>
refactor(newsletter): extract shared format utilities
Some checks failed
CI / Unit Tests (pull_request) Successful in 1m29s
CI / Lint, Type Check & Format (pull_request) Successful in 2m38s
CI / E2E Tests (pull_request) Failing after 3m24s
3107ed3365
Co-authored-by: Shelley <shelley@exe.dev>
refactor(auth): isolate service client with ESLint restrictions
Some checks failed
CI / Unit Tests (pull_request) Successful in 1m48s
CI / Lint, Type Check & Format (pull_request) Successful in 2m5s
CI / E2E Tests (pull_request) Failing after 3m15s
dc20c67e0b
Co-authored-by: Shelley <shelley@exe.dev>
test: add newsletter email preview script for Mailpit
Some checks failed
CI / Lint, Type Check & Format (pull_request) Failing after 1m21s
CI / Unit Tests (pull_request) Successful in 1m38s
CI / E2E Tests (pull_request) Failing after 4m47s
7342e3ba95
Co-authored-by: Shelley <shelley@exe.dev>
revert: remove test email script (will test via actual cron job)
Some checks failed
CI / Unit Tests (pull_request) Successful in 1m57s
CI / Lint, Type Check & Format (pull_request) Successful in 2m7s
CI / E2E Tests (pull_request) Failing after 3m16s
3158065884
Co-authored-by: Shelley <shelley@exe.dev>
refactor: use html-entities library instead of handmade escapeHtml
Some checks failed
CI / Unit Tests (pull_request) Successful in 1m32s
CI / Lint, Type Check & Format (pull_request) Successful in 1m46s
CI / E2E Tests (pull_request) Failing after 3m56s
58824269ae
Co-authored-by: Shelley <shelley@exe.dev>
fix: simplify newsletter link description
Some checks failed
CI / Unit Tests (pull_request) Failing after 1m19s
CI / Lint, Type Check & Format (pull_request) Successful in 2m21s
CI / E2E Tests (pull_request) Failing after 3m16s
68f5130ec4
Co-authored-by: Shelley <shelley@exe.dev>
fix: use signed URLs for newsletter images
Some checks failed
CI / Lint, Type Check & Format (pull_request) Successful in 2m6s
CI / Unit Tests (pull_request) Successful in 2m4s
CI / E2E Tests (pull_request) Failing after 3m19s
5a50a972e6
Co-authored-by: Shelley <shelley@exe.dev>
fix: align card footers to bottom in newsletter view
Some checks failed
CI / Unit Tests (pull_request) Successful in 1m25s
CI / Lint, Type Check & Format (pull_request) Successful in 2m6s
CI / E2E Tests (pull_request) Failing after 3m8s
b360d5f607
Co-authored-by: Shelley <shelley@exe.dev>
addison deleted branch feat/newsletter-generation 2026-02-06 04:40:10 -05:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
kwila/market!69
No description provided.