Skip to content

simpledigitalsolution/Shirts

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 

Repository files navigation

👕 Custom T-Shirt Website — Powered by Printful

A step-by-step guide to building your own custom t-shirt e-commerce website using JavaScript and Printful for on-demand printing and fulfillment.

Who is this guide for?
This guide is written for people who are new to web development or running an online store. Every step is explained in plain language. You don't need to be a programmer — just follow along patiently and you'll have a working t-shirt store!


📋 Table of Contents

  1. What You're Building
  2. What You Need Before You Start
  3. Create Your Printful Account
  4. Set Up Your Development Environment
  5. Create the Project
  6. Project Structure Explained
  7. Connect to the Printful API
  8. Build the Product Catalog Page
  9. Build the Shopping Cart
  10. Set Up Payments (Stripe)
  11. Send Orders to Printful for Fulfillment
  12. Deploy Your Website
  13. Troubleshooting
  14. Helpful Resources

1. What You're Building

You will build a website where customers can:

  • Browse a catalog of custom t-shirts with your designs
  • Select sizes and colors
  • Add items to a shopping cart
  • Pay securely online
  • Receive their order — which Printful automatically prints and ships on your behalf

You never touch inventory. Printful handles all printing, packing, and shipping after a customer places an order. This is called print-on-demand.

How the money flows:

  1. Customer pays your website (via Stripe)
  2. Your website automatically places an order with Printful
  3. Printful charges you their base price (lower than what your customer paid)
  4. You keep the difference as profit

2. What You Need Before You Start

Accounts to Create (all free to start)

Service Why You Need It Link
Printful Handles printing & shipping your shirts printful.com
Stripe Processes customer payments stripe.com
Vercel or Netlify Hosts your website for free vercel.com or netlify.com
GitHub Stores your code (free) github.com

Software to Install on Your Computer

  1. Node.js (version 18 or newer) — the engine that runs your JavaScript code
    → Download from nodejs.org — choose the LTS version
    → After installing, open your terminal and verify: node --version

  2. A code editor — we recommend Visual Studio Code (free)
    → Download from code.visualstudio.com

  3. Git — saves and tracks your code changes
    → Download from git-scm.com

What is a terminal?
A terminal (also called "command prompt" on Windows) is a text-based window where you type commands. On Mac, press Cmd + Space, type "Terminal", and press Enter. On Windows, press the Windows key, type "cmd", and press Enter.


3. Create Your Printful Account

Step 3.1 — Sign Up

  1. Go to printful.com and click Get Started for Free
  2. Create an account with your email

Step 3.2 — Create a Store

  1. In your Printful dashboard, click New Store
  2. Choose Manual Order / API as the store type (this lets your website connect directly to Printful)
  3. Give your store a name (e.g., "My Shirt Store")

Step 3.3 — Add Your Designs and Products

  1. In Printful, go to Product Catalog
  2. Choose a t-shirt style (e.g., Gildan 64000 Unisex Softstyle T-Shirt)
  3. Click Add product → upload your design image (PNG with transparent background works best, at least 300 DPI)
  4. Set your retail price (what customers pay)
  5. Save the product — Printful gives it a Product ID (a number like 12345). Write this down — you'll need it later.

Step 3.4 — Get Your API Key

  1. In Printful, click your profile icon → Settings → API
  2. Click Create Token
  3. Give it a name like "My Website"
  4. Copy the token and store it somewhere safe (like a password manager). You won't be able to see it again.

⚠️ Security warning: Never share your API key publicly or put it directly in your code. We'll store it safely using environment variables in a later step.


4. Set Up Your Development Environment

Step 4.1 — Open Your Terminal

  • Mac: Press Cmd + Space, type "Terminal", press Enter
  • Windows: Press Win key, type "cmd" or "PowerShell", press Enter
  • Linux: Press Ctrl + Alt + T

Step 4.2 — Choose Where to Save Your Project

In your terminal, navigate to where you want to create your project. For example, to put it on your Desktop:

# Mac/Linux
cd ~/Desktop

# Windows
cd C:\Users\YourName\Desktop

5. Create the Project

