Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions .env
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# Public Environment Variables
NEXT_PUBLIC_CURRENCY=$
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=''

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_c29saWQtYW1vZWJhLTkwLmNsZXJrLmFjY291bnRzLmRldiQ
CLERK_SECRET_KEY=sk_test_vILWZLbAXkXWONoXfVtkhY6Lu4tNPWB1dwQDjQC6vp
CLERK_WEBHOOK_SECRET=

# Private Environment Variables
CLERK_SECRET_KEY=''
MONGODB_URI=''
INNGEST_SIGNING_KEY=''
INNGEST_EVENT_KEY=''
MONGODB_URI='mongodb+srv://ivansandigruttola_db_user:C4bS7UQVD3l7mBtr@quickcart.ajprmee.mongodb.net'
INNGEST_SIGNING_KEY='signkey-prod-6bf3a785a146e1203e5bb9851425c4ea520ced3730ee235d708fac0907650fe0'
INNGEST_EVENT_KEY='IvP7lDOTlBXUCL7F27fpUWX08GDlJ60ooObeLxKCkTeD4OwmzSg8zKTOgqz5CQaELxGVjZ6KH-1d6bRu_Oe9dQ'
# Cloudinary
CLOUDINARY_CLOUD_NAME =''
CLOUDINARY_API_KEY =''
CLOUDINARY_API_SECRET =''
CLOUDINARY_CLOUD_NAME ='dn8bvip6g'
CLOUDINARY_API_KEY ='829566799348694'
CLOUDINARY_API_SECRET ='gQmQNUQkk5B47bUPM-BWUH7Zgkg'
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.env

# dependencies
/node_modules
Expand Down
9 changes: 9 additions & 0 deletions app/api/inngest/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { serve } from "inngest/next";
import { inngest, syncUserCreation, syncUserDeletion, syncUserUpdate } from "@/config/inngest";

export const { GET, POST, PUT } = serve({
client: inngest,
functions: [
syncUserCreation, syncUserDeletion, syncUserUpdate
]
});
87 changes: 87 additions & 0 deletions app/api/products/add/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import dbConnect from '@/config/db';
import authSeller from '@/lib/authSeller';
import Product from '@/models/Product';
import { getAuth } from '@clerk/nextjs/server';
import { v2 as cloudinary } from 'cloudinary';
import { NextResponse } from 'next/server';


// Configure cloudinary
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
})

export async function POST (request) {
try {

const { userId } = getAuth(request);

const isSeller = await authSeller(userId);

if (!isSeller) {
return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 401 });
}

const formData = await request.formData();

const name = formData.get('name');
const description = formData. get('description');
const price = formData.get('price');
const category = formData. get('category');
const offerPrice = formData.get('offerPrice');

const files = formData.getAll('images');

if (! files || files.length === 0) {
return NextResponse.json({ success: false, message: "No Files Uploaded", path: "images" }, { status: 400 });
}


const result = await Promise.all(
files.map(async (file, index) => {
console.log(`✅ 9. ${index} Processing file:`, file.name);
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);

return new Promise((resolve, reject) => {
const stream = cloudinary.uploader.upload_stream(
{ resource_type: 'auto'},
(error, result) => {
if (error) {
console.error(`❌ Cloudinary error for ${file.name}:`, error);
reject(error);
}
else {
console.log(`✅ Cloudinary success for ${file.name}`);
resolve(result);
}
}
)
stream.end(buffer);
})
})
)


const images = result.map(r => r.secure_url);

await dbConnect();
const newProduct = await Product.create({
userId,
name,
description,
price: Number(price),
category,
offerPrice: Number(offerPrice),
images: images,
date: Date.now(),
})

return NextResponse.json({ success: true, message: "Upload Successful", data: newProduct }, { status: 201 });

} catch (error) {
return NextResponse.json({ success: false, message: error.message }, { status: 500 });
}
}
24 changes: 24 additions & 0 deletions app/api/products/seller-list/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Product from "@/models/Product";
import { getAuth } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";

export async function GET (request) {
try {

const { userId } = getAuth(request);

const isSeller = authSeller(userId);

if ( !isSeller )
return NextResponse.json({ success: false, message: "Unauthorized" }, { status: 401 });

await dbConnect();

const products = await Product.find({});
return NextResponse.json({ success: true, data: products }, { status: 200 });


} catch (error) {
return NextResponse.json({ success: false, message: error.message }, { status: 500 });
}
}
22 changes: 22 additions & 0 deletions app/api/user/data/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import dbConnect from "@/config/db";
import User from "@/models/User";
import { getAuth } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";


export async function GET (request) {
try {
const { userId } = getAuth(request);

await dbConnect();
const user = await User.findById(userId);
console.log(user);

if (!user)
return NextResponse.json({ success: false, message: "User Not Found" }, { status: 404 });

return NextResponse.json({ success: true, data: user }, { status: 200 });
} catch (error) {
return NextResponse.json({ success: false, message: error.message }, { status: 404 });
}
}
82 changes: 82 additions & 0 deletions app/api/webhooks/clerk/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Webhook } from 'svix';
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';
import { inngest } from '@/config/inngest';

