AndymickandClaude Haiku 4.5 7cb8d9b5a6 Stage 3: Complete expense pages + reports
- Add Expense CRUD pages (/expenses list, /new form, /[id] detail) + actions
- Expense form: date/description/category/amount/receipt, auto-create from purchases
- Reports page: 3 tabs (monthly profit, expense breakdown, cash flow) with tables
- Add Expenses + Reports to AccountsNav tabs

All expense features wired. Reports skeleton ready for data integration.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-15 18:14:14 +01:00

Craft2Prints

A personalized e-commerce site: browse templates (apparel, drinkware, phone cases), customize them with your own text/images in an in-browser design tool, and check out with real Stripe payments. Built with Next.js (App Router), Prisma, and Stripe.

Sharing via WhatsApp / Messenger

Instead of (or alongside) email, the confirmation screen after uploading a design gives you two direct share buttons:

  • WhatsApp works immediately, no setup — opens WhatsApp with the message and link pre-filled, you just pick who to send it to.
  • Messenger needs a one-time setup: create a free app at https://developers.facebook.com/apps, add your site's domain under the app's settings, copy the App ID, and set NEXT_PUBLIC_FACEBOOK_APP_ID in .env. Until that's set, the Messenger button just won't appear — WhatsApp sharing still works either way.

Right now NEXT_PUBLIC_SITE_URL is http://localhost:3000, which only works on the same computer that's running npm run dev. If you open a design-approval link on your phone or another device, it will fail to load — localhost always means "this device," never your computer over the network. Some phones/carriers redirect failed addresses like that to random landing pages, which can look like a broken or suspicious link.

This isn't a bug to fix — it resolves itself once the site is deployed to a real domain (later, when we cover deployment). Until then, only test approval links on the same computer running the dev server.

Sending emails

To have the approval link emailed to the customer automatically (instead of just copying it yourself), add SMTP credentials to .env:

SMTP_HOST="smtp.gmail.com"
SMTP_PORT="587"
SMTP_USER="youraddress@gmail.com"
SMTP_PASSWORD="your app password"
MAIL_FROM="Craft2Prints <youraddress@gmail.com>"

For Gmail, SMTP_PASSWORD needs to be an app password, not your normal login password — generate one at https://myaccount.google.com/apppasswords (requires 2-factor authentication to be turned on). Most other providers (Outlook, a business email host, SendGrid, etc.) work the same way — check their SMTP settings page.

If SMTP_HOST is left blank, nothing breaks — the design still gets a shareable link, you just have to copy and send it yourself instead of it emailing automatically.

If you upload a photo, drag the pink box that appears over it to mark exactly where the design tool should let customers place their text/artwork — no more relying on a generic preset. The preview crops to a square (matching how the design tool displays it), so the box you draw lines up correctly for the customer later.

Optionally, upload a real photo instead of using the illustration. Worth knowing: the color swatches recolor the illustration live, but a photo is fixed — picking a different color still gets saved with the order, it just won't change the picture. Leave the photo field blank to keep the illustration and its live color-recoloring.

Personalizing the back too

If you set a back photo, customers get a Front/Back toggle on the design page and can add a completely separate design to each side — their own text/images on the front, different ones on the back. When you upload the back photo, drag a print-area box for it just like the front (defaults to the same box as the front if you skip this). "Add to bag" captures both sides.

Real photo per color

