diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..6790320 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,33 @@ +name: Deploy to Vercel + +on: + push: + branches: [ main, feature/ecommerce-complete ] + pull_request: + branches: [ main ] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build application + run: npm run build + + - name: Deploy to Vercel + uses: amondnet/vercel-action@v25 + with: + vercel-token: ${{ secrets.VERCEL_TOKEN }} + vercel-org-id: ${{ secrets.ORG_ID }} + vercel-project-id: ${{ secrets.PROJECT_ID }} + vercel-args: '--prod' \ No newline at end of file diff --git a/env.example b/env.example new file mode 100644 index 0000000..bfc6b9d --- /dev/null +++ b/env.example @@ -0,0 +1,12 @@ +# Database +DATABASE_URL="postgresql://username:password@localhost:5432/cartzy" + +# NextAuth +NEXTAUTH_URL="http://localhost:3001" +NEXTAUTH_SECRET="your-secret-key-here" + +# OAuth Providers +GOOGLE_CLIENT_ID="your-google-client-id" +GOOGLE_CLIENT_SECRET="your-google-client-secret" +GITHUB_CLIENT_ID="your-github-client-id" +GITHUB_CLIENT_SECRET="your-github-client-secret" \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index c85fb67..40b5201 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,16 +1,25 @@ -import { dirname } from "path"; -import { fileURLToPath } from "url"; -import { FlatCompat } from "@eslint/eslintrc"; +import js from '@eslint/js' +import { FlatCompat } from '@eslint/eslintrc' +import path from 'path' +import { fileURLToPath } from 'url' -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) const compat = new FlatCompat({ baseDirectory: __dirname, -}); +}) const eslintConfig = [ - ...compat.extends("next/core-web-vitals", "next/typescript"), -]; + js.configs.recommended, + ...compat.extends('next/core-web-vitals'), + { + rules: { + 'react-hooks/exhaustive-deps': 'warn', + 'no-undef': 'off', + 'no-unused-vars': 'warn', + }, + }, +] -export default eslintConfig; +export default eslintConfig diff --git a/next.config.ts b/next.config.ts index 19eb793..ea59f21 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,21 +2,10 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { images: { - remotePatterns: [ - { - protocol: "https", - hostname: "lh3.googleusercontent.com", - }, - { - protocol: "https", - hostname: "avatars.githubusercontent.com", - }, - { - protocol: "https", - hostname: "images.unsplash.com", - }, - ], + domains: ['images.unsplash.com'], }, + serverExternalPackages: ['@prisma/client'], + output: 'standalone', }; export default nextConfig; diff --git a/package.json b/package.json index 1e3e1ac..a7aee6a 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,15 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev -p 3001", "build": "next build", - "start": "next start", + "start": "bash start.sh", "lint": "next lint", - "db:seed": "tsx prisma/seed.ts" + "db:generate": "prisma generate", + "db:push": "prisma db push", + "db:migrate": "prisma migrate deploy", + "db:seed": "tsx prisma/seed.ts", + "postinstall": "prisma generate" }, "dependencies": { "@auth/prisma-adapter": "^2.10.0", diff --git a/railway-env.md b/railway-env.md new file mode 100644 index 0000000..b36c31f --- /dev/null +++ b/railway-env.md @@ -0,0 +1,37 @@ +# Railway Environment Variables Setup + +## Required Environment Variables for Railway: + +### Database (Auto-configured by Railway) +- `DATABASE_URL` - Automatically set by Railway PostgreSQL service + +### NextAuth Configuration +- `NEXTAUTH_URL` - Your Railway app URL (e.g., https://your-app.railway.app) +- `NEXTAUTH_SECRET` - Generate with: `openssl rand -base64 32` + +### OAuth Providers (Optional for now) +- `GOOGLE_CLIENT_ID` - Your Google OAuth client ID +- `GOOGLE_CLIENT_SECRET` - Your Google OAuth client secret +- `GITHUB_CLIENT_ID` - Your GitHub OAuth client ID +- `GITHUB_CLIENT_SECRET` - Your GitHub OAuth client secret + +## Setup Steps: + +1. **In Railway Dashboard:** + - Go to your project + - Click on your app service + - Go to "Variables" tab + - Add the environment variables above + +2. **Generate NEXTAUTH_SECRET:** + ```bash + openssl rand -base64 32 + ``` + +3. **Set NEXTAUTH_URL:** + - Use your Railway app URL + - Format: https://your-app-name.railway.app + +## Database Connection: +- Railway automatically provides `DATABASE_URL` +- No additional setup needed for database \ No newline at end of file diff --git a/railway.toml b/railway.toml new file mode 100644 index 0000000..a20656e --- /dev/null +++ b/railway.toml @@ -0,0 +1,9 @@ +[build] +builder = "nixpacks" + +[deploy] +startCommand = "npm start" +healthcheckPath = "/" +healthcheckTimeout = 300 +restartPolicyType = "on_failure" +restartPolicyMaxRetries = 10 \ No newline at end of file diff --git a/src/app/api/cart/route.ts b/src/app/api/cart/route.ts index c42e871..087ee8e 100644 --- a/src/app/api/cart/route.ts +++ b/src/app/api/cart/route.ts @@ -1,11 +1,10 @@ import { NextRequest, NextResponse } from "next/server" -import { getServerSession } from "next-auth" -import { authOptions } from "@/lib/auth" +import { auth } from "@/lib/auth" import { prisma } from "@/lib/prisma" export async function GET() { try { - const session = await getServerSession(authOptions) + const session = await auth() if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) @@ -44,7 +43,7 @@ export async function GET() { export async function POST(request: NextRequest) { try { - const session = await getServerSession(authOptions) + const session = await auth() if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) @@ -68,7 +67,7 @@ export async function POST(request: NextRequest) { // Add new cart items if (items.length > 0) { await prisma.cartItem.createMany({ - data: items.map((item: any) => ({ + data: items.map((item: { productId: string; quantity: number }) => ({ userId: user.id, productId: item.productId, quantity: item.quantity diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 35489eb..8e566c2 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,3 +1,4 @@ +import React from "react" import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; diff --git a/src/app/products/page.tsx b/src/app/products/page.tsx index bdbd9f3..fb2727b 100644 --- a/src/app/products/page.tsx +++ b/src/app/products/page.tsx @@ -7,7 +7,7 @@ import { Notification } from "@/components/Notification"; import { SearchBar } from "@/components/SearchBar"; import { FeaturedCarousel } from "@/components/FeaturedCarousel"; import Image from "next/image"; -import { LoadingSpinner, ProductCardSkeleton, CategorySkeleton } from "@/components/LoadingSpinner"; +import { ProductCardSkeleton, CategorySkeleton } from "@/components/LoadingSpinner"; import { useEffect, useState } from "react"; interface Product { diff --git a/src/components/CartButton.tsx b/src/components/CartButton.tsx index bcb3537..ae7514e 100644 --- a/src/components/CartButton.tsx +++ b/src/components/CartButton.tsx @@ -13,32 +13,32 @@ export function CartButton({ userId }: CartButtonProps) { const [cartItemCount, setCartItemCount] = useState(0) useEffect(() => { - loadCartItemCount() - }, [userId]) - - const loadCartItemCount = async () => { - if (userId) { - // Load from database for logged-in users - try { - const response = await fetch('/api/cart') - if (response.ok) { - const data = await response.json() - const count = data.reduce((total: number, item: any) => total + item.quantity, 0) + const loadCartItemCount = async () => { + if (userId) { + // Load from database for logged-in users + try { + const response = await fetch('/api/cart') + if (response.ok) { + const data = await response.json() + const count = data.reduce((total: number, item: { quantity: number }) => total + (item.quantity || 0), 0) + setCartItemCount(count) + } + } catch (error) { + console.error('Error loading cart count:', error) + } + } else { + // Load from localStorage for non-logged-in users + const savedCart = localStorage.getItem('cartzy-cart') + if (savedCart) { + const cartItems = JSON.parse(savedCart) + const count = cartItems.reduce((total: number, item: { quantity: number }) => total + (item.quantity || 0), 0) setCartItemCount(count) } - } catch (error) { - console.error('Error loading cart count:', error) - } - } else { - // Load from localStorage for non-logged-in users - const savedCart = localStorage.getItem('cartzy-cart') - if (savedCart) { - const cartItems = JSON.parse(savedCart) - const count = cartItems.reduce((total: number, item: any) => total + item.quantity, 0) - setCartItemCount(count) } } - } + + loadCartItemCount() + }, [userId]) return ( <> diff --git a/src/components/Notification.tsx b/src/components/Notification.tsx index eee7d4f..fac074e 100644 --- a/src/components/Notification.tsx +++ b/src/components/Notification.tsx @@ -22,32 +22,32 @@ export function Notification({ userId }: NotificationProps) { const [unreadCount, setUnreadCount] = useState(0) useEffect(() => { - loadNotifications() - }, [userId]) - - const loadNotifications = async () => { - if (userId) { - // Load from database for logged-in users - try { - const response = await fetch('/api/notifications') - if (response.ok) { - const data = await response.json() + const loadNotifications = async () => { + if (userId) { + // Load from database for logged-in users + try { + const response = await fetch('/api/notifications') + if (response.ok) { + const data = await response.json() + setNotifications(data) + setUnreadCount(data.filter((n: Notification) => !n.read).length) + } + } catch (error) { + console.error('Error loading notifications:', error) + } + } else { + // Load from localStorage for non-logged-in users + const savedNotifications = localStorage.getItem('cartzy-notifications') + if (savedNotifications) { + const data = JSON.parse(savedNotifications) setNotifications(data) setUnreadCount(data.filter((n: Notification) => !n.read).length) } - } catch (error) { - console.error('Error loading notifications:', error) - } - } else { - // Load from localStorage for non-logged-in users - const savedNotifications = localStorage.getItem('cartzy-notifications') - if (savedNotifications) { - const data = JSON.parse(savedNotifications) - setNotifications(data) - setUnreadCount(data.filter((n: Notification) => !n.read).length) } } - } + + loadNotifications() + }, [userId]) const markAsRead = async (notificationId: string) => { const updatedNotifications = notifications.map(n => diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 220965d..f201464 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -1,4 +1,5 @@ "use client" +import React from "react" import { useState } from "react" import { MagnifyingGlassIcon } from "@heroicons/react/24/outline" diff --git a/src/components/ShoppingCart.tsx b/src/components/ShoppingCart.tsx index 1522446..f7a8e23 100644 --- a/src/components/ShoppingCart.tsx +++ b/src/components/ShoppingCart.tsx @@ -21,33 +21,32 @@ interface ShoppingCartProps { export function ShoppingCart({ isOpen, onClose, userId }: ShoppingCartProps) { const [cartItems, setCartItems] = useState([]) - const [isLoading, setIsLoading] = useState(false) // Load cart items on mount useEffect(() => { - loadCartItems() - }, [userId]) - - const loadCartItems = async () => { - if (userId) { - // Load from database for logged-in users - try { - const response = await fetch('/api/cart') - if (response.ok) { - const data = await response.json() - setCartItems(data) + const loadCartItems = async () => { + if (userId) { + // Load from database for logged-in users + try { + const response = await fetch('/api/cart') + if (response.ok) { + const data = await response.json() + setCartItems(data) + } + } catch (error) { + console.error('Error loading cart:', error) + } + } else { + // Load from localStorage for non-logged-in users + const savedCart = localStorage.getItem('cartzy-cart') + if (savedCart) { + setCartItems(JSON.parse(savedCart)) } - } catch (error) { - console.error('Error loading cart:', error) - } - } else { - // Load from localStorage for non-logged-in users - const savedCart = localStorage.getItem('cartzy-cart') - if (savedCart) { - setCartItems(JSON.parse(savedCart)) } } - } + + loadCartItems() + }, [userId]) const saveCartItems = async (items: CartItem[]) => { if (userId) { @@ -67,30 +66,7 @@ export function ShoppingCart({ isOpen, onClose, userId }: ShoppingCartProps) { } } - const addToCart = async (product: any) => { - const existingItem = cartItems.find(item => item.productId === product.id) - - let newItems: CartItem[] - if (existingItem) { - newItems = cartItems.map(item => - item.productId === product.id - ? { ...item, quantity: item.quantity + 1 } - : item - ) - } else { - newItems = [...cartItems, { - id: Date.now().toString(), - productId: product.id, - name: product.name, - price: product.price, - quantity: 1, - image: product.images[0] - }] - } - - setCartItems(newItems) - await saveCartItems(newItems) - } + const updateQuantity = async (itemId: string, newQuantity: number) => { if (newQuantity <= 0) { diff --git a/src/components/providers/SessionProvider.tsx b/src/components/providers/SessionProvider.tsx index 9d51ef9..ee4b4af 100644 --- a/src/components/providers/SessionProvider.tsx +++ b/src/components/providers/SessionProvider.tsx @@ -1,5 +1,6 @@ "use client" +import React from "react" import { SessionProvider as NextAuthSessionProvider } from "next-auth/react" export function SessionProvider({ children }: { children: React.ReactNode }) { diff --git a/src/components/providers/ThemeProvider.tsx b/src/components/providers/ThemeProvider.tsx index 00b4fdb..2aee8bf 100644 --- a/src/components/providers/ThemeProvider.tsx +++ b/src/components/providers/ThemeProvider.tsx @@ -1,4 +1,5 @@ "use client" +import React from "react" import { ThemeProvider as NextThemesProvider } from "next-themes" diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..7e3c44d --- /dev/null +++ b/start.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Wait for database to be ready +echo "Waiting for database to be ready..." +sleep 10 + +# Run database migrations +echo "Running database migrations..." +npx prisma migrate deploy + +# Start the application +echo "Starting the application..." +npm start \ No newline at end of file diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..167abf0 --- /dev/null +++ b/vercel.json @@ -0,0 +1,12 @@ +{ + "buildCommand": "npm run build", + "devCommand": "npm run dev", + "installCommand": "npm install", + "framework": "nextjs", + "regions": ["iad1"], + "functions": { + "src/app/api/**/*.ts": { + "maxDuration": 30 + } + } +} \ No newline at end of file