diff --git a/src/api/package-lock.json b/src/api/package-lock.json index a114eecd..0b85be6d 100644 --- a/src/api/package-lock.json +++ b/src/api/package-lock.json @@ -13,6 +13,7 @@ "@aws-sdk/lib-storage": "^3.513.0", "@aws-sdk/s3-request-presigner": "^3.513.0", "@supabase/supabase-js": "^2.33.2", + "api": "file:", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", @@ -2030,6 +2031,10 @@ "node": ">= 8" } }, + "node_modules/api": { + "resolved": "", + "link": true + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", diff --git a/src/api/package.json b/src/api/package.json index 84d0916c..d8567cf2 100644 --- a/src/api/package.json +++ b/src/api/package.json @@ -5,7 +5,7 @@ "main": "dist/server.js", "scripts": { "start": "node dist/server.js", - "dev": "tsc --watch & nodemon dist/server.js", + "dev": "ts-node-dev --respawn --transpile-only src/server.ts", "build": "tsc", "test": "echo \"Error: no test specified\" && exit 1" }, @@ -20,6 +20,7 @@ "@aws-sdk/lib-storage": "^3.513.0", "@aws-sdk/s3-request-presigner": "^3.513.0", "@supabase/supabase-js": "^2.33.2", + "api": "file:", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", diff --git a/src/api/src/server.ts b/src/api/src/server.ts index 2b25256e..05db63bf 100644 --- a/src/api/src/server.ts +++ b/src/api/src/server.ts @@ -55,7 +55,7 @@ const app = express(); // Rate limiting configuration const limiter = rateLimit({ windowMs: 15 * 60 * 1000, - max: 100, + max: 1000, legacyHeaders: false, standardHeaders: true, skip: (req) => { @@ -1200,6 +1200,7 @@ app.patch('/api/profile', jwtCheck, async (req: Request, res: Response): Promise } }); + // Business Profile Routes app.post('/api/business-profile', jwtCheck, async (req: Request, res: Response): Promise => { try { @@ -1285,69 +1286,103 @@ app.post('/api/business-profile', jwtCheck, async (req: Request, res: Response): } }); -// Get business profile +// Get business profile by id or custom url app.get('/api/business-profile/:id', optionalJwtCheck, async (req: Request, res: Response): Promise => { try { const { id } = req.params; - const requestingUserId = req.auth?.payload?.sub; + // Safely extract the requesting user ID if available + const requestingUserId = req.auth?.payload?.sub || null; - // Get the business profile - const { data, error } = await supabase + console.log(`Business profile request for: ${id}, requesting user: ${requestingUserId || 'unauthenticated'}`); + + if (!id) { + res.status(400).json({ error: 'Business Profile ID is required' }); + return; + } + + // First try to find by ID + let { data, error } = await supabase .from('business_profiles') - .select(` - *, - profiles ( - name, - avatar_url, - is_public - ) - `) + .select(`*`) .eq('id', id) - .single(); + .maybeSingle(); if (error) { - console.error('Error fetching business profile:', error); + console.error('Error fetching business profile by id:', { + error, + message: error.message, + details: error.details + }); res.status(500).json({ - error: 'Database Error', - message: 'Failed to fetch business profile' + error: 'Database error', + message: 'An error occurred while fetching the business profile', + details: error.message }); return; } + // If no error but also no data, it means no record was found if (!data) { - res.status(404).json({ - error: 'Not Found', - message: 'Business profile not found' - }); - return; - } + // Then try to find by custom_url + const { data: customUrlData, error: customUrlError } = await supabase + .from('business_profiles') + .select(`*`) + .eq('custom_url', id) + .maybeSingle(); + + if (customUrlError) { + console.error('Error fetching business profile by custom_url:', customUrlError); + res.status(500).json({ + error: 'Database error', + message: 'An error occurred while fetching the business profile' + }); + return; + } + console.log("Custom URL DATA: ", customUrlData); - // Check if the profile is public or if the requester is the owner - if (!data.profiles.is_public && requestingUserId !== id) { - res.status(403).json({ - error: 'Access Denied', - message: 'This business profile is private' - }); - return; + if (customUrlData) { + res.status(200).json(customUrlData); + return; + } + + if (!customUrlData) { + console.log(`Business profile not found for ID or custom_url: ${id}`); + res.status(404).json({ + error: 'Business Profile not found', + message: 'This business profile does not exist or might be private.' + }); + return; + } } - res.json(data); + res.status(200).json(data); } catch (error: any) { - console.error('Error in GET /api/business-profile/:id:', error); + console.error('Error fetching business profile by id:', { + message: error.message || 'Unknown error', + details: error.stack || '', + hint: '', + code: error.code || '' + }); res.status(500).json({ - error: 'Server Error', - message: error.message || 'An unexpected error occurred' + error: 'Server error', + message: error.message || 'An unexpected error occurred' }); } }); // Update business profile app.patch('/api/business-profile/:id', jwtCheck, async (req: Request, res: Response): Promise => { + console.log("PATCHING BUSINESS PROFILE FROM SERVER"); try { const { id } = req.params; - const updates = req.body; + const updates = req.body.profile; const userId = req.auth?.payload.sub; + const imageUrls = req.body.imageUrls; + + console.log('updates', updates); + console.log('imageUrls', imageUrls); + // Verify ownership if (id !== userId) { res.status(403).json({ @@ -1383,7 +1418,23 @@ app.patch('/api/business-profile/:id', jwtCheck, async (req: Request, res: Respo .select() .single(); + // Update business images + if (imageUrls) { + const { error: existingImagesError } = await supabase + .from('business_images') + .insert(imageUrls.map((url: string) => ({ + business_id: id, + url: url + }))) + .select('*') + + if (existingImagesError) { + console.log('Error updating business images:', existingImagesError); + } + } + if (error) { + console.log('Error updating business profile:', error); console.error('Error updating business profile:', error); res.status(500).json({ error: 'Database Error', @@ -1402,6 +1453,37 @@ app.patch('/api/business-profile/:id', jwtCheck, async (req: Request, res: Respo } }); +// Get business images +app.get('/api/business-profile/images/:id', optionalJwtCheck, async (req: Request, res: Response): Promise => { + try { + console.log(`Fetching business images FROM SERVER: ${req.params.id}`); + const { id } = req.params; + const { data, error } = await supabase + .from('business_images') + .select('url') + .eq('business_id', id); + + if (error) { + console.error('Error fetching business images:', error); + res.status(500).json({ + error: 'Database error', + message: 'An error occurred while fetching the business images' + }); + return; + } + + // Transform data to return just an array of URLs + const urls = data.map(item => item.url); + res.status(200).json(urls); + } catch (error: any) { + console.error('Error fetching business images:', error); + res.status(500).json({ + error: 'Server Error', + message: error.message || 'An unexpected error occurred' + }); + } +}); + if (!process.env.RHINO_COMPUTE_ENDPOINT || !process.env.RHINO_COMPUTE_KEY) { throw new Error('Rhino Compute environment variables not configured'); } diff --git a/src/apps/design/package.json b/src/apps/design/package.json index 067522b5..aa7aaa89 100644 --- a/src/apps/design/package.json +++ b/src/apps/design/package.json @@ -7,6 +7,10 @@ "@auth0/auth0-react": "^2.2.4", "@aws-sdk/client-s3": "^3.369.0", "@aws-sdk/s3-request-presigner": "^3.369.0", + "@babel/core": "^7.25.8", + "@babel/preset-env": "^7.25.8", + "@babel/preset-react": "^7.25.7", + "@babel/preset-typescript": "^7.25.8", "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", "@mantine/core": "^7.1.0", @@ -21,49 +25,46 @@ "@stripe/stripe-js": "^5.5.0", "@supabase/supabase-js": "^2.47.12", "@tabler/icons-react": "^2.30.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.13.10", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@types/three": "^0.169.0", + "autoprefixer": "^10.4.20", "axios": "^1.6.8", + "babel-loader": "^9.2.1", "clean-webpack-plugin": "^4.0.0", "compression": "^1.7.4", "compression-webpack-plugin": "^10.0.0", "copy-webpack-plugin": "^12.0.2", "cors": "^2.8.5", + "css-loader": "^7.1.2", + "design": "file:", "dotenv": "^16.4.5", "express": "^4.19.2", "express-rate-limit": "^7.5.0", + "file-loader": "^6.2.0", "helmet": "^7.1.0", "html-webpack-plugin": "^5.6.0", + "postcss": "^8.5.3", + "postcss-loader": "^8.1.1", "react": "^18.2.0", "react-colorful": "^5.6.1", "react-dom": "^18.2.0", "react-error-boundary": "^5.0.0", "react-hot-toast": "^2.4.1", "react-router-dom": "^6.22.3", + "style-loader": "^4.0.0", + "tailwindcss": "^3.4.17", "three": "^0.162.0", "three-mesh-bvh": "^0.7.0", - "uuid": "^9.0.1", - "webpack-bundle-analyzer": "^4.10.1", - "@babel/core": "^7.25.8", - "@babel/preset-env": "^7.25.8", - "@babel/preset-react": "^7.25.7", - "@babel/preset-typescript": "^7.25.8", - "babel-loader": "^9.2.1", - "css-loader": "^7.1.2", - "file-loader": "^6.2.0", - "postcss": "^8.5.3", - "postcss-loader": "^8.1.1", - "style-loader": "^4.0.0", "ts-loader": "^9.5.1", + "typescript": "^5.7.2", + "uuid": "^9.0.1", "webpack": "^5.95.0", + "webpack-bundle-analyzer": "^4.10.1", "webpack-cli": "^5.1.4", - "webpack-dev-server": "^5.1.0", - "@types/jest": "^29.5.14", - "@types/node": "^22.13.10", - "@types/react": "^18.2.0", - "@types/react-dom": "^18.2.0", - "@types/three": "^0.169.0", - "autoprefixer": "^10.4.20", - "tailwindcss": "^3.4.17", - "typescript": "^5.7.2" + "webpack-dev-server": "^5.1.0" }, "scripts": { "start": "node server.js", @@ -80,7 +81,6 @@ "postinstall": "npm run copy-shared", "heroku-postbuild": "npm run copy-shared && npm run build:css && webpack --mode production" }, - "devDependencies": {}, "eslintConfig": { "extends": [ "react-app" diff --git a/src/apps/design/server.js b/src/apps/design/server.js index 76d14386..7e99010f 100644 --- a/src/apps/design/server.js +++ b/src/apps/design/server.js @@ -23,7 +23,7 @@ app.set('trust proxy', 1); // Apply rate limiting to all requests const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes - max: 100, // limit each IP to 100 requests per windowMs + max: 1000, // limit each IP to 100 requests per windowMs standardHeaders: true, legacyHeaders: false, }); diff --git a/src/apps/marketplace/components/dashboard/Dashboard.tsx b/src/apps/marketplace/components/dashboard/Dashboard.tsx index 7eac4766..c5062ed4 100644 --- a/src/apps/marketplace/components/dashboard/Dashboard.tsx +++ b/src/apps/marketplace/components/dashboard/Dashboard.tsx @@ -96,7 +96,7 @@ const Dashboard: React.FC = ({ children }) => { void } -const MarketplaceCard: React.FC = ({ image, user, name, price, onClick }) => { +const MarketplaceCard: React.FC = ({ image, user, business, name, price, onClick }) => { const navigate = useNavigate(); return ( @@ -22,12 +23,12 @@ const MarketplaceCard: React.FC = ({ image, user, name, pr {name} { e.stopPropagation(); // Prevent triggering the card's onClick - navigate(`/profile/${user?.id}`); + navigate(`/profile/${business?.id || user?.id}`); }}> - - {user?.name?.charAt(0)} + + {business?.company_name?.charAt(0) || user?.name?.charAt(0)} - Produced by {user?.name || 'Unknown'} + Produced by {business?.company_name || user?.name || 'Unknown'} ${price} diff --git a/src/apps/marketplace/components/dashboard/components/ProfileModels/ProfileModels.tsx b/src/apps/marketplace/components/dashboard/components/ProfileModels/ProfileModels.tsx new file mode 100644 index 00000000..c90f82a8 --- /dev/null +++ b/src/apps/marketplace/components/dashboard/components/ProfileModels/ProfileModels.tsx @@ -0,0 +1,58 @@ +import React, { useEffect, useState } from 'react'; +import { SimpleGrid, Loader } from '@mantine/core'; +import { getModelsPerUser, ModelMetadata } from '@shared/services/modelService'; +import { ProfileStorageService } from '@shared/services/profileStorage'; +import { Profile } from '@shared/types/Profile'; +import MarketplaceCard from '../MarketplaceCard/MarketplaceCard'; + +interface ProfileModelsProps { + profileId?: string; +} + +const ProfileModels: React.FC = ({ profileId }) => { + const [models, setModels] = useState([]); + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + if (!profileId) return; + setLoading(true); + try { + const [fetchedModels, fetchedUser] = await Promise.all([ + getModelsPerUser(profileId), + ProfileStorageService.getProfile(profileId) + ]); + + setModels(fetchedModels); + setUser(fetchedUser); + } catch (error) { + console.error('Failed to fetch profile models:', error); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [profileId]); + + if (loading) { + return ; + } + + return ( + + {models.map((model) => ( + + ))} + + ); +}; + +export default ProfileModels; \ No newline at end of file diff --git a/src/apps/marketplace/components/dashboard/components/ProfileSidebar/ProfileSidebar.css b/src/apps/marketplace/components/dashboard/components/ProfileSidebar/ProfileSidebar.css deleted file mode 100644 index 76e2e031..00000000 --- a/src/apps/marketplace/components/dashboard/components/ProfileSidebar/ProfileSidebar.css +++ /dev/null @@ -1,24 +0,0 @@ -.profile-sidebar { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 100%; - text-align: center; - padding: 1rem 0.5rem; - gap: 1rem; -} - -.profile-sidebar-header { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 100%; - text-align: center; - gap: 0.5rem; -} - -.profile-sidebar-content { - gap: 0.5rem; -} \ No newline at end of file diff --git a/src/apps/marketplace/components/dashboard/components/ProfileSidebar/ProfileSidebar.tsx b/src/apps/marketplace/components/dashboard/components/ProfileSidebar/ProfileSidebar.tsx index 2f5ac239..f63d0907 100644 --- a/src/apps/marketplace/components/dashboard/components/ProfileSidebar/ProfileSidebar.tsx +++ b/src/apps/marketplace/components/dashboard/components/ProfileSidebar/ProfileSidebar.tsx @@ -1,36 +1,50 @@ import { Avatar, Button, Divider, Text, Title } from '@mantine/core'; import React from 'react'; -import './ProfileSidebar.css'; -import { IconArrowRight } from '@tabler/icons-react'; +import { IconArrowRight, IconSettings } from '@tabler/icons-react'; import { Profile } from '@shared/types/Profile'; +import { useNavigate } from 'react-router-dom'; interface ProfileSidebarProps { profile?: Profile + ownProfile?: boolean } -const ProfileSidebar: React.FC = ({profile}) => { - return
-
- - {profile?.name || "Unknown profile"} -
-
- {profile?.email} - {profile?.location} - {profile?.description && ( - <> - - {profile?.description} - - +const ProfileSidebar: React.FC = ({profile, ownProfile}) => { + const navigate = useNavigate(); + + return ( +
+
+ + {profile?.name || "Unknown profile"} +
+
+ {profile?.email} + {profile?.location} + {profile?.description && ( + <> + + {profile?.description} + + + )} + {profile?.website && ( + + + + )} +
+ {ownProfile && ( +
+ +
)} - {profile?.website && - - }
-
; + ); }; export default ProfileSidebar; diff --git a/src/apps/marketplace/components/dashboard/pages/Marketplace/components/ProductCard.tsx b/src/apps/marketplace/components/dashboard/pages/Marketplace/components/ProductCard.tsx index e91079e8..553ba69d 100644 --- a/src/apps/marketplace/components/dashboard/pages/Marketplace/components/ProductCard.tsx +++ b/src/apps/marketplace/components/dashboard/pages/Marketplace/components/ProductCard.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Card, Image, Text, Group, Avatar, Badge, Rating } from '@mantine/core'; import { Link } from 'react-router-dom'; -import { Profile } from '@shared/types/Profile'; +import { BusinessProfile, Profile } from '@shared/types/Profile'; export interface ProductCardProps { id: string; @@ -11,6 +11,7 @@ export interface ProductCardProps { price: number | null; priceOnRequest?: boolean; user?: Profile; + business?: BusinessProfile; rating?: number; features?: string[]; onClick?: () => void; // Optional click handler for component preview @@ -28,6 +29,7 @@ const ProductCard: React.FC = ({ price, priceOnRequest, user, + business, rating = 0, features = [], onClick, @@ -102,6 +104,21 @@ const ProductCard: React.FC = ({ Produced by {user?.name || 'Unknown'}
)} + {business && ( +
{ + e.preventDefault(); + e.stopPropagation(); + window.location.href = `/dashboard/profile/${business.id}`; + }} + > + + {business.company_name?.charAt(0)} + + Produced by {business.company_name || 'Unknown'} +
+ )}
diff --git a/src/apps/marketplace/components/dashboard/pages/ProfilePage/BusinessProfilePage.tsx b/src/apps/marketplace/components/dashboard/pages/ProfilePage/BusinessProfilePage.tsx new file mode 100644 index 00000000..b8daf88f --- /dev/null +++ b/src/apps/marketplace/components/dashboard/pages/ProfilePage/BusinessProfilePage.tsx @@ -0,0 +1,375 @@ +import { Button, Image, Menu, SimpleGrid } from "@mantine/core"; +import { getCategoryNameById, getModelsPerUser } from "@marketplace/shared/services/modelService"; +import { ModelMetadata } from "@shared/services/modelService"; +import { BusinessProfile as BusinessProfileType } from "@shared/types/Profile"; +import { IconArrowLeft, IconArrowRight, IconBrandFacebook, IconBrandInstagram, IconBrandLinkedin, IconBrandYoutube, IconCertificate, IconFlag, IconGlobe, IconInfoCircle, IconMail, IconMapPin, IconPhone, IconRefresh, IconSettings, IconStar, IconStarFilled, IconStarHalfFilled } from "@tabler/icons-react"; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { ProfileStorageService } from '../../../../src/shared/services/profileStorage'; +import { ComponentModal } from "../../components/ComponentModal/ComponentModal"; +import ProductCard from "../Marketplace/components/ProductCard"; + +const RATING = 3.3 + +const BusinessProfilePage = ({businessProfile, ownProfile, onSwitch}: {businessProfile: BusinessProfileType, ownProfile: boolean, onSwitch: () => void}) => { + const [images, setImages] = useState([]); + const [previewImages, setPreviewImages] = useState([]); + const [products, setProducts] = useState([]); + const [selectedImageIndex, setSelectedImageIndex] = useState(0); + const [categories, setCategories] = useState<{id: number, name: string}[]>([]); + const [selectedCategory, setSelectedSubcategory] = useState(null); + const [selectedProduct, setSelectedProduct] = useState(null); + + const navigate = useNavigate(); + + useEffect(() => { + const fetchImages = async () => { + const images = await ProfileStorageService.getBusinessImages(businessProfile.id); + setImages(images); + }; + fetchImages(); + }, [businessProfile]); + + useEffect(() => { + const fetchProducts = async () => { + try { + const userProducts = await getModelsPerUser(businessProfile.id); + setProducts(userProducts); + console.log("products", userProducts); + } catch (error) { + console.error('Error fetching products:', error); + } + }; + fetchProducts(); + }, [businessProfile]); + + useEffect(() => { + const fetchSubcategoryNames = async () => { + if (products.length === 0) return; + try { + const uniquecategories = Array.from(new Set(products.map((product) => product.category).filter((category) => category !== undefined))); + const allcategories = await Promise.all(uniquecategories.map(async (category) => { + const categoryName = await getCategoryNameById(category); + return { id: category, name: categoryName }; + })); + setCategories(allcategories); + } catch (error) { + console.error('Error fetching categories:', error); + } + }; + fetchSubcategoryNames(); + }, [products]); + + const nextImage = () => { + const newIndex = (selectedImageIndex + 1) % images.length; + setSelectedImageIndex(newIndex); + }; + + const previousImage = () => { + setSelectedImageIndex((prev) => (prev - 1 + images.length) % images.length); + }; + + useEffect(() => { + if (images.length === 0) return; + + const total = images.length; + const prevIndex = (selectedImageIndex - 1 + total) % total; + const nextIndex = (selectedImageIndex + 1) % total; + + const updatedPreviewImages = [ + images[prevIndex], + images[selectedImageIndex], + images[nextIndex], + ]; + console.log(updatedPreviewImages); + + setPreviewImages(updatedPreviewImages); + }, [images, selectedImageIndex]); + + + return ( + <> +
+
+
+ {businessProfile.company_name} +
+
+

{businessProfile.business_type}

+
+
+

{businessProfile.company_name},

+

{businessProfile.country}

+
+
+ {(() => { + const roundedRating = Math.round(RATING * 2) / 2; + const fullStars = Math.floor(roundedRating); + const halfStar = roundedRating % 1 !== 0; + const emptyStars = 5 - fullStars - (halfStar ? 1 : 0); + + return ( +
+ {Array.from({length: fullStars}).map((_, index) => ( + + ))} + {halfStar && } + {Array.from({length: emptyStars}).map((_, index) => ( + + ))} +

{RATING}

+

Bazaar Rating

+
+ ); + })()} +
+
+ + + + + + { + !businessProfile.country && !businessProfile.city && !businessProfile.description && !businessProfile.website && ( + + {businessProfile.company_name} has no information + + ) + } + { + businessProfile.country && ( + } + > + {businessProfile.country} + + ) + } + { + businessProfile.city && ( + } + > + {businessProfile.city} + + ) + } + { + businessProfile.description && ( + } + > + {businessProfile.description} + + ) + } + { + businessProfile.website && ( + } + > + {businessProfile.website} + + ) + } + + + + + + + + + { + !businessProfile.facebook && !businessProfile.instagram && !businessProfile.linkedin && !businessProfile.youtube && ( + + {businessProfile.company_name} has no social media links + + ) + } + {businessProfile.facebook && ( + } + > + Facebook + + )} + {businessProfile.instagram && ( + } + > + Instagram + + )} + {businessProfile.linkedin && ( + } + > + LinkedIn + + )} + {businessProfile.youtube && ( + } + > + YouTube + + )} + + + + + + + + } + > + Certifications + + + + + + + + + { + !businessProfile.phone && !businessProfile.email && ( + + {businessProfile.company_name} has no contact information + + ) + } + {businessProfile.phone && ( + } + > + {businessProfile.phone} + + )} + {businessProfile.email && ( + } + > + {businessProfile.email} + + )} + + + +
+ {ownProfile && ( + + + + + )} +
+ +
+ {/* Image carousel */} +
+ {`${businessProfile.company_name} +
+ +
+ + {previewImages.map((image, index) => { + const actualIndex = (selectedImageIndex - 1 + index + images.length) % images.length; + + return ( + + ); + })} + +
+
+
+ + {/* Products section */} +
+
+ + {categories.map((category) => ( + + ))} +
+
+ {products.filter((product) => (product.category === selectedCategory || selectedCategory === null)).map((product) => ( + setSelectedProduct(product)} id={product.id || ""} type="component" /> + ))} +
+
+ {selectedProduct && ( + setSelectedProduct(null)} /> + )} + + ); +}; + +export default BusinessProfilePage; + diff --git a/src/apps/marketplace/components/dashboard/pages/ProfilePage/EditProfile.tsx b/src/apps/marketplace/components/dashboard/pages/ProfilePage/EditProfile.tsx new file mode 100644 index 00000000..c256492c --- /dev/null +++ b/src/apps/marketplace/components/dashboard/pages/ProfilePage/EditProfile.tsx @@ -0,0 +1,426 @@ +import { useEffect, useState } from 'react'; +import { Tabs, TextInput, Textarea, Button, Stack, FileInput, SimpleGrid, Image, Text, Group, Switch, Select } from '@mantine/core'; +import { IconPhotoPlus } from '@tabler/icons-react'; +import { ProfileStorageService } from '../../../../../marketplace/src/shared/services/profileStorage'; +import { Profile, BusinessProfile, UserType } from '@shared/types/Profile'; +import { useUser } from '@shared/hooks/useUser'; +import { useAuth0 } from '@auth0/auth0-react'; +import { Notifications, Notification } from '@marketplace/shared/components'; + +const EditProfile = () => { + const { user } = useUser(); + const [profile, setProfile] = useState(null); + const [businessProfile, setBusinessProfile] = useState(null); + const [loading, setLoading] = useState(true); + const [businessImages, setBusinessImages] = useState([]); + + const [businessLogo, setBusinessLogo] = useState(null); + const [newBusinessImages, setNewBusinessImages] = useState([]); + + const [activeTab, setActiveTab] = useState<'personal' | 'business'>('personal'); + + const {getAccessTokenSilently} = useAuth0(); + + const [notification, setNotification] = useState(); + const [saving, setSaving] = useState(false); + + const [avatarFile, setAvatarFile] = useState(null); + + useEffect(() => { + const fetchProfiles = async () => { + if (!user?.id) return; + + try { + const [profileData, businessProfileData, images] = await Promise.all([ + ProfileStorageService.getProfile(user.id), + ProfileStorageService.getBusinessProfile(user.id).catch(() => null), + ProfileStorageService.getBusinessImages(user.id) + ]); + + setProfile(profileData); + setBusinessProfile(businessProfileData); + setBusinessImages(images); + if (businessProfileData) { + setActiveTab('business'); + } + } catch (error) { + console.error('Error fetching profiles:', error); + } finally { + setLoading(false); + } + }; + + fetchProfiles(); + }, [user?.id]); + + const handleProfileSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!profile || !user?.id) return; + + setSaving(true); + try { + let avatarUrl = profile.avatar_url; + if (avatarFile) { + try { + avatarUrl = await ProfileStorageService.uploadProfilePicture(avatarFile); + } catch (error) { + console.error('Error uploading avatar:', error); + return; + } + } + + const token = await getAccessTokenSilently(); + + await ProfileStorageService.updateProfile({ + ...profile, + avatar_url: avatarUrl, + }, token); + setNotification({ + id: '1', + type: 'success', + message: 'Profile updated successfully', + }) + } catch (error) { + console.error('Error updating profile:', error); + setNotification({ + id: '1', + type: 'error', + message: 'Error updating profile', + }) + } finally { + setSaving(false); + } + }; + + const handleBusinessProfileSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!businessProfile || !user?.id) return; + + setSaving(true); + try { + const token = await getAccessTokenSilently(); + + // Upload new images first + const newImageUrls = await Promise.all( + newBusinessImages.map(file => ProfileStorageService.uploadBusinessImage(file)) + ); + + let logoUrl = businessProfile.logo; + if (businessLogo) { + try { + logoUrl = await ProfileStorageService.uploadBusinessLogo(businessLogo); + } catch (error) { + console.error('Error uploading business logo:', error); + return; + } + } + // Update business profile + await ProfileStorageService.updateBusinessProfile(user.id, { + ...businessProfile, + logo: logoUrl, + }, newImageUrls, token); + + setNotification({ + id: '1', + type: 'success', + message: 'Business profile updated successfully', + }) + + setBusinessImages(prev => [...prev, ...newImageUrls]); + setNewBusinessImages([]); + } catch (error) { + console.error('Error updating business profile:', error); + setNotification({ + id: '1', + type: 'error', + message: 'Error updating business profile', + }) + } finally { + setSaving(false); + } + }; + + if (loading) { + return
Laster...
; + } + + return ( +
+ setNotification(null)} /> +
+ + +
+ +
+ {activeTab === 'personal' ? ( +
+ +
+

Profile Picture

+
+ {(avatarFile || profile?.avatar_url) && ( +
+ Profile Picture +
+ )} +
document.getElementById('avatarInput')?.click()} + > + + + Click to upload + +
+ setAvatarFile(e.target.files?.[0] || null)} + /> +
+
+ setProfile(prev => prev ? { ...prev, name: e.target.value } : null)} + /> + setProfile(prev => prev ? { ...prev, email: e.target.value } : null)} + /> + setProfile(prev => prev ? { ...prev, location: e.target.value } : null)} + /> + setProfile(prev => prev ? { ...prev, website: e.target.value } : null)} + /> + setProfile(prev => prev ? { ...prev, custom_url: e.target.value } : null)} + description="This will be the URL of your Bazaar profile. The URL will be https://marketplace.bazaar.com/profile/[custom_url]" + /> +