Each color's photo slot now has both a Front and Back upload — the "Photo per color" section shows both side by side. Attach a back photo for a color and it shows correctly when a customer views that color's back (falls back to the product's main back photo, or the illustration, if left blank).

For the most accurate look, attach an actual photo for specific colors instead of relying on the tint. Once you've added colors above, a "Photo per color" section appears — attach a file to any color and that exact photo is shown the instant a customer picks that color on the design page. Colors left blank fall back to the front photo (tinted or fixed) or the illustration. This is the best option when you have real supplier/product photography for each color — no tinting compromise, just the true photo.

Two separate product pages now

  • /made-to-order/[slug] — the full design tool (only reachable for products with "Personalised catalog" checked)
  • /products/[slug] — a plain "pick a color/size, buy as-is" page, no design tool, no print-area box (only reachable for products with "Products catalog" checked)

A product checked for both catalogs shows the design tool when reached via /made-to-order and the plain buy-as-is page when reached via /products — same photos and colors either way, just a different experience depending on which catalog someone came from.

Listing a product on both catalogs

On the product form there's a "Show on" section with two checkboxes:

  • Personalised catalog (/made-to-order) — customers design their own version
  • Products catalog (/products) — same photos, browse and buy as-is

Check either one or both. The same product record — same photos, colors, price — can appear on both pages at once; nothing needs to be duplicated. /products now has its own real search/category/color/price filters, same as /made-to-order, just scoped to whichever products have that checkbox on. Each links to its own kind of detail page — see "Two separate product pages" above.

Currency

Prices are stored as an integer in GBP pence — that's the canonical currency for the whole site. A currency selector in the header (£ GBP / $ USD / € EUR) lets customers switch the display currency; it remembers their choice in the browser.

Important: the conversion rates in src/lib/currency.ts are fixed approximations, not live exchange rates, and this only changes what's displayed — there's no checkout yet, so no real payment currency conversion happens. Update those rates periodically if you want displayed foreign-currency prices to stay roughly current, and revisit this properly once Stripe checkout is built (Stripe can charge in whichever currency you configure, which may or may not want to match what's shown here).

Admin pages (product forms, proofs, order management) always show plain £ — they're managing the actual stored value, not a customer-facing display, so they're not tied to the selector.

Cart storage

The cart is persisted to IndexedDB, not localStorage. This matters because each personalized item can carry up to 4 full-size images (print files + placement references) — that adds up fast, and localStorage has a hard ~5-10MB cap per site that a cart with even 2-3 custom items can blow past, causing a hard crash (QuotaExceededError) right when someone tries to check out. IndexedDB doesn't have that low ceiling, so this is a real fix, not a workaround.

One thing worth knowing as the cart/checkout evolves further: the checkout request itself sends the full cart (images included) from the browser to the server in one go. That's fine at normal sizes, but if you ever see checkout failing specifically with very large/many-image carts, that request payload size is the next place to look — some hosts cap request body size (this isn't an issue on a normal self-hosted Node server, but would be on some serverless platforms).

Archiving orders

Paid or failed orders older than 30 days archive automatically — this happens as a sweep each time you open Admin → Orders, not a background job, but it's effectively invisible either way. You can also archive any order manually right away with the button on its row or its detail page, no need to wait. Archived orders move out of the main list; view them at Admin → Orders → "View archived," with an Unarchive button to bring one back. Orders still PENDING are never auto-archived, only PAID or FAILED ones.

Checkout — setting it up

Checkout is now real: the cart page creates an Order in the database, sends the customer to Stripe's hosted payment page, and a webhook marks the order paid once Stripe confirms it. Here's what you need to actually make it work:

1. Get Stripe test keys

Sign up / log in at https://dashboard.stripe.com, make sure you're in test mode (toggle top-right), then go to Developers → API keys. Copy the Secret key (sk_test_...) into .env:

STRIPE_SECRET_KEY="sk_test_..."

2. Set up the webhook for local testing

Stripe needs to reach your webhook endpoint, but localhost isn't reachable from the internet. For local development, use the Stripe CLI:

  1. Install it: https://docs.stripe.com/stripe-cli (or brew install stripe/stripe-cli/stripe on Mac)
  2. Run stripe login once
  3. While your dev server is running, in another terminal: stripe listen --forward-to localhost:3000/api/webhook
  4. It'll print a webhook signing secret starting whsec_... — copy that into .env:
STRIPE_WEBHOOK_SECRET="whsec_..."
  1. Keep that stripe listen command running in the background whenever you're testing checkout locally — it forwards Stripe's events to your local server.

(Once deployed to a real domain, you'd instead create a proper webhook endpoint in the Stripe Dashboard pointing at https://yourdomain.com/api/webhook and use that endpoint's signing secret instead — the CLI approach is just for local dev.)

3. Test a payment

Add something to your bag, go to /cart, click Checkout. On Stripe's page, use a test card: 4242 4242 4242 4242, any future expiry date, any 3-digit CVC, any postcode. It'll redirect to /checkout/success, and — assuming stripe listen is running — the order should show as PAID within a second or two. Check Admin → Orders to see it, download the print-ready PNGs, etc.

4. Going live later

When you're ready for real payments: switch to your live Stripe keys (toggle out of test mode in the dashboard), set up a real webhook endpoint in the Stripe Dashboard for your live domain, and update .env with the live secret key and that webhook's signing secret. Also worth adding: order confirmation emails to the customer (not built yet — currently they only see the on-screen confirmation and whatever Stripe itself emails them), and probably restricting shipping_address_collection in src/app/api/checkout/route.ts to just the countries you actually ship to.

Print-ready design files

The exported design image (used for the cart preview, and downloadable from the cart page and from Admin → Orders) is just the customer's artwork — transparent background, no shirt, no print-area guide marks baked in. Every order's items show "Download front PNG" / "Download back PNG" links on its Admin → Orders detail page, so once a real order comes in, that's where to get the print files from.

Two images per item, two different jobs: alongside that clean print file, there's also a "placement reference" image — the design shown composited on the actual product photo, so you can see exactly where it sits before you print it. That's the larger thumbnail shown on the order detail page (and on the cart/success pages); the download links next to it are the clean, print-only files. For products using the illustration (no real photo uploaded), there's no placement reference to show — only real photos can be composited this way.

Adding products

Go to Admin → Add product (or /admin/products/new). Fill in the name, category (pulled from your manageable category list — see below), price, and colors (click a swatch to add it, no hex-typing needed). Pick a template shape (t-shirt, hoodie, mug, tumbler, or phone case) to use the built-in illustration, or upload your own front — and optionally back — photo instead. It goes straight into the personalized catalog at /made-to-order.

Manage what's there at /admin/products — view, edit (change anything, including swapping the photo or colors later), or delete. Deleting is blocked if a product already has orders or design proofs tied to it, to avoid orphaning that history.

Recoloring a photo (front/back + live color tint)

If you upload a photo, drag the pink box that appears over it to mark exactly where the design tool should let customers place their text/artwork. You can also add a back photo — customers get a Front/Back toggle (the design tool only applies to the front).

Check "Let the color swatches recolor this photo" to have the color picker actually retint the photo live, instead of just being a label. This only looks good for photos that are black, white, or gray — the color is applied as a multiply-blend tint, so a photo that's already colored (like a red mug) will just look muddy if you turn this on. For black-photo products specifically: expect punchy mid-tone colors (oranges, blues, greens) to look great, but very light colors (white, pastels) will come out as a muted gray rather than true white — that's a real limit of tinting a photo that has no bright highlight data to begin with, not a bug. If you need a true white variant to look right, upload a separate white-photo product instead of relying on the tint.

Scoping categories to a catalog

Categories can now be scoped too, same idea as products: on Admin → Add category, two checkboxes control where a category shows up as a filter — Personalised (/made-to-order), Products (/products), or both (the default). Toggle these on existing categories from Admin → All categories. This only affects which catalog's filter sidebar shows the category — it doesn't restrict which products can use it.

Managing categories

Categories are managed from Admin → Add category / All categories, not hardcoded. A new category shows up immediately in: the product form's category dropdown, the shop's filter sidebar, and the "Personalised" menu in the header. Deleting a category is blocked if any product is still using it.

Sizes

On the product form, add sizes via the preset buttons (XSXXL) or type a custom one (for anything non-apparel-shaped, like "One Size" or a numeric size) and hit Enter. Leave it empty for products that don't need sizing — mugs, phone cases, etc. When a product has sizes, a size picker automatically appears on its design page, and the chosen size is saved with the cart item.

This only applies to the personalized catalog (/made-to-order) for now — the ready-made catalog (/products) is still just a placeholder page, so there's nowhere for a size picker to live yet.

Dark mode

There's a toggle in the header (sun/moon icon) that switches between light and dark. It respects the visitor's system preference on first visit, then remembers whatever they pick in localStorage after that. Most of the site flips automatically since the color system is built on a handful of tokens (background, text, borders, card surfaces) that swap together — new pages you add will pick this up for free as long as they use the existing bg-paper / text-ink / border-line / bg-surface classes rather than hardcoded colors like bg-white.

Chat with customers

Each design proof has its own chat thread. The customer sees it right on their approval page (/proofs/[id]) and can ask for changes without emailing back and forth. You see and reply to every conversation at /admin/inbox.

It's polling-based (checks for new messages every few seconds) rather than true real-time — simple and reliable to self-host, just a couple seconds of lag instead of instant delivery. The initial email still goes out when you upload a design, so the customer gets pinged even if they're not looking at the page; the chat is for the conversation that follows.

Custom design approvals (you upload, customer approves)

For one-off custom jobs — you design a T-shirt yourself and want a specific customer to review and buy it — there's a separate flow from the self-serve design tool:

  1. Go to http://localhost:3000/admin/proofs/new (password-protected, see below)
  2. Pick the product template, enter the customer's email, upload your finished design image, set color/quantity/price
  3. You'll get a shareable link like /proofs/abc123
  4. Send that link to the customer any way you like (email, text)
  5. They open it, see the design, and click "Approve & add to bag" — it goes straight into their cart, ready for checkout

See everything you've sent at /admin/proofs.

Change the admin password before deploying. The /admin pages are protected by a login page at /admin/login, checked against ADMIN_USER / ADMIN_PASSWORD in .env. The defaults (admin / changeme) are fine for local testing but must be changed before this site is reachable from the internet. Once logged in, "Log out" is in the Admin dropdown in the header. This uses its own session cookie, signed with ADMIN_SESSION_SECRETchange that to a random value before deploying too (a fresh one has already been generated into your local .env).

Customer photo requests (they upload, you turn it into a design)

The other direction from the flow above — a customer can send you their own photo for you to work with, instead of you already having a finished design ready:

  1. They visit /custom-request, pick a product, upload their photo, and leave their contact info (name, email, optional phone) and a note
  2. You get an email alert (see "Sending emails" — same SMTP setup) and can browse all submissions at /admin/custom-requests
  3. Open a submission, and once you've worked on it, click "Start a design for this" — it takes you straight to /admin/proofs/new with the product/customer details pre-filled, so you just upload the finished result and send it back through the normal proof-approval flow above
  4. Each submission also has a one-click WhatsApp/Messenger button (opens pre-filled, for you to send/forward manually) and, if they left a phone number, a direct "Message on WhatsApp" link addressed to them

Set ADMIN_NOTIFY_EMAIL in .env to choose where new-submission alerts go — leave it blank and it defaults to your SMTP_USER address.

Customer accounts

Separate from the admin login above, customers can create their own accounts to see their order history:

  • /account/register and /account/login — sign up / sign in
  • /account — order history (only orders placed while logged in, plus any past guest orders that used the same email — those get linked in automatically the first time someone registers or logs in)
  • /account/forgot-password — emails a reset link (uses the same SMTP setup as design-approval emails, see "Sending emails" above; if SMTP isn't configured you'll just see a generic confirmation with nothing actually sent)

Checkout still works without an account — customer accounts are optional, not required to buy.

This uses its own login cookie (SESSION_SECRET in .env), completely separate from the admin ADMIN_USER/ADMIN_PASSWORD — logging into one never grants access to the other. Change SESSION_SECRET to a random value before deploying, same as the admin password (a fresh one has already been generated into your local .env, so this only matters when you deploy somewhere new).

Security: rate limiting & CAPTCHA

  • Login rate limiting works with no setup — customer login (/account/login), admin login (/admin/login), and the checkout API are all limited per IP address (5 attempts / 15 min for logins, 20 requests / 10 min for checkout), backed by the database so it works the same on a single server or serverless. Nothing to configure.
  • CAPTCHA (Cloudflare Turnstile) protects the custom-request, account registration, newsletter signup, and contact forms — but only once you set NEXT_PUBLIC_TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY in .env. Get a free site key + secret at dash.cloudflare.com under Turnstile. Until those are set, the forms work normally with no CAPTCHA shown — it's opt-in, not required to run the site.

What's built so far

  • Homepage (/) with a split path between Personalised and Products
  • Personalized product catalog with search, category, color, and price filters (/made-to-order)
  • Product detail page with the design tool, reviews, and related products (/made-to-order/[slug])
  • Full product management: add, edit, delete, with admin-managed categories and sizes (/admin/products, /admin/categories)
  • Custom design approval flow (/admin/proofs/new, /admin/proofs, /proofs/[id])
  • Per-design chat between you and the customer (/admin/inbox)
  • Dark mode
  • Cart page (/cart) — view, adjust quantity, remove items, see total
  • Real checkout via Stripe, order persistence, and admin order management (/admin/orders)

Still to come: order confirmation emails, and a "buy it now" flow that skips straight to checkout for a single item (right now everything goes through the bag).

Updating an existing install

Already had this running before? After pulling in new files:

npx prisma generate
npm run db:push
npm run db:seed

db:push is safe to re-run — it only adds what's changed (new tables/columns), it won't wipe your existing data. db:seed is also safe to re-run: it only touches the 6 starter products that ship with this repo (matched by their slug — classic-tee, heavyweight-hoodie, etc.), syncing them to whatever's currently in prisma/seed.ts. Any products or categories you've added yourself through the admin panel are never touched by it. Also check .env.example for any new variables and add them to your own .env.

1. Install prerequisites

You need Node.js 18 or later. Check with:

node -v

If you don't have it, install it from https://nodejs.org (the LTS version is fine).

2. Install dependencies

From the project folder (where this README lives):

npm install

This will also automatically run prisma generate (via the postinstall script).

3. Set up your environment file

Copy the example file:

cp .env.example .env

Open .env and fill in:

  • DATABASE_URL — leave as file:./dev.db for now (SQLite, zero config)
  • STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET — get these later from https://dashboard.stripe.com/apikeys once we build checkout. Not needed yet to run the homepage/catalog.
  • NEXT_PUBLIC_SITE_URLhttp://localhost:3000 for local dev

4. Create the database and load starter products

npm run db:push
npm run db:seed

db:push creates the SQLite database file from prisma/schema.prisma. db:seed loads the 6 starter products (tee, hoodie, mug, tumbler, 2 phone cases).

5. Run it

npm run dev

Open http://localhost:3000 in your browser. You should see the homepage; click "Shop all" to see the catalog page with filters.

Any time you change code, the page auto-reloads. If you change prisma/schema.prisma, re-run npm run db:push.

Troubleshooting

  • "Cannot find module '@prisma/client'" — run npx prisma generate manually.
  • Port 3000 already in use — stop whatever else is running on that port, or run npm run dev -- -p 3001 to use a different port.
  • Blank product grid — make sure you ran npm run db:seed.

Deploying to your own server later

This app is a standard Next.js app, so once it's finished it can run anywhere that supports Node.js:

npm run build
npm run start   # serves on port 3000 by default

Put it behind nginx (or similar) as a reverse proxy, and use a process manager like pm2 to keep it running. We'll cover this in detail once the whole app is built and ready to deploy.

S
Description
No description provided
Readme
1,010 MiB
Languages
TypeScript 98.7%
JavaScript 0.9%
CSS 0.2%
PowerShell 0.2%