docs: consolidate project tracking into PROGRESS.md
- Create PROGRESS.md as single source of truth for status - Slim ROADMAP.md to vision only (~100 lines, down from ~500) - Expand CLAUDE.md with streams, auth routing, forms, workflow - Convert AGENTS.md to stub pointing to CLAUDE.md - Update plan files with status headers, remove progress trackers Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
455
ROADMAP.md
455
ROADMAP.md
@@ -1,195 +1,31 @@
|
||||
# SimpleShop Roadmap
|
||||
|
||||
This document tracks future improvements, features, and known gaps.
|
||||
> Vision and future features. For current status, see [PROGRESS.md](PROGRESS.md).
|
||||
|
||||
---
|
||||
|
||||
## Core MVP: Real Products & Checkout (Priority)
|
||||
## Core MVP: Cart & Checkout
|
||||
|
||||
This section covers the work needed to turn SimpleShop from a theme demo into a working e-commerce storefront. Estimated total effort: **3-4 days** (leveraging existing Printify demo code).
|
||||
### Session-Based Cart
|
||||
Store cart in Phoenix session (no separate table needed for MVP).
|
||||
|
||||
### Phase A: Products Context + Printify Sync
|
||||
**Status:** Not implemented
|
||||
**Effort:** 1-1.5 days
|
||||
**Dependencies:** None
|
||||
|
||||
Replace `PreviewData` with real products synced from Printify.
|
||||
|
||||
**Schemas:**
|
||||
```elixir
|
||||
# products table
|
||||
field :printify_id, :string
|
||||
field :title, :string
|
||||
field :description, :text
|
||||
field :images, {:array, :map} # [{src, position}]
|
||||
field :print_provider_id, :integer
|
||||
field :blueprint_id, :integer
|
||||
field :synced_at, :utc_datetime
|
||||
field :published, :boolean, default: true
|
||||
timestamps()
|
||||
|
||||
# product_variants table
|
||||
field :printify_variant_id, :integer
|
||||
field :title, :string # e.g. "Black / M"
|
||||
field :sku, :string
|
||||
field :price_cents, :integer # selling price
|
||||
field :cost_cents, :integer # Printify cost (for profit calc)
|
||||
field :options, :map # %{"Color" => "Black", "Size" => "M"}
|
||||
field :is_available, :boolean
|
||||
belongs_to :product
|
||||
|
||||
# product_cost_history table (append-only for future analytics)
|
||||
field :cost_cents, :integer
|
||||
field :recorded_at, :utc_datetime
|
||||
belongs_to :product_variant
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
1. Migrate OAuth + Client modules from `simpleshop_printify` demo
|
||||
- Adapt `Simpleshop.Printify.OAuth` → `SimpleshopTheme.Printify.OAuth`
|
||||
- Adapt `Simpleshop.Printify.Client` → `SimpleshopTheme.Printify.Client`
|
||||
- Adapt `Simpleshop.Printify.TokenStore` → `SimpleshopTheme.Printify.TokenStore`
|
||||
2. Create `SimpleshopTheme.Products` context with schemas
|
||||
3. Add `mix sync_products` task to pull products from Printify
|
||||
4. Add webhook endpoint for `product:publish:started` events
|
||||
5. Replace `PreviewData` calls in LiveViews with `Products` context queries
|
||||
6. Store cost history on each sync for future profit analytics
|
||||
|
||||
**Webhook flow:**
|
||||
1. Seller clicks "Publish to SimpleShop" in Printify dashboard
|
||||
2. Printify fires `product:publish:started` with product data
|
||||
3. SimpleShop stores product locally in SQLite
|
||||
4. SimpleShop calls Printify "publish succeeded" endpoint
|
||||
|
||||
**Files to create:**
|
||||
- `lib/simpleshop_theme/printify/oauth.ex`
|
||||
- `lib/simpleshop_theme/printify/token_store.ex`
|
||||
- `lib/simpleshop_theme/products.ex`
|
||||
- `lib/simpleshop_theme/products/product.ex`
|
||||
- `lib/simpleshop_theme/products/variant.ex`
|
||||
- `lib/simpleshop_theme/products/cost_history.ex`
|
||||
- `lib/simpleshop_theme_web/controllers/printify_webhook_controller.ex`
|
||||
- `lib/mix/tasks/sync_products.ex`
|
||||
- `priv/repo/migrations/*_create_products.exs`
|
||||
|
||||
---
|
||||
|
||||
### Phase B: Session-Based Cart
|
||||
**Status:** Not implemented
|
||||
**Effort:** 0.5 days
|
||||
**Dependencies:** Phase A (Products)
|
||||
|
||||
Real cart functionality with session persistence.
|
||||
|
||||
**Approach:** Store cart in Phoenix session (no separate cart table needed for MVP). Cart is a map of `%{variant_id => quantity}` stored in the session.
|
||||
|
||||
**Implementation:**
|
||||
```elixir
|
||||
# lib/simpleshop_theme/cart.ex
|
||||
defmodule SimpleshopTheme.Cart do
|
||||
alias SimpleshopTheme.Products
|
||||
|
||||
def get(session), do: Map.get(session, "cart", %{})
|
||||
|
||||
def add_item(session, variant_id, quantity \\ 1)
|
||||
def remove_item(session, variant_id)
|
||||
def update_quantity(session, variant_id, quantity)
|
||||
def clear(session)
|
||||
|
||||
def to_line_items(cart) do
|
||||
# Returns list of %{variant: variant, quantity: qty, subtotal: price}
|
||||
end
|
||||
|
||||
def total(cart) # Returns total in cents
|
||||
def item_count(cart) # For header badge
|
||||
def to_line_items(cart)
|
||||
def total(cart)
|
||||
def item_count(cart)
|
||||
end
|
||||
```
|
||||
|
||||
**LiveView integration:**
|
||||
- Add `phx-click="add_to_cart"` to product pages
|
||||
- Update cart LiveView to use real data
|
||||
- Add cart count to header (assign in `on_mount`)
|
||||
|
||||
**Files to create/modify:**
|
||||
- `lib/simpleshop_theme/cart.ex`
|
||||
- Modify `lib/simpleshop_theme_web/live/shop_live/product_show.ex`
|
||||
- Modify `lib/simpleshop_theme_web/live/shop_live/cart.ex`
|
||||
- Modify `lib/simpleshop_theme_web/components/layouts.ex` (cart count)
|
||||
|
||||
---
|
||||
|
||||
### Phase C: Stripe Checkout
|
||||
**Status:** Not implemented
|
||||
**Effort:** 0.5-1 day
|
||||
**Dependencies:** Phase B (Cart)
|
||||
|
||||
### Stripe Checkout
|
||||
Stripe Checkout (hosted payment page) integration.
|
||||
|
||||
**Dependencies to add:**
|
||||
```elixir
|
||||
# mix.exs
|
||||
{:stripity_stripe, "~> 3.0"}
|
||||
```
|
||||
|
||||
**Config:**
|
||||
```elixir
|
||||
# config/runtime.exs
|
||||
config :stripity_stripe,
|
||||
api_key: System.get_env("STRIPE_SECRET_KEY")
|
||||
|
||||
# Also need STRIPE_WEBHOOK_SECRET for webhook verification
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
```elixir
|
||||
# lib/simpleshop_theme/checkout.ex
|
||||
defmodule SimpleshopTheme.Checkout do
|
||||
def create_session(cart, success_url, cancel_url) do
|
||||
line_items = Enum.map(cart, fn {variant_id, qty} ->
|
||||
variant = Products.get_variant!(variant_id)
|
||||
%{
|
||||
price_data: %{
|
||||
currency: "gbp",
|
||||
unit_amount: variant.price_cents,
|
||||
product_data: %{
|
||||
name: "#{variant.product.title} - #{variant.title}",
|
||||
images: [hd(variant.product.images)["src"]]
|
||||
}
|
||||
},
|
||||
quantity: qty
|
||||
}
|
||||
end)
|
||||
|
||||
Stripe.Checkout.Session.create(%{
|
||||
mode: "payment",
|
||||
line_items: line_items,
|
||||
success_url: success_url <> "?session_id={CHECKOUT_SESSION_ID}",
|
||||
cancel_url: cancel_url,
|
||||
shipping_address_collection: %{allowed_countries: ["GB"]},
|
||||
metadata: %{cart: Jason.encode!(cart)}
|
||||
})
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Webhook handler:**
|
||||
```elixir
|
||||
# lib/simpleshop_theme_web/controllers/stripe_webhook_controller.ex
|
||||
def handle_event(%Stripe.Event{type: "checkout.session.completed"} = event) do
|
||||
session = event.data.object
|
||||
cart = Jason.decode!(session.metadata["cart"])
|
||||
shipping = session.shipping_details
|
||||
|
||||
# Create order and push to Printify (Phase D)
|
||||
Orders.create_from_checkout(session, cart, shipping)
|
||||
end
|
||||
```
|
||||
|
||||
**Files to create:**
|
||||
- `lib/simpleshop_theme/checkout.ex`
|
||||
- `lib/simpleshop_theme_web/controllers/stripe_webhook_controller.ex`
|
||||
- `lib/simpleshop_theme_web/live/shop_live/checkout_success.ex`
|
||||
- `lib/simpleshop_theme_web/live/shop_live/checkout_cancel.ex`
|
||||
**Dependencies:** `{:stripity_stripe, "~> 3.0"}`
|
||||
|
||||
**Routes:**
|
||||
```elixir
|
||||
@@ -198,177 +34,34 @@ live "/checkout/success", ShopLive.CheckoutSuccess
|
||||
live "/checkout/cancel", ShopLive.CheckoutCancel
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase D: Orders + Printify Fulfillment
|
||||
**Status:** Not implemented
|
||||
**Effort:** 0.5-1 day
|
||||
**Dependencies:** Phase C (Stripe Checkout)
|
||||
|
||||
Create orders locally and push to Printify for fulfillment.
|
||||
|
||||
**Schema:**
|
||||
```elixir
|
||||
# orders table
|
||||
field :stripe_session_id, :string
|
||||
field :stripe_payment_intent_id, :string
|
||||
field :printify_order_id, :string
|
||||
field :status, Ecto.Enum, values: [:pending, :paid, :submitted, :in_production, :shipped, :delivered, :cancelled]
|
||||
field :total_cents, :integer
|
||||
field :shipping_address, :map
|
||||
field :customer_email, :string
|
||||
timestamps()
|
||||
|
||||
# order_items table
|
||||
field :quantity, :integer
|
||||
field :unit_price_cents, :integer
|
||||
field :unit_cost_cents, :integer # For profit tracking
|
||||
belongs_to :order
|
||||
belongs_to :product_variant
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
```elixir
|
||||
# lib/simpleshop_theme/orders.ex
|
||||
def create_from_checkout(stripe_session, cart, shipping) do
|
||||
Repo.transaction(fn ->
|
||||
# 1. Create local order
|
||||
order = create_order(stripe_session, shipping)
|
||||
|
||||
# 2. Create order items
|
||||
create_order_items(order, cart)
|
||||
|
||||
# 3. Push to Printify
|
||||
case Printify.Client.create_order(order) do
|
||||
{:ok, printify_response} ->
|
||||
update_order(order, %{
|
||||
printify_order_id: printify_response["id"],
|
||||
status: :submitted
|
||||
})
|
||||
{:error, reason} ->
|
||||
# Log error but don't fail - can retry later
|
||||
Logger.error("Failed to submit to Printify: #{inspect(reason)}")
|
||||
update_order(order, %{status: :paid}) # Mark as paid, needs manual submission
|
||||
end
|
||||
end)
|
||||
end
|
||||
```
|
||||
|
||||
**Printify order creation:**
|
||||
```elixir
|
||||
# Add to lib/simpleshop_theme/printify/client.ex
|
||||
def create_order(order) do
|
||||
body = %{
|
||||
external_id: order.id,
|
||||
shipping_method: 1, # Standard
|
||||
address_to: %{
|
||||
first_name: order.shipping_address["name"] |> String.split() |> hd(),
|
||||
last_name: order.shipping_address["name"] |> String.split() |> List.last(),
|
||||
email: order.customer_email,
|
||||
address1: order.shipping_address["line1"],
|
||||
address2: order.shipping_address["line2"],
|
||||
city: order.shipping_address["city"],
|
||||
zip: order.shipping_address["postal_code"],
|
||||
country: order.shipping_address["country"]
|
||||
},
|
||||
line_items: Enum.map(order.items, fn item ->
|
||||
%{
|
||||
product_id: item.variant.product.printify_id,
|
||||
variant_id: item.variant.printify_variant_id,
|
||||
quantity: item.quantity
|
||||
}
|
||||
end)
|
||||
}
|
||||
|
||||
post("/shops/#{shop_id()}/orders.json", body)
|
||||
end
|
||||
```
|
||||
|
||||
**Files to create:**
|
||||
- `lib/simpleshop_theme/orders.ex`
|
||||
- `lib/simpleshop_theme/orders/order.ex`
|
||||
- `lib/simpleshop_theme/orders/order_item.ex`
|
||||
- `priv/repo/migrations/*_create_orders.exs`
|
||||
- Update `lib/simpleshop_theme/printify/client.ex` with `create_order/1`
|
||||
|
||||
---
|
||||
|
||||
### Phase E: Cost Verification at Checkout (Safety Net)
|
||||
**Status:** Not implemented
|
||||
**Effort:** 0.25 days
|
||||
**Dependencies:** Phase D (Orders)
|
||||
|
||||
Verify Printify costs haven't changed before completing checkout.
|
||||
|
||||
**Implementation:**
|
||||
```elixir
|
||||
# In checkout flow, before creating Stripe session
|
||||
def verify_costs(cart) do
|
||||
Enum.reduce_while(cart, :ok, fn {variant_id, _qty}, _acc ->
|
||||
variant = Products.get_variant!(variant_id)
|
||||
|
||||
case Printify.Client.get_product(variant.product.printify_id) do
|
||||
{:ok, printify_product} ->
|
||||
current_cost = find_variant_cost(printify_product, variant.printify_variant_id)
|
||||
|
||||
if current_cost != variant.cost_cents do
|
||||
# Update local cost
|
||||
Products.update_variant_cost(variant, current_cost)
|
||||
|
||||
if cost_increase_exceeds_threshold?(variant.cost_cents, current_cost) do
|
||||
{:halt, {:error, :costs_changed, variant}}
|
||||
else
|
||||
{:cont, :ok}
|
||||
end
|
||||
else
|
||||
{:cont, :ok}
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
# Can't verify - proceed but log warning
|
||||
Logger.warning("Could not verify costs for variant #{variant_id}")
|
||||
{:cont, :ok}
|
||||
end
|
||||
end)
|
||||
end
|
||||
```
|
||||
|
||||
This ensures sellers never unknowingly sell at a loss due to Printify price changes.
|
||||
|
||||
---
|
||||
### Cost Verification at Checkout
|
||||
Verify Printify costs haven't changed before completing checkout to prevent selling at a loss.
|
||||
|
||||
---
|
||||
|
||||
## Medium Features
|
||||
|
||||
### Page Builder (Database-Driven Pages)
|
||||
**Status:** Planned (see `docs/plans/page-builder.md`)
|
||||
**Effort:** Large
|
||||
|
||||
Allow shop owners to build custom pages by combining pre-built sections:
|
||||
- Hero, Featured Products, Testimonials, Newsletter, etc.
|
||||
- Drag-and-drop section ordering
|
||||
### Page Builder
|
||||
Database-driven pages with drag-and-drop sections:
|
||||
- Hero, Featured Products, Testimonials, Newsletter
|
||||
- Per-section configuration
|
||||
- Database-backed page storage
|
||||
- See: [docs/plans/page-builder.md](docs/plans/page-builder.md)
|
||||
|
||||
---
|
||||
|
||||
## Future Features (Large Scope)
|
||||
## Future Features
|
||||
|
||||
### Multi-Admin Support
|
||||
Currently single-user authentication:
|
||||
- Multiple admin users
|
||||
- Role-based permissions
|
||||
- Audit logging
|
||||
|
||||
### Custom Domains
|
||||
Allow shops to use their own domain:
|
||||
- Domain verification
|
||||
- SSL certificate provisioning
|
||||
- DNS configuration guidance
|
||||
|
||||
### Theme Export/Import
|
||||
Backup and restore theme settings:
|
||||
- JSON export of all settings
|
||||
- Import with validation
|
||||
- Preset sharing between shops
|
||||
@@ -378,7 +71,7 @@ Backup and restore theme settings:
|
||||
- Custom JavaScript snippets
|
||||
- Code-level overrides for developers
|
||||
|
||||
### Multi-Provider Support (Future)
|
||||
### Multi-Provider Support
|
||||
Support multiple POD providers beyond Printify:
|
||||
- Prodigi (better for art prints)
|
||||
- Gelato (global fulfillment)
|
||||
@@ -390,7 +83,7 @@ Support multiple POD providers beyond Printify:
|
||||
## Technical Debt
|
||||
|
||||
### Test Coverage
|
||||
Phase 9 testing is basic. Areas needing better coverage:
|
||||
Areas needing better coverage:
|
||||
- Shop LiveView integration tests
|
||||
- CSS cache invalidation flow
|
||||
- Theme application across all pages
|
||||
@@ -402,113 +95,5 @@ Phase 9 testing is basic. Areas needing better coverage:
|
||||
- Graceful degradation when theme settings are invalid
|
||||
- Network error handling in LiveView
|
||||
|
||||
### Rename Project to SimpleShop
|
||||
**Status:** Not implemented
|
||||
**Effort:** Medium
|
||||
|
||||
The project is currently named `simpleshop_theme` (reflecting its origins as a theme system), but it's now a full e-commerce storefront. Rename to `simple_shop` or `simpleshop` to reflect this.
|
||||
|
||||
**Files to update:**
|
||||
- `mix.exs` - app name
|
||||
- `lib/simpleshop_theme/` → `lib/simple_shop/`
|
||||
- `lib/simpleshop_theme_web/` → `lib/simple_shop_web/`
|
||||
- All module names (`SimpleshopTheme` → `SimpleShop`)
|
||||
- `config/*.exs` - endpoint and repo references
|
||||
- `test/` directories
|
||||
- Database file name
|
||||
|
||||
---
|
||||
|
||||
## Completed (For Reference)
|
||||
|
||||
### Sample Content ("Wildprint Studio") ✅
|
||||
- 16 POD products across 5 categories
|
||||
- Nature/botanical theme with testimonials
|
||||
- UK-focused (prices in £)
|
||||
- Printify API integration for mockup generation (`mix generate_mockups`)
|
||||
|
||||
### Phase 1-8: Theme Editor ✅
|
||||
- Theme settings schema and persistence
|
||||
- CSS three-layer architecture
|
||||
- 8 theme presets
|
||||
- All customisation controls
|
||||
- Logo/header image uploads
|
||||
- SVG recolouring
|
||||
- Preview system with 7 pages
|
||||
|
||||
### Phase 9: Storefront Integration ✅
|
||||
- Public shop routes (/, /collections/:slug, /products/:id, /cart, /about, /contact)
|
||||
- Shared PageTemplates for shop and preview
|
||||
- CSS injection via shop layout
|
||||
- Themed error pages (404/500)
|
||||
- Dev routes for error page preview
|
||||
|
||||
### CSS Cache Warming on Startup ✅
|
||||
- ETS cache pre-warmed in `CSSCache.init/1`
|
||||
- First request doesn't need to generate CSS
|
||||
|
||||
### Navigation Links Between Admin and Shop ✅
|
||||
- "View Shop" button in theme editor header
|
||||
- Collapsible navigation sidebar in theme editor
|
||||
|
||||
### Collection Routes with Filtering & Sorting ✅
|
||||
- `/collections/:slug` routes for category filtering
|
||||
- `/collections/all` for all products
|
||||
- Product sorting (featured, newest, price, name)
|
||||
- Sort parameter preserved in URL across navigation
|
||||
- Category filter pills with sort persistence
|
||||
|
||||
### Header Navigation Accessibility ✅
|
||||
- Current page is not a link (avoids self-links)
|
||||
- Logo links to home except when on home page
|
||||
- `aria-current="page"` with visual underline indicator
|
||||
|
||||
### Enhanced Contact Page ✅
|
||||
- `newsletter_card` component with `:card` and `:inline` variants (shared with footer)
|
||||
- `social_links_card` component with icon + text label cards
|
||||
- Contact form with integrated email link and response time
|
||||
- Reorganized layout: contact form left, info cards right
|
||||
|
||||
### Mobile Bottom Navigation ✅
|
||||
- Fixed bottom tab bar for thumb-friendly mobile navigation
|
||||
- Icons + labels for Home, Shop, About, Contact
|
||||
- Active page has accent-colored background highlight
|
||||
- Shadow above nav for visual separation
|
||||
- Hidden on desktop (≥768px), replaces header nav on mobile
|
||||
- Works in both live shop and theme preview modes
|
||||
|
||||
### Self-Hosted Fonts ✅
|
||||
- Removed Google Fonts external dependency
|
||||
- All 10 typefaces (35 font files, 728KB) served from `/fonts/`
|
||||
- Privacy improvement (no Google tracking)
|
||||
- Performance improvement (no DNS lookup to fonts.googleapis.com)
|
||||
- GDPR compliant (no third-party requests)
|
||||
|
||||
### Admin Access Route ✅
|
||||
- `/admin` redirects to `/admin/theme` (requires auth)
|
||||
- Shop owners can bookmark or type `/admin` to access
|
||||
|
||||
### PageSpeed 100% Score ✅
|
||||
- Self-hosted fonts (removed Google Fonts external dependency)
|
||||
- Production asset pipeline (minified, gzipped CSS/JS)
|
||||
- Proper image dimensions on all `<img>` tags (CLS prevention)
|
||||
- Critical font preloading via `<link rel="preload">`
|
||||
- Lazy loading for below-fold images
|
||||
- `fetchpriority="high"` on hero/priority images via `responsive_image` component
|
||||
- Responsive image variants (AVIF/WebP/JPEG) via image optimization pipeline
|
||||
- Shop CSS reduced from 122KB to 36KB (split bundles)
|
||||
- Cache headers (`max-age=31536000, immutable`) on static assets
|
||||
|
||||
### Image Optimization Pipeline ✅
|
||||
- Oban background job processing for variant generation
|
||||
- Responsive `<picture>` element with AVIF/WebP/JPEG sources
|
||||
- Only generates sizes ≤ source dimensions (no upscaling)
|
||||
- Disk cache for variants (regenerable from DB)
|
||||
- Mix task for mockup optimization
|
||||
- On-demand JPEG fallback generation
|
||||
|
||||
### Themed Form Components ✅
|
||||
- Semantic CSS classes (`.themed-input`, `.themed-button`, `.themed-card`, etc.)
|
||||
- Phoenix components (`shop_input`, `shop_button`, `shop_card`, etc.)
|
||||
- Consistent styling across all shop forms
|
||||
- Reduced repeated inline styles
|
||||
### Rename Project
|
||||
The project is named `simpleshop_theme` but it's now a full storefront. Consider renaming to `simple_shop`.
|
||||
|
||||
Reference in New Issue
Block a user