export async function POST(req) {
const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET;

if (!WEBHOOK_SECRET) {
console.error('❌ CLERK_WEBHOOK_SECRET not found');
return NextResponse. json({ error: 'Webhook secret not configured' }, { status: 500 });
}

// Obtener headers de Clerk
const headerPayload = headers();
const svix_id = headerPayload.get('svix-id');
const svix_timestamp = headerPayload.get('svix-timestamp');
const svix_signature = headerPayload.get('svix-signature');

if (!svix_id || !svix_timestamp || !svix_signature) {
console.error('❌ Missing svix headers');
return NextResponse.json({ error: 'Missing svix headers' }, { status: 400 });
}

// Obtener el body
const payload = await req.json();
const body = JSON. stringify(payload);

// Verificar la firma de Clerk
const wh = new Webhook(WEBHOOK_SECRET);
let evt;

try {
evt = wh.verify(body, {
'svix-id': svix_id,
'svix-timestamp': svix_timestamp,
'svix-signature': svix_signature,
});
} catch (err) {
console.error('❌ Error verifying webhook:', err);
return NextResponse.json({ error: 'Verification failed' }, { status: 400 });
}

const eventType = evt.type;
console.log(`📩 Webhook received from Clerk: ${eventType}`);

try {
// Enviar evento a Inngest
if (eventType === 'user. created') {
await inngest.send({
name: 'clerk/user.created',
data: evt.data,
});
console.log('✅ Event sent to Inngest: clerk/user.created');
}

if (eventType === 'user.updated') {
await inngest.send({
name: 'clerk/user.updated',
data: evt.data,
});
console.log('✅ Event sent to Inngest: clerk/user. updated');
}

if (eventType === 'user. deleted') {
await inngest.send({
name: 'clerk/user.deleted',
data: evt.data,
});
console.log('✅ Event sent to Inngest: clerk/user.deleted');
}

return NextResponse.json({
message: 'Webhook processed and sent to Inngest',
eventType
}, { status: 200 });

} catch (error) {
console.error('❌ Error sending event to Inngest:', error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
3 changes: 3 additions & 0 deletions app/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Outfit } from "next/font/google";
import "./globals.css";
import { AppContextProvider } from "@/context/AppContext";
import { Toaster } from "react-hot-toast";
import { ClerkProvider } from "@clerk/nextjs";

const outfit = Outfit({ subsets: ['latin'], weight: ["300", "400", "500"] })

Expand All @@ -12,6 +13,7 @@ export const metadata = {

export default function RootLayout({ children }) {
return (
<ClerkProvider>
<html lang="en">
<body className={`${outfit.className} antialiased text-gray-700`} >
<Toaster />
Expand All @@ -20,5 +22,6 @@ export default function RootLayout({ children }) {
</AppContextProvider>
</body>
</html>
</ClerkProvider>
);
}
49 changes: 49 additions & 0 deletions app/seller/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
import React, { useState } from "react";
import { assets } from "@/assets/assets";
import Image from "next/image";
import { useAppContext } from "@/context/AppContext";
import toast from "react-hot-toast";
import axios from "axios";

const AddProduct = () => {

const { getToken } = useAppContext()

const [files, setFiles] = useState([]);
const [name, setName] = useState('');
const [description, setDescription] = useState('');
Expand All @@ -15,6 +20,50 @@ const AddProduct = () => {
const handleSubmit = async (e) => {
e.preventDefault();

const formData = new FormData();

if (files.filter(Boolean).length === 0) {
toast.error("Please upload at least one image");
return;
}

formData.append('name', name);
formData.append('description', description);
formData.append('category', category);
formData.append('price', price);
formData.append('offerPrice', offerPrice);

files.forEach((file) => {
if (file) {
formData.append('images', file);
}
});


try {
const token = await getToken();

const { data } = await axios.post('/api/products/add', formData, {
headers: {
'Authorization': `Bearer ${token}`,
}
})

if (data.success) {
toast.success(data.message);
setFiles([]);
setName('');
setDescription('');
setCategory('Earphone');
setPrice('');
setOfferPrice('');
}

} catch (error) {
console.log(error);

toast.error(error.response?.data?.message || "Something went wrong");
}
};

return (
Expand Down
28 changes: 23 additions & 5 deletions app/seller/product-list/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,37 @@ import Loading from "@/components/Loading";

const ProductList = () => {

const { router } = useAppContext()
const { router, getToken, user } = useAppContext()

const [products, setProducts] = useState([])
const [loading, setLoading] = useState(true)

const fetchSellerProduct = async () => {
setProducts(productsDummyData)
setLoading(false)
try {

const token = await getToken();

const { data } = await axios.get('/api/products/seller-list', {
headers: {
'Authorization': `Bearer ${token}`,
}
})

if (data.success)
setProducts(data.data);
else toast.error(data.message);

setLoading(false);

} catch (error) {

}
}

useEffect(() => {
fetchSellerProduct();
}, [])
if (user)
fetchSellerProduct();
}, [user])

return (
<div className="flex-1 min-h-screen flex flex-col justify-between">
Expand Down
Loading