diff --git a/.dockerignore b/.dockerignore
index 4c7db2f..f6c0283 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,8 +1,11 @@
-Dockerfile
-.dockerignore
node_modules
npm-debug.log
-README.md
+pnpm-debug.log
.next
+.env*
docker
-.git
\ No newline at end of file
+.git
+.gitignore
+README.md
+*.swp
+.vscode
\ No newline at end of file
diff --git a/README.md b/README.md
index 1394a4d..8152fc4 100644
--- a/README.md
+++ b/README.md
@@ -4,22 +4,14 @@ Full-stack e-commerce store for developer laptop stickers. Automated with dropsh
https://github.com/user-attachments/assets/d32962b3-6aa8-401c-ab43-440fff3e31cc
-
-
## 🎯 Project Overview
-
-
-- designed autonomous and scalable e-commerce store
-- enabled both guest checkout and no-password login for seamless checkout experience (JSON-Web-Token (JWT))
-- designed an accessible, responsive, and performant UI with Next.js and TailwindCSS
-- utilized open-source SDKs to integrate third-party APIs
-- reduced initial load time to 1.2secs
-- generated product images with OpenCV python script
-- optimized load times and SEO with server-side rendering
-- avoided complex database setup with JSON product catalog and GraphQL for easy migration to headless CMS
-- secured checkout payments with stripe forms and bot detection
-- load test, >95% success rate with <300ms response for up to XXX users peak traffic
+- Developed an autonomous, full-stack **e-commerce** store using **Next.js** and **TypeScript**, supporting automated dropshipping via **Printify SDK**
+- Implemented **passwordless authentication** using JSON Web Tokens (JWT) for a secure, seamless checkout experience
+- Integrated **Stripe SDK** with embedded payment forms and Webhooks for secure, automated payment processing, including bot detection for fraud prevention
+- Enhanced page load speed and SEO with **server-side rendering**, achieving sub-2.5s initial load times
+- Scaled to handle 1,500+ monthly active users, supporting up to 50 requests per second during peak traffic
+- Conducted load tests to validate system reliability, achieving a >95% success rate with **P90 response times** between under 300 ms
## 🛠️ Built With
@@ -40,6 +32,10 @@ https://github.com/user-attachments/assets/d32962b3-6aa8-401c-ab43-440fff3e31cc
![Account Page Feature](./.github/feature-view-orders.png) _View orders, download receipts, and track shipping status._
+## 🏗️ System Diagram
+
+
+
## 💻 Local Development
#### Prerequisites
@@ -70,6 +66,8 @@ npm run dev
# visit http://locahost:3000
```
+## 🐋 Docker
+
#### Local Docker Container
```sh
@@ -77,16 +75,17 @@ stripe login
stripe listen --forward-to localhost:3000/api/v1/webhook/checkout
# *open separate terminal*
-cp .env.template .env.development
-docker-compose -f ./docker/docker-compose.dev.yml --env-file .env.development up --build
+cp .env.template .env.production
+docker-compose -f ./docker/docker-compose.dev.yml --env-file .env.production up --build
# visit http://locahost:3000
```
-#### Production Build
+#### Production Docker Build
```sh
cp .env.template .env.production
-npm run build
+docker-compose -f ./docker/docker-compose.prod.yml --env-file .env.production up --build
+# visit http://localhost
```
## License
diff --git a/docker/Dockerfile b/docker/Dockerfile
index bb57d92..81b554a 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,31 +1,23 @@
FROM node:20.16-alpine3.19 AS base
-# 1. Install dependencies and build
-FROM --platform=$BUILDPLATFORM base AS builder
+# enable pnpm - https://pnpm.io/docker
+ENV PNPM_HOME="/pnpm"
+ENV PATH="$PNPM_HOME:$PATH"
+RUN corepack enable
# source: https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine
RUN apk add --no-cache libc6-compat
+# 1. Install dependencies and build
+FROM --platform=$BUILDPLATFORM base AS builder
+
WORKDIR /app
-# Install dependencies based on the preferred package manager
-# COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
-# RUN \
-# if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
-# elif [ -f package-lock.json ]; then npm ci; \
-# elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i; \
-# else echo "Lockfile not found." && exit 1; \
-# fi
-
-# Install dependencies with pnpm (with cache)
-RUN npm install -g pnpm
+# Copy lockfile and install dependencies
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
-# Copy source code and environment files
COPY . .
-COPY .env.production .env.development ./
-
RUN pnpm build
# 2. Production image, copy all the files and run
@@ -40,7 +32,7 @@ ENV NODE_ENV=production \
PORT=3000 \
HOSTNAME="0.0.0.0"
-COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
+COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone /app
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
-CMD ["node", ".next/standalone/server.js"]
\ No newline at end of file
+CMD ["node", "./server.js"]
\ No newline at end of file
diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml
index d164fa5..f8ce43c 100644
--- a/docker/docker-compose.dev.yml
+++ b/docker/docker-compose.dev.yml
@@ -4,4 +4,7 @@ services:
context: ../
dockerfile: docker/Dockerfile
ports:
- - "3000:3000"
\ No newline at end of file
+ - "3000:3000"
+ env_file:
+ - ../.env.production
+ restart: always
\ No newline at end of file
diff --git a/next.config.mjs b/next.config.js
similarity index 56%
rename from next.config.mjs
rename to next.config.js
index bf1cf3c..74fad98 100644
--- a/next.config.mjs
+++ b/next.config.js
@@ -1,9 +1,13 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
+ productionBrowserSourceMaps: false,
+ trailingSlash: false,
+ experimental: {
+ optimizePackageImports: ['react-icons'],
+ },
swcMinify: true,
output: 'standalone', // for Docker
- trailingSlash: false,
};
-export default nextConfig;
+module.exports = nextConfig
\ No newline at end of file
diff --git a/package.json b/package.json
index 9dae666..8fd2428 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,6 @@
"react-icons": "^5.3.0",
"sharp": "0.32.6",
"stripe": "^16.9.0",
- "tailwindcss-animate": "^1.0.7",
"use-shopping-cart": "^3.2.0",
"winston": "^3.15.0"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 27587a2..8dd00c3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -47,9 +47,6 @@ importers:
stripe:
specifier: ^16.9.0
version: 16.12.0
- tailwindcss-animate:
- specifier: ^1.0.7
- version: 1.0.7(tailwindcss@3.4.14)
use-shopping-cart:
specifier: ^3.2.0
version: 3.2.0(react@18.3.1)(redux@4.2.1)
@@ -2066,11 +2063,6 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
- tailwindcss-animate@1.0.7:
- resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==}
- peerDependencies:
- tailwindcss: '>=3.0.0 || insiders'
-
tailwindcss@3.4.14:
resolution: {integrity: sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==}
engines: {node: '>=14.0.0'}
@@ -4468,10 +4460,6 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
- tailwindcss-animate@1.0.7(tailwindcss@3.4.14):
- dependencies:
- tailwindcss: 3.4.14
-
tailwindcss@3.4.14:
dependencies:
'@alloc/quick-lru': 5.2.0
diff --git a/src/app/account/page.tsx b/src/app/account/page.tsx
index a4b5931..752d563 100644
--- a/src/app/account/page.tsx
+++ b/src/app/account/page.tsx
@@ -11,7 +11,6 @@ export default async function AccountPage() {
const { email, error } = verifyJwt(token);
if (error) return notFound();
- console.log(email);
const orders = await getOrders(email as string);
return (
diff --git a/src/app/api/v1/auth/logout/route.ts b/src/app/api/v1/auth/logout/route.ts
index 7eb1b2d..f51d75a 100644
--- a/src/app/api/v1/auth/logout/route.ts
+++ b/src/app/api/v1/auth/logout/route.ts
@@ -7,7 +7,7 @@ export const POST = async () => {
response.cookies.set('authToken', '', {
httpOnly: true,
- secure: process.env.NODE_ENV === 'production',
+ secure: true,
maxAge: 0,
path: '/',
});
diff --git a/src/app/api/v1/status/route.ts b/src/app/api/v1/status/route.ts
new file mode 100644
index 0000000..e0db66a
--- /dev/null
+++ b/src/app/api/v1/status/route.ts
@@ -0,0 +1,22 @@
+import { NextResponse } from 'next/server';
+import logger from '@/lib/logger';
+import { checkPrintifyStatus } from '@/lib/printify';
+import { checkStripeStatus } from '@/lib/stripe';
+
+export async function GET() {
+ try {
+ const printifyStatus = await checkPrintifyStatus();
+ const stripeStatus = await checkStripeStatus();
+ const status = printifyStatus === "operational" && stripeStatus === "operational" ? "operational" : "degraded";
+
+ if (status === "degraded") {
+ logger.error('[Status] report:', { status, printifyStatus, stripeStatus });
+ }
+
+ return NextResponse.json({ status });
+ } catch (error) {
+ logger.error('[Status] Error checking status', { error });
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
+
diff --git a/src/app/order-confirmation/page.tsx b/src/app/order-confirmation/page.tsx
index c0e44f5..ef2caf9 100644
--- a/src/app/order-confirmation/page.tsx
+++ b/src/app/order-confirmation/page.tsx
@@ -2,6 +2,8 @@ import OrderConfirmation from '@/components/OrderConfirmation';
import { stripe } from '@/lib/stripe';
import { notFound } from 'next/navigation';
+export const dynamic = 'force-dynamic'; // Dynamic server-side rendering
+
async function getSession(sessionId: string) {
try {
const session = await stripe.checkout.sessions.retrieve(sessionId!);
diff --git a/src/lib/printify.ts b/src/lib/printify.ts
index c8bf08a..be53a76 100644
--- a/src/lib/printify.ts
+++ b/src/lib/printify.ts
@@ -22,17 +22,15 @@ const PRINTIFY_VARIANT_IDS = {
[STICKER_SIZES.FOUR_BY_FOUR_IN]: 95745,
};
-// export const formatCartItemsForPrintify = (cartItems: CartItem[]): PrintifyLineItem[] => {
-// return cartItems.map(cartItem => ({
-// print_provider_id: PRINTIFY_PRINT_PROVIDER_ID,
-// blueprint_id: PRINTIFY_BLUEPRINT_ID,
-// variant_id: PRINTIFY_VARIANT_IDS[cartItem.product_data.size],
-// print_areas: {
-// front: retrieveStickerPNGFileUrl(cartItem.product_data.productId),
-// },
-// quantity: cartItem.quantity,
-// }));
-// };
+export async function checkPrintifyStatus() {
+ try {
+ await printify.shops.list();
+ return "operational";
+ } catch (error) {
+ logger.error('[Status] Printify status check failed', { error });
+ return "degraded";
+ }
+}
export const formatCartItemsForPrintify = (lineItems: StripeType.LineItem[]): PrintifyLineItem[] => {
return lineItems.map(item => ({
diff --git a/src/lib/stripe.ts b/src/lib/stripe.ts
index 8279e71..6c1f6f4 100644
--- a/src/lib/stripe.ts
+++ b/src/lib/stripe.ts
@@ -15,6 +15,16 @@ export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, {
},
});
+export async function checkStripeStatus() {
+ try {
+ await stripe.accounts.list({ limit: 1 });
+ return "operational";
+ } catch (error) {
+ logger.error('[Status] Stripe status check failed', { error });
+ return "degraded";
+ }
+}
+
export const formatCartItemsForStripe = (cartItems: CartItem[]): Stripe.Checkout.SessionCreateParams.LineItem[] => {
return cartItems.map(cartItem => {
const lineItem = {