We'll use Next.js — a popular JavaScript framework that makes it easy to build websites with a built-in backend (so your Printful API key stays secret).

Step 5.1 — Create the App

Run this command in your terminal:

npx create-next-app@latest my-shirt-store

You'll be asked some questions. Answer them like this:

✔ Would you like to use TypeScript? → No
✔ Would you like to use ESLint? → Yes
✔ Would you like to use Tailwind CSS? → Yes
✔ Would you like to use `src/` directory? → No
✔ Would you like to use App Router? → Yes
✔ Would you like to customize the default import alias? → No

Then move into your new project folder:

cd my-shirt-store

Step 5.2 — Install Required Packages

Run each of these commands one at a time:

# Stripe — for payment processing
npm install stripe @stripe/stripe-js @stripe/react-stripe-js

# Axios — for making API calls to Printful
npm install axios

# React Icons — for nice-looking icons
npm install react-icons

Step 5.3 — Create Your Environment File

Your API keys must never be visible to the public. We store them in a special hidden file called .env.local.

In your project folder, create a new file named .env.local:

# Mac/Linux
touch .env.local

# Windows (in PowerShell)
New-Item .env.local

Open .env.local in your code editor and add the following — replacing the placeholder values with your actual keys:

# Printful API Key (from Printful dashboard → Settings → API)
PRINTFUL_API_KEY=your_printful_api_key_here

# Stripe Keys (from Stripe dashboard → Developers → API Keys)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
STRIPE_SECRET_KEY=sk_test_your_secret_key_here

# Your website's URL (use this during development)
NEXT_PUBLIC_BASE_URL=http://localhost:3000

What is NEXT_PUBLIC_?
Variables starting with NEXT_PUBLIC_ are safe to expose in the browser. Variables without it (like STRIPE_SECRET_KEY) stay hidden on the server only.

Make sure .env.local is listed in your .gitignore file (Next.js adds it automatically) so it's never uploaded to GitHub.


6. Project Structure Explained

After setup, your project will look like this. Here's what each part does:

my-shirt-store/
│
├── app/                        ← All your website pages live here
│   ├── page.js                 ← Home page (your product catalog)
│   ├── cart/
│   │   └── page.js             ← Shopping cart page
│   ├── checkout/
│   │   └── page.js             ← Checkout / payment page
│   └── api/                    ← Backend code (hidden from users)
│       ├── products/
│       │   └── route.js        ← Fetches products from Printful
│       ├── checkout/
│       │   └── route.js        ← Creates Stripe payment session
│       └── webhook/
│           └── route.js        ← Receives payment confirmation & creates Printful order
│
├── components/                 ← Reusable pieces of your site
│   ├── ProductCard.js          ← Displays a single product
│   ├── Cart.js                 ← Cart sidebar component
│   └── Navbar.js               ← Top navigation bar
│
├── context/
│   └── CartContext.js          ← Manages cart state across pages
│
├── public/                     ← Images and static files
│
├── .env.local                  ← Your secret API keys (never commit this!)
├── next.config.js              ← Next.js configuration
└── package.json                ← Project dependencies list

7. Connect to the Printful API

Step 7.1 — Create the API Folders

In your terminal (from inside my-shirt-store):

mkdir -p app/api/products app/api/checkout app/api/webhook components context

Step 7.2 — Create the Products API Route

This file asks Printful for your product list and sends it to your website. Create app/api/products/route.js:

import axios from 'axios';
import { NextResponse } from 'next/server';

// This runs on the server - your API key is never exposed to visitors
export async function GET() {
  try {
    const response = await axios.get('https://api.printful.com/store/products', {
      headers: {
        Authorization: `Bearer ${process.env.PRINTFUL_API_KEY}`,
      },
    });

    // Return the list of products from Printful
    return NextResponse.json(response.data.result);
  } catch (error) {
    console.error('Error fetching products from Printful:', error.message);
    return NextResponse.json(
      { error: 'Failed to load products. Please try again later.' },
      { status: 500 }
    );
  }
}

Step 7.3 — Create the Product Detail Route

Create app/api/products/[id]/route.js to fetch a single product with all its variants (sizes/colors):

