From 10d4bcb715c3ebc9af9f47a5f1d41e977de05fe4 Mon Sep 17 00:00:00 2001 From: GustavH Date: Mon, 2 Dec 2024 15:53:38 +0100 Subject: [PATCH 1/2] chore: remove production Docker Compose configuration --- docker-compose.prod.yml | 96 ----------------------------------------- 1 file changed, 96 deletions(-) delete mode 100644 docker-compose.prod.yml diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index 9ea94c5..0000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,96 +0,0 @@ -services: - watchtower: - image: containrrr/watchtower - command: - - "--label-enable" - - "--interval" - - "30" - - "--rolling-restart" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - reverse-proxy: - image: traefik:v3.1 - command: - - "--providers.docker" - - "--providers.docker.exposedbydefault=false" - - "--entryPoints.websecure.address=:443" - - "--certificatesresolvers.myresolver.acme.tlschallenge=true" - - "--certificatesresolvers.myresolver.acme.email=commercify@zenfulcode.com" - - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json" - - "--entrypoints.web.address=:80" - - "--entrypoints.web.http.redirections.entrypoint.to=websecure" - - "--entrypoints.web.http.redirections.entrypoint.scheme=https" - ports: - - "80:80" - - "8080:8080" - - "443:443" - volumes: - - letsencrypt:/letsencrypt - - /var/run/docker.sock:/var/run/docker.sock - mysql-db: - image: docker.io/bitnami/mysql:8.4 - container_name: mysql-db - env_file: .env - environment: - ALLOW_EMPTY_PASSWORD: "no" - MYSQL_DATABASE: commercifydb - MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} - MYSQL_USER: ${MYSQL_USER} - MYSQL_PASSWORD: ${MYSQL_PASSWORD} - expose: - - 3306 - volumes: - - mysql-data:/bitnami/mysql/data - # - ./mysql-init-scripts:/docker-entrypoint-initdb.d - healthcheck: - test: ["CMD", "/opt/bitnami/scripts/mysql/healthcheck.sh"] - interval: 15s - timeout: 5s - retries: 6 - - commercifyweb: - image: ghcr.io/zenfulcode/commercifyweb:dev - env_file: .env - labels: - - "traefik.enable=true" - - "traefik.http.routers.commercify.rule=Host(`commercify.app`)" - - "traefik.http.routers.commercify.entrypoints=websecure" - - "traefik.http.routers.commercify.tls.certresolver=myresolver" - - "com.centurylinklabs.watchtower.enable=true" - environment: - - NEXT_PUBLIC_COMMERCIFY_API_URL=https://commercify.app:6091/api/v1 - # deploy: - # mode: replicated - # replicas: 3 - depends_on: - - commercify - - commercify: - image: ghcr.io/zenfulcode/commercify:dev - env_file: .env - labels: - - "traefik.enable=true" - - "traefik.http.routers.commercify-api.rule=Host(`api.commercify.app`)" - - "traefik.http.routers.commercify-api.entrypoints=websecure" - - "traefik.http.routers.commercify-api.tls.certresolver=myresolver" - - "com.centurylinklabs.watchtower.enable=true" - environment: - - SPRING_PROFILES_ACTIVE=docker - - SPRING_DATASOURCE_URL=${SPRING_DATASOURCE_URL} - - SPRING_DATASOURCE_USERNAME=${SPRING_DATASOURCE_USERNAME} - - SPRING_DATASOURCE_PASSWORD=${SPRING_DATASOURCE_PASSWORD} - - STRIPE_TEST_SECRET=${STRIPE_TEST_SECRET} - - JWT_SECRET_KEY=${JWT_SECRET_KEY} - expose: - - 6091 - # deploy: - # mode: replicated - # replicas: 3 - depends_on: - mysql-db: - condition: service_healthy - -volumes: - letsencrypt: - mysql-data: - driver: local From 05267ae1db9c64d5e6e3fa4f08556ce711edf920 Mon Sep 17 00:00:00 2001 From: GustavH Date: Mon, 2 Dec 2024 17:44:28 +0100 Subject: [PATCH 2/2] Updates Docker configuration and removes unused code Updates .dockerignore to include additional files and directories Updates .env.example with new backend configuration options Removes unused code in AuthenticationStep and PaymentStep components Deletes unused files: ProductDetails.tsx and page.tsx Updates AuthContext to fix isLoading initial state --- .dockerignore | 21 +++- .env.example | 8 +- .gitignore | 4 +- Dockerfile | 13 ++ docker-compose.yml | 71 +++++------ .../_components/AuthenticationStep.tsx | 2 - .../checkout/_components/PaymentStep.tsx | 8 +- src/app/(client)/page.tsx | 14 --- src/app/(client)/products/[id]/page.tsx | 14 --- .../page.tsx} | 113 +++++++++++++++--- src/context/AuthContext.tsx | 2 +- src/services/authService.ts | 4 +- src/types/apiBase.ts | 25 +++- 13 files changed, 195 insertions(+), 104 deletions(-) delete mode 100644 src/app/(client)/products/[id]/page.tsx rename src/app/(client)/products/{[id]/_components/ProductDetails.tsx => [productId]/page.tsx} (56%) diff --git a/.dockerignore b/.dockerignore index cd12b7c..d561215 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,10 +1,21 @@ +node_modules +dist +build +.env +.env.local Dockerfile +docker-compose.yml +docker-compose.prod.yml +.git +.gitignore +.vscode +.idea +*.log +*.tsbuildinfo +coverage .dockerignore -node_modules -npm-debug.log README.md .next -.git .env.example -mysql-init-scripts -docker-compose.yml \ No newline at end of file +.es +.github \ No newline at end of file diff --git a/.env.example b/.env.example index 47ad158..38834f6 100644 --- a/.env.example +++ b/.env.example @@ -9,10 +9,12 @@ SMTP_PASSWORD= EMAIL_DEV=dev@email.com # Backend Configuration -# use "host.docker.internal" as host if you are connecting to a MySQL database running on the host machine +# use "host.docker.internal" as host if you are connecting to +# a MySQL database running on the host machine while the backend +# is running in a Docker container SPRING_DATASOURCE_URL=jdbc:mysql://localhost:3306/commercifydb?createDatabaseIfNotExist=true -SPRING_DATASOURCE_USERNAME=root -SPRING_DATASOURCE_PASSWORD=Password1234! +SPRING_DATASOURCE_USERNAME= +SPRING_DATASOURCE_PASSWORD= STRIPE_TEST_SECRET=sk_test_ STRIPE_SECRET_WEBHOOK=whsec_ STRIPE_WEBHOOK_ENDPOINT=we_ diff --git a/.gitignore b/.gitignore index e44db61..b4f1c55 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts .env -.vscode \ No newline at end of file +.vscode + +docker-compose.prod.yml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index aacaf5b..ad63f31 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,18 @@ FROM build AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . + +ARG NODE_ENV='production' +ENV NODE_ENV=$NODE_ENV + +ARG NEXT_PUBLIC_COMMERCIFY_API_URL="https://dev.commercify.app/api/v1" +ENV NEXT_PUBLIC_COMMERCIFY_API_URL=$NEXT_PUBLIC_COMMERCIFY_API_URL + +ARG NEXT_PUBLIC_DEV_COMMERCIFY_API_URL="http://localhost:6091/api/v1" +ENV NEXT_PUBLIC_DEV_COMMERCIFY_API_URL=$NEXT_PUBLIC_DEV_COMMERCIFY_API_URL + +RUN echo "Application is running in ${NODE_ENV} mode" + RUN npm run build # Production image, copy all the files and run next @@ -43,4 +55,5 @@ ENV PORT=3000 # server.js is created by next build from the standalone output # https://nextjs.org/docs/pages/api-reference/next-config-js/output ENV HOSTNAME="0.0.0.0" + CMD ["node", "server.js"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 082451b..fae2321 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,49 +1,52 @@ services: - mysql-db: - image: docker.io/bitnami/mysql:8.4 - container_name: mysql-db + commercify-web: + # image: ghcr.io/zenfulcode/commercifyweb:dev + container_name: commercify-web + build: + context: . + args: + - NODE_ENV=production + - NEXT_PUBLIC_COMMERCIFY_API_URL=http://localhost:6091/api/v1 env_file: .env environment: - ALLOW_EMPTY_PASSWORD: "no" - MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} - MYSQL_USER: ${MYSQL_USER} - MYSQL_PASSWORD: ${MYSQL_PASSWORD} + - NODE_ENV=${NODE_ENV} + - NEXT_PUBLIC_COMMERCIFY_API_URL=${NEXT_PUBLIC_COMMERCIFY_API_URL} + - NEXT_PUBLIC_DEV_COMMERCIFY_API_URL=${NEXT_PUBLIC_DEV_COMMERCIFY_API_URL} + - SMTP_HOST=${SMTP_HOST} + - SMTP_PORT=${SMTP_PORT} + - SMTP_USER=${SMTP_USER} + - SMTP_PASSWORD=${SMTP_PASSWORD} + - EMAIL_DEV=${EMAIL_DEV} ports: - - "3307:3306" + - "3000:3000" networks: - - spring-net - volumes: - - mysql-data:/bitnami/mysql/data - - ./mysql-init-scripts:/docker-entrypoint-initdb.d - healthcheck: - test: ["CMD", "/opt/bitnami/scripts/mysql/healthcheck.sh"] - interval: 15s - timeout: 5s - retries: 6 + - commercify - commercify: - image: ghcr.io/zenfulcode/commercify:dev + commercify-api: + image: ghcr.io/zenfulcode/commercify:main env_file: .env container_name: commercify-api - ports: - - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=docker - - SPRING_DATASOURCE_URL=${SPRING_DATASOURCE_URL} - - SPRING_DATASOURCE_USERNAME=${SPRING_DATASOURCE_USERNAME} - - SPRING_DATASOURCE_PASSWORD=${SPRING_DATASOURCE_PASSWORD} - - STRIPE_TEST_SECRET=${STRIPE_TEST_SECRET} + - SPRING_DATASOURCE_URL=${DATASOURCE_URL} + - SPRING_DATASOURCE_USERNAME=${DATASOURCE_USERNAME} + - SPRING_DATASOURCE_PASSWORD=${DATASOURCE_PASSWORD} - JWT_SECRET_KEY=${JWT_SECRET_KEY} - depends_on: - mysql-db: - condition: service_healthy + - ADMIN_EMAIL=${ADMIN_EMAIL} + - ADMIN_PASSWORD=${ADMIN_PASSWORD} + - MOBILEPAY_MERCHANT_ID=${MOBILEPAY_MERCHANT_ID} + - MOBILEPAY_CLIENT_ID=${MOBILEPAY_CLIENT_ID} + - MOBILEPAY_CLIENT_SECRET=${MOBILEPAY_CLIENT_SECRET} + - MOBILEPAY_SUBSCRIPTION_KEY=${MOBILEPAY_SUBSCRIPTION_KEY} + - MOBILEPAY_API_URL=${MOBILEPAY_API_URL} + - MOBILEPAY_SYSTEM_NAME=${MOBILEPAY_SYSTEM_NAME} + - STRIPE_SECRET_TEST_KEY=${STRIPE_SECRET_TEST_KEY} + - STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET} + ports: + - "6091:6091" networks: - - spring-net + - commercify networks: - spring-net: + commercify: driver: bridge - -volumes: - mysql-data: - driver: local diff --git a/src/app/(client)/checkout/_components/AuthenticationStep.tsx b/src/app/(client)/checkout/_components/AuthenticationStep.tsx index 33180f7..fa9ae16 100644 --- a/src/app/(client)/checkout/_components/AuthenticationStep.tsx +++ b/src/app/(client)/checkout/_components/AuthenticationStep.tsx @@ -40,8 +40,6 @@ export function AuthenticationStep() { resolver: zodResolver(loginSchema), }); - console.log(isAuthenticated); - if (isAuthenticated) { setStep('information'); return null; diff --git a/src/app/(client)/checkout/_components/PaymentStep.tsx b/src/app/(client)/checkout/_components/PaymentStep.tsx index ef325c1..6278b68 100644 --- a/src/app/(client)/checkout/_components/PaymentStep.tsx +++ b/src/app/(client)/checkout/_components/PaymentStep.tsx @@ -28,7 +28,7 @@ type PaymentMethod = typeof PAYMENT_METHODS[keyof typeof PAYMENT_METHODS]; export function PaymentStep() { const { toast } = useToast(); const { cart, clearCart } = useCart(); - const { state } = useCheckout(); + const { state, setStep } = useCheckout(); const { user } = useAuth(); const [isProcessing, setIsProcessing] = React.useState(false); const [selectedMethod, setSelectedMethod] = React.useState(PAYMENT_METHODS.MOBILEPAY); @@ -61,16 +61,12 @@ export function PaymentStep() { ...(item.selectedVariant && { variantId: item.selectedVariant.id }), })); - console.log(orderLines); - // Create order const orderResponse = await orderService.createOrder(user.id, { currency: "DKK", orderLines, }); - console.log(orderResponse); - // Create payment based on selected method if (selectedMethod === PAYMENT_METHODS.MOBILEPAY) { const paymentResponse = await orderService.createMobilePayPayment({ @@ -162,7 +158,7 @@ export function PaymentStep() { diff --git a/src/app/(client)/page.tsx b/src/app/(client)/page.tsx index 858fb52..f82c818 100644 --- a/src/app/(client)/page.tsx +++ b/src/app/(client)/page.tsx @@ -4,17 +4,11 @@ import React, { useEffect, useState } from 'react'; import { Button } from '@/components/ui/button'; import { - AlertCircle, RefreshCw, ServerCrash, Wifi, WifiOff } from 'lucide-react'; -import { - Alert, - AlertDescription, - AlertTitle, -} from "@/components/ui/alert"; import { Product } from '@/types/product'; import { useToast } from '@/hooks/use-toast'; import { productApi } from '@/services/productsService'; @@ -67,14 +61,6 @@ export default function ProductsPage() { return (
- - - Error Loading Products - - We couldn't load the products at this time. - - -
diff --git a/src/app/(client)/products/[id]/page.tsx b/src/app/(client)/products/[id]/page.tsx deleted file mode 100644 index d442bcd..0000000 --- a/src/app/(client)/products/[id]/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { productApi } from "@/services/productsService"; -import ProductDetails from "./_components/ProductDetails"; - -interface ProductPageProps { - params: { - id: string; - }; - } - - export default async function ProductPage({ params }: ProductPageProps) { - const product = await productApi.getProductById(params.id); - - return ; - } \ No newline at end of file diff --git a/src/app/(client)/products/[id]/_components/ProductDetails.tsx b/src/app/(client)/products/[productId]/page.tsx similarity index 56% rename from src/app/(client)/products/[id]/_components/ProductDetails.tsx rename to src/app/(client)/products/[productId]/page.tsx index f9e1a04..2826c2f 100644 --- a/src/app/(client)/products/[id]/_components/ProductDetails.tsx +++ b/src/app/(client)/products/[productId]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Product, ProductVariant } from '@/types/product'; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -14,22 +14,54 @@ import { import Image from 'next/image'; import placeholderImage from '@/public/placeholder.webp'; import { useRouter } from 'next/navigation'; -import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from '@/components/ui/breadcrumb'; -import { ChevronLeft } from 'lucide-react'; +import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from '@/components/ui/breadcrumb'; +import { ArrowLeft, ChevronLeft, Loader2 } from 'lucide-react'; import { useCart } from '@/context/CartContext'; +import { productApi } from '@/services/productsService'; +import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert'; +import Link from 'next/link'; +import { useToast } from '@/hooks/use-toast'; -interface ProductDetailsProps { - product: Product; -} +export default function ProductDetails({ params }: { params: { productId: string } }) { + const [product, setProduct] = useState(null); -const ProductDetails: React.FC = ({ product }) => { const router = useRouter(); const { addToCart } = useCart(); - const [selectedVariant, setSelectedVariant] = React.useState( - product.variants?.[0] || null + const [selectedVariant, setSelectedVariant] = useState( + product?.variants?.[0] || undefined ); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const { toast } = useToast(); + + useEffect(() => { + const fetchProduct = async () => { + if (!params.productId) { + setError('Product not found'); + return; + } + + setIsLoading(true); + + try { + const response = await productApi.getProductById(params.productId); + setProduct(response); + setSelectedVariant(response.variants?.[0] || null); + } catch (error) { + if (error instanceof Error) { + setError(error.message); + } + } finally { + setIsLoading(false); + } + }; + + fetchProduct(); + }, [params.productId]); + const formatPrice = (amount: number, currency: string) => { return new Intl.NumberFormat('da-DK', { style: 'currency', @@ -38,15 +70,51 @@ const ProductDetails: React.FC = ({ product }) => { }; const handleAddToCart = () => { - // If product has variants, only add to cart if a variant is selected + if (!product) { + setError('Product not found'); + return; + } + if (product.variants.length > 0 && !selectedVariant) { - // You might want to add a toast or error message here + toast({ + title: 'Please select a size', + description: 'Please select a size before adding to cart', + variant: 'destructive' + }); return; } addToCart(product, selectedVariant); }; + if (error || !product) { + return ( +
+ + Error + + {error || 'Product not found'} + + + + + Back to Shop + +
+ ); + } + + if (isLoading) { + return ( +
+ +
+ ); + } + return (
{/* Navigation */} @@ -61,10 +129,19 @@ const ProductDetails: React.FC = ({ product }) => { - Home - Products - {product.name} - + + + Home + + + + products + + + + {product.name} + +
@@ -136,8 +213,6 @@ const ProductDetails: React.FC = ({ product }) => {
-
+
); -}; - -export default ProductDetails; \ No newline at end of file +}; \ No newline at end of file diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index cc2498f..1d50aa8 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -21,7 +21,7 @@ const AuthContext = createContext<{ export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState(null); - const [isLoading, setIsLoading] = useState(true); + const [isLoading, setIsLoading] = useState(false); const { toast } = useToast(); const router = useRouter(); diff --git a/src/services/authService.ts b/src/services/authService.ts index 9a99479..986cb45 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -6,7 +6,7 @@ class AuthService extends BaseApiService { private static instance: AuthService; private constructor() { - super('http://localhost:6091/api/v1/auth'); + super('http://localhost:6091/api/v1/auth', '/auth'); } public static getInstance(): AuthService { @@ -20,8 +20,6 @@ class AuthService extends BaseApiService { try { const response = await this.post('/signup', data); - console.log(response); - if (response.token.length >= 0) { authStorage.setToken(response.token); } diff --git a/src/types/apiBase.ts b/src/types/apiBase.ts index e226168..031166e 100644 --- a/src/types/apiBase.ts +++ b/src/types/apiBase.ts @@ -20,8 +20,29 @@ interface RequestOptions { export abstract class BaseApiService { protected constructor( - protected baseUrl: string - ) { } + protected baseUrl: string, + protected endpoint: string = '' + ) { + if (process.env.NODE_ENV === 'development') { + this.baseUrl = process.env.NEXT_PUBLIC_DEV_COMMERCIFY_API_URL as string; + } else { + this.baseUrl = process.env.NEXT_PUBLIC_COMMERCIFY_API_URL as string; + } + + if (this.baseUrl.endsWith('/')) { + this.baseUrl = this.baseUrl.slice(0, -1); + } + + if (endpoint.length > 0) { + if (endpoint.startsWith('/')) { + this.endpoint = endpoint; + } else { + this.endpoint = `/${endpoint}`; + } + + this.baseUrl += this.endpoint; + } + } protected getHeaders(options: RequestOptions = {}): HeadersInit { const { requiresAuth = false, includeContentType = true } = options;