Files
craft2prints/README.md
T

492 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
## Important: testing links on your phone
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_..."
```
5. 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_SECRET`**change 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](https://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:
```bash
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:
```bash
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):
```bash
npm install
```
This will also automatically run `prisma generate` (via the `postinstall` script).
## 3. Set up your environment file
Copy the example file:
```bash
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_URL``http://localhost:3000` for local dev
## 4. Create the database and load starter products
```bash
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
```bash
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:
```bash
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.