import axios from 'axios';
import { NextResponse } from 'next/server';

export async function GET(request, { params }) {
  const { id } = params;

  try {
    const response = await axios.get(`https://api.printful.com/store/products/${id}`, {
      headers: {
        Authorization: `Bearer ${process.env.PRINTFUL_API_KEY}`,
      },
    });

    return NextResponse.json(response.data.result);
  } catch (error) {
    console.error(`Error fetching product ${id}:`, error.message);
    return NextResponse.json(
      { error: 'Product not found.' },
      { status: 404 }
    );
  }
}

8. Build the Product Catalog Page

Step 8.1 — Create the Cart Context

The cart context lets every page on your site access the shopping cart. Create context/CartContext.js:

'use client';
import { createContext, useContext, useState } from 'react';

// Create the cart "bucket" that holds cart data
const CartContext = createContext();

export function CartProvider({ children }) {
  const [cartItems, setCartItems] = useState([]);

  // Add an item to the cart
  function addToCart(product) {
    setCartItems((prev) => {
      const existing = prev.find((item) => item.variantId === product.variantId);
      if (existing) {
        // If already in cart, increase quantity
        return prev.map((item) =>
          item.variantId === product.variantId
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      }
      return [...prev, { ...product, quantity: 1 }];
    });
  }

  // Remove an item from the cart
  function removeFromCart(variantId) {
    setCartItems((prev) => prev.filter((item) => item.variantId !== variantId));
  }

  // Calculate the total price
  const cartTotal = cartItems.reduce(
    (total, item) => total + item.price * item.quantity,
    0
  );

  return (
    <CartContext.Provider value={{ cartItems, addToCart, removeFromCart, cartTotal }}>
      {children}
    </CartContext.Provider>
  );
}

// Custom hook to use the cart anywhere in the app
export function useCart() {
  return useContext(CartContext);
}

Step 8.2 — Update the Root Layout

Open app/layout.js and wrap the app with the cart provider:

import { CartProvider } from '@/context/CartContext';
import './globals.css';

export const metadata = {
  title: 'My Shirt Store',
  description: 'Custom t-shirts printed on demand',
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <CartProvider>
          {children}
        </CartProvider>
      </body>
    </html>
  );
}

Step 8.3 — Create the Product Card Component

Create components/ProductCard.js:

'use client';
import { useCart } from '@/context/CartContext';

export default function ProductCard({ product }) {
  const { addToCart } = useCart();

  // Handle adding item to cart
  function handleAddToCart() {
    addToCart({
      variantId: product.id,
      name: product.name,
      price: product.retail_price,
      image: product.thumbnail_url,
    });
  }

  return (
    <div className="border rounded-lg p-4 shadow hover:shadow-lg transition">
      {/* Product image */}
      <img
        src={product.thumbnail_url}
        alt={product.name}
        className="w-full h-64 object-cover rounded mb-4"
      />

      {/* Product name and price */}
      <h2 className="text-lg font-semibold">{product.name}</h2>
      <p className="text-gray-600 mt-1">${product.retail_price}</p>

      {/* Add to cart button */}
      <button
        onClick={handleAddToCart}
        className="mt-4 w-full bg-black text-white py-2 rounded hover:bg-gray-800 transition"
      >
        Add to Cart
      </button>
    </div>
  );
}

Step 8.4 — Build the Home Page

Replace the contents of app/page.js with:

'use client';
import { useEffect, useState } from 'react';
import ProductCard from '@/components/ProductCard';
import { useCart } from '@/context/CartContext';
import Link from 'next/link';

export default function HomePage() {
  const [products, setProducts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const { cartItems } = useCart();

  // Load products when the page opens
  useEffect(() => {
    fetch('/api/products')
      .then((res) => res.json())
      .then((data) => {
        setProducts(data);
        setLoading(false);
      })
      .catch(() => {
        setError('Could not load products. Please refresh the page.');
        setLoading(false);
      });
  }, []);

  return (
    <main className="max-w-6xl mx-auto px-4 py-8">
      {/* Header */}
      <div className="flex justify-between items-center mb-8">
        <h1 className="text-3xl font-bold">Our T-Shirts</h1>
        <Link href="/cart" className="relative">
          🛒 Cart
          {cartItems.length > 0 && (
            <span className="ml-1 bg-red-500 text-white text-xs rounded-full px-2 py-0.5">
              {cartItems.length}
            </span>
          )}
        </Link>
      </div>

      {/* Loading state */}
      {loading && <p className="text-center text-gray-500">Loading products...</p>}

      {/* Error state */}
      {error && <p className="text-center text-red-500">{error}</p>}

      {/* Product grid */}
      {!loading && !error && (
        <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
          {products.map((product) => (
            <ProductCard key={product.id} product={product} />
          ))}
        </div>
      )}
    </main>
  );
}

9. Build the Shopping Cart

Create app/cart/page.js:

'use client';
import { useCart } from '@/context/CartContext';
import Link from 'next/link';

export default function CartPage() {
  const { cartItems, removeFromCart, cartTotal } = useCart();

  // Show message if cart is empty
  if (cartItems.length === 0) {
    return (
      <main className="max-w-2xl mx-auto px-4 py-16 text-center">
        <h1 className="text-2xl font-bold mb-4">Your cart is empty</h1>
        <Link href="/" className="text-blue-600 underline">
          ← Back to shop
        </Link>
      </main>
    );
  }

  return (
    <main className="max-w-2xl mx-auto px-4 py-8">
      <h1 className="text-2xl font-bold mb-6">Shopping Cart</h1>

      {/* List each cart item */}
      <ul className="space-y-4">
        {cartItems.map((item) => (
          <li key={item.variantId} className="flex items-center gap-4 border-b pb-4">
            <img src={item.image} alt={item.name} className="w-20 h-20 object-cover rounded" />
            <div className="flex-1">
              <p className="font-medium">{item.name}</p>
              <p className="text-gray-500">Qty: {item.quantity}</p>
              <p className="text-gray-500">${(item.price * item.quantity).toFixed(2)}</p>
            </div>
            <button
              onClick={() => removeFromCart(item.variantId)}
              className="text-red-500 hover:text-red-700"
            >
              Remove
            </button>
          </li>
        ))}
      </ul>

      {/* Total and checkout button */}
      <div className="mt-6 text-right">
        <p className="text-xl font-bold">Total: ${cartTotal.toFixed(2)}</p>
        <Link
          href="/checkout"
          className="mt-4 inline-block bg-black text-white px-8 py-3 rounded hover:bg-gray-800 transition"
        >
          Proceed to Checkout →
        </Link>
      </div>
    </main>
  );
}

10. Set Up Payments (Stripe)

Step 10.1 — Create a Stripe Account

  1. Go to stripe.com and create a free account
  2. Go to Developers → API Keys
  3. Copy your Publishable key (starts with pk_test_) and Secret key (starts with sk_test_)
  4. Add both to your .env.local file (see Step 5.3)

Step 10.2 — Create the Checkout API Route

Create app/api/checkout/route.js:

import Stripe from 'stripe';
import { NextResponse } from 'next/server';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export async function POST(request) {
  const { cartItems } = await request.json();

  try {
    // Build the list of items Stripe will show on its checkout page
    const lineItems = cartItems.map((item) => ({
      price_data: {
        currency: 'usd',
        product_data: {
          name: item.name,
          images: [item.image],
        },
        unit_amount: Math.round(item.price * 100), // Stripe uses cents
      },
      quantity: item.quantity,
    }));

    // Create the Stripe checkout session
    const session = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      line_items: lineItems,
      mode: 'payment',
      // Where to redirect after successful payment
      success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
      // Where to redirect if customer cancels
      cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/cart`,
      // Save cart items so the webhook can use them later
      metadata: {
        cartItems: JSON.stringify(cartItems),
      },
    });

    return NextResponse.json({ url: session.url });
  } catch (error) {
    console.error('Stripe error:', error.message);
    return NextResponse.json(
      { error: 'Payment setup failed. Please try again.' },
      { status: 500 }
    );
  }
}

Step 10.3 — Create the Checkout Page

Create app/checkout/page.js:

'use client';
import { useCart } from '@/context/CartContext';
import { useState } from 'react';

export default function CheckoutPage() {
  const { cartItems, cartTotal } = useCart();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  async function handleCheckout() {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch('/api/checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ cartItems }),
      });

      const data = await response.json();

      if (data.url) {
        // Redirect customer to Stripe's secure payment page
        window.location.href = data.url;
      } else {
        setError('Could not start checkout. Please try again.');
      }
    } catch {
      setError('An unexpected error occurred. Please try again.');
    } finally {
      setLoading(false);
    }
  }

  return (
    <main className="max-w-2xl mx-auto px-4 py-8">
      <h1 className="text-2xl font-bold mb-6">Checkout</h1>

      <div className="border rounded p-4 mb-6">
        <h2 className="font-semibold mb-2">Order Summary</h2>
        {cartItems.map((item) => (
          <div key={item.variantId} className="flex justify-between text-sm py-1">
            <span>{item.name} × {item.quantity}</span>
            <span>${(item.price * item.quantity).toFixed(2)}</span>
          </div>
        ))}
        <div className="border-t mt-2 pt-2 font-bold flex justify-between">
          <span>Total</span>
          <span>${cartTotal.toFixed(2)}</span>
        </div>
      </div>

      {error && <p className="text-red-500 mb-4">{error}</p>}

      <button
        onClick={handleCheckout}
        disabled={loading}
        className="w-full bg-black text-white py-3 rounded hover:bg-gray-800 transition disabled:opacity-50"
      >
        {loading ? 'Redirecting to payment...' : 'Pay with Card →'}
      </button>
    </main>
  );
}

11. Send Orders to Printful for Fulfillment

When a customer successfully pays, Stripe sends your server a "webhook" — a notification saying "payment successful!" Your server then automatically places the order with Printful.

Step 11.1 — Create the Webhook Handler

Create app/api/webhook/route.js:

import Stripe from 'stripe';
import axios from 'axios';
import { NextResponse } from 'next/server';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export async function POST(request) {
  const payload = await request.text();
  const signature = request.headers.get('stripe-signature');

  let event;

  try {
    // Verify the webhook actually came from Stripe (security check)
    event = stripe.webhooks.constructEvent(
      payload,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (error) {
    console.error('Webhook signature verification failed:', error.message);
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
  }

  // Only act when payment is confirmed complete
  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;

    // Retrieve customer shipping address from Stripe
    const stripeSession = await stripe.checkout.sessions.retrieve(session.id, {
      expand: ['shipping_details'],
    });

    const shipping = stripeSession.shipping_details;
    const cartItems = JSON.parse(session.metadata.cartItems);

    // Build the order for Printful
    const printfulOrder = {
      recipient: {
        name: shipping?.name || 'Customer',
        address1: shipping?.address?.line1 || '',
        address2: shipping?.address?.line2 || '',
        city: shipping?.address?.city || '',
        state_code: shipping?.address?.state || '',
        country_code: shipping?.address?.country || 'US',
        zip: shipping?.address?.postal_code || '',
      },
      items: cartItems.map((item) => ({
        sync_variant_id: item.variantId, // Printful product variant ID
        quantity: item.quantity,
      })),
    };

    try {
      // Send the order to Printful — they handle printing and shipping
      await axios.post('https://api.printful.com/orders', printfulOrder, {
        headers: {
          Authorization: `Bearer ${process.env.PRINTFUL_API_KEY}`,
        },
      });
      console.log('Order successfully sent to Printful for order:', session.id);
    } catch (printfulError) {
      console.error('Failed to create Printful order:', printfulError.message);
      // In production, you would want to alert yourself here (e.g., send an email)
    }
  }

  return NextResponse.json({ received: true });
}

Step 11.2 — Enable Shipping Address Collection in Stripe

Update your app/api/checkout/route.js to collect the customer's shipping address:

// Add this inside stripe.checkout.sessions.create({ ... })
shipping_address_collection: {
  allowed_countries: ['US', 'CA', 'GB', 'AU'], // Countries you ship to
},

Step 11.3 — Set Up Your Stripe Webhook

  1. Go to Stripe Dashboard → Developers → Webhooks
  2. Click Add endpoint
  3. Enter your website's webhook URL: https://your-domain.com/api/webhook
  4. Under Select events, choose checkout.session.completed
  5. Click Add endpoint
  6. Copy the Signing secret (starts with whsec_)
  7. Add it to .env.local:
    STRIPE_WEBHOOK_SECRET=whsec_your_signing_secret_here
    

Testing webhooks locally:
While developing, use the Stripe CLI to forward events to your local machine:

stripe listen --forward-to localhost:3000/api/webhook

12. Deploy Your Website

Step 12.1 — Push Your Code to GitHub

  1. Create a new repository on github.com
  2. Follow GitHub's instructions to push your code
  3. Make absolutely sure .env.local is in your .gitignore — never upload your secrets to GitHub!

Step 12.2 — Deploy to Vercel (Recommended, Free)

Vercel is made by the same team as Next.js and makes deployment very easy.

  1. Go to vercel.com and sign up with your GitHub account
  2. Click New Project → Import your GitHub repository
  3. Add your environment variables:
    • Click Environment Variables
    • Add each key-value pair from your .env.local file
    • Update NEXT_PUBLIC_BASE_URL to your actual Vercel URL (e.g., https://my-shirt-store.vercel.app)
  4. Click Deploy

Vercel will automatically redeploy your site whenever you push new code to GitHub.

Step 12.3 — Update Stripe Webhook URL

After deployment, go back to your Stripe webhook settings and update the URL to your live domain:

https://your-actual-domain.vercel.app/api/webhook

Step 12.4 — Switch to Live Stripe Keys

When you're ready to accept real payments:

  1. In Stripe, go to Developers → API Keys and switch to Live mode (toggle at top of page)
  2. Copy your live keys (they start with pk_live_ and sk_live_)
  3. Update your environment variables on Vercel with the live keys
  4. Create a new live Stripe webhook and update STRIPE_WEBHOOK_SECRET

13. Troubleshooting

"Products are not loading"

  • Check that your PRINTFUL_API_KEY is set correctly in .env.local
  • Verify you created products in Printful (Catalog → Your Products)
  • Check your browser's Developer Tools console for error messages (press F12)

"Payment is failing"

  • Make sure you're using test keys during development (pk_test_ / sk_test_)
  • Use Stripe's test card number: 4242 4242 4242 4242 (any future expiry, any CVC)
  • Check the Stripe Dashboard → Logs for detailed error information

"Orders are not appearing in Printful"

  • Check that your Stripe webhook is set up and active
  • During local development, use the Stripe CLI to simulate webhook events:
    stripe trigger checkout.session.completed
  • Verify STRIPE_WEBHOOK_SECRET matches the one in your Stripe Dashboard

"Changes to my code aren't showing up"

  • Stop the development server (press Ctrl + C) and restart it: npm run dev
  • Hard-refresh your browser: Ctrl + Shift + R (Windows) or Cmd + Shift + R (Mac)

"How do I run the site locally for testing?"

npm run dev

Then open your browser and go to: http://localhost:3000


14. Helpful Resources

Resource Link
Printful API Documentation developers.printful.com
Printful Product Catalog printful.com/products
Stripe Documentation stripe.com/docs
Stripe Testing Guide stripe.com/docs/testing
Next.js Documentation nextjs.org/docs
Tailwind CSS (for styling) tailwindcss.com/docs
Vercel Deployment Guide vercel.com/docs

🎉 Congratulations!

You now have everything you need to launch your own custom t-shirt store. Here's a quick recap of what you've built:

  • ✅ A product catalog that automatically syncs with your Printful designs
  • ✅ A shopping cart that tracks items and quantities
  • ✅ A secure checkout powered by Stripe
  • ✅ Automatic order fulfillment — Printful prints and ships for you

Suggested next steps:

  • Add more shirt designs in Printful and they'll appear on your store automatically
  • Customize the look and feel using Tailwind CSS classes
  • Add a custom domain name through Vercel's settings
  • Set up email notifications for customers using Resend or SendGrid

Built with ❤️ using Next.js, Printful, and Stripe.

About

shirt site repo

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors