Approve tool use

This commit is contained in:
gpt-engineer-app[bot]
2025-10-10 22:41:12 +00:00
parent 303a338a99
commit 15babfa801
6 changed files with 649 additions and 132 deletions

160
package-lock.json generated
View File

@@ -37,6 +37,9 @@
"@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.7",
"@stripe/react-stripe-js": "^5.2.0",
"@stripe/stripe-js": "^8.0.0",
"@supabase/supabase-js": "^2.75.0",
"@tanstack/react-query": "^5.83.0", "@tanstack/react-query": "^5.83.0",
"@types/google.maps": "^3.58.1", "@types/google.maps": "^3.58.1",
"apexcharts": "^3.45.2", "apexcharts": "^3.45.2",
@@ -2592,6 +2595,103 @@
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@stripe/react-stripe-js": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-5.2.0.tgz",
"integrity": "sha512-m6CFXWjI5yikOcaP1L9xiabgkljdhu8vspoqF+BDD9mIjZIh1yEjeU0++oefqlXBd9U3QZj+C2ds4t3BDmICMg==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.7.2"
},
"peerDependencies": {
"@stripe/stripe-js": ">=8.0.0 <9.0.0",
"react": ">=16.8.0 <20.0.0",
"react-dom": ">=16.8.0 <20.0.0"
}
},
"node_modules/@stripe/stripe-js": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.0.0.tgz",
"integrity": "sha512-dLvD55KT1cBmrqzgYRgY42qNcw6zW4HS5oRZs0xRvHw9gBWig5yDnWNop/E+/t2JK+OZO30zsnupVBN2MqW2mg==",
"license": "MIT",
"engines": {
"node": ">=12.16"
}
},
"node_modules/@supabase/auth-js": {
"version": "2.75.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.75.0.tgz",
"integrity": "sha512-J8TkeqCOMCV4KwGKVoxmEBuDdHRwoInML2vJilthOo7awVCro2SM+tOcpljORwuBQ1vHUtV62Leit+5wlxrNtw==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "2.6.15"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.75.0",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.75.0.tgz",
"integrity": "sha512-18yk07Moj/xtQ28zkqswxDavXC3vbOwt1hDuYM3/7xPnwwpKnsmPyZ7bQ5th4uqiJzQ135t74La9tuaxBR6e7w==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "2.6.15"
}
},
"node_modules/@supabase/node-fetch": {
"version": "2.6.15",
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "2.75.0",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.75.0.tgz",
"integrity": "sha512-YfBz4W/z7eYCFyuvHhfjOTTzRrQIvsMG2bVwJAKEVVUqGdzqfvyidXssLBG0Fqlql1zJFgtsPpK1n4meHrI7tg==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "2.6.15"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.75.0",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.75.0.tgz",
"integrity": "sha512-B4Xxsf2NHd5cEnM6MGswOSPSsZKljkYXpvzKKmNxoUmNQOfB7D8HOa6NwHcUBSlxcjV+vIrYKcYXtavGJqeGrw==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "2.6.15",
"@types/phoenix": "^1.6.6",
"@types/ws": "^8.18.1",
"ws": "^8.18.2"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.75.0",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.75.0.tgz",
"integrity": "sha512-wpJMYdfFDckDiHQaTpK+Ib14N/O2o0AAWWhguKvmmMurB6Unx17GGmYp5rrrqCTf8S1qq4IfIxTXxS4hzrUySg==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "2.6.15"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.75.0",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.75.0.tgz",
"integrity": "sha512-8UN/vATSgS2JFuJlMVr51L3eUDz+j1m7Ww63wlvHLKULzCDaVWYzvacCjBTLW/lX/vedI2LBI4Vg+01G9ufsJQ==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.75.0",
"@supabase/functions-js": "2.75.0",
"@supabase/node-fetch": "2.6.15",
"@supabase/postgrest-js": "2.75.0",
"@supabase/realtime-js": "2.75.0",
"@supabase/storage-js": "2.75.0"
}
},
"node_modules/@swc/core": { "node_modules/@swc/core": {
"version": "1.13.2", "version": "1.13.2",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.2.tgz", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.2.tgz",
@@ -2960,12 +3060,17 @@
"version": "22.16.5", "version": "22.16.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz",
"integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==", "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/phoenix": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
"license": "MIT"
},
"node_modules/@types/prop-types": { "node_modules/@types/prop-types": {
"version": "15.7.13", "version": "15.7.13",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
@@ -3000,6 +3105,15 @@
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.38.0", "version": "8.38.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
@@ -6750,6 +6864,12 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/ts-api-utils": { "node_modules/ts-api-utils": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@@ -6831,7 +6951,6 @@
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
@@ -7029,6 +7148,22 @@
} }
} }
}, },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -7150,6 +7285,27 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/yaml": { "node_modules/yaml": {
"version": "2.6.0", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",

View File

@@ -40,6 +40,9 @@
"@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.7",
"@stripe/react-stripe-js": "^5.2.0",
"@stripe/stripe-js": "^8.0.0",
"@supabase/supabase-js": "^2.75.0",
"@tanstack/react-query": "^5.83.0", "@tanstack/react-query": "^5.83.0",
"@types/google.maps": "^3.58.1", "@types/google.maps": "^3.58.1",
"apexcharts": "^3.45.2", "apexcharts": "^3.45.2",

6
src/lib/supabase.ts Normal file
View File

@@ -0,0 +1,6 @@
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || '';
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY || '';
export const supabase = createClient(supabaseUrl, supabaseAnonKey);

View File

@@ -1,5 +1,7 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { reviewService, ReviewStats, type Review, type ReviewReply } from '@/services/reviewService';
import { useToast } from '@/hooks/use-toast';
import { import {
Star, Star,
ThumbsUp, ThumbsUp,
@@ -23,114 +25,50 @@ import { Progress } from '@/components/ui/progress';
import { ReviewReplyDialog } from '@/components/dashboard/ReviewReplyDialog'; import { ReviewReplyDialog } from '@/components/dashboard/ReviewReplyDialog';
import { ReviewPhotoUpload } from '@/components/dashboard/ReviewPhotoUpload'; import { ReviewPhotoUpload } from '@/components/dashboard/ReviewPhotoUpload';
interface ReviewReply {
id: string;
authorId: string;
authorName: string;
authorAvatar: string;
content: string;
createdAt: string;
images?: string[];
}
interface Review {
id: string;
itemId: string;
userId: string;
userName: string;
userAvatar: string;
rating: number;
comment: string;
images: string[];
createdAt: string;
helpful: number;
isHelpful: boolean;
replies: ReviewReply[];
canReply: boolean;
}
const Reviews = () => { const Reviews = () => {
const { user } = useAuth(); const { user } = useAuth();
const { toast } = useToast();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [replyingToReview, setReplyingToReview] = useState<string | null>(null); const [replyingToReview, setReplyingToReview] = useState<string | null>(null);
const [replyContent, setReplyContent] = useState(''); const [replyContent, setReplyContent] = useState('');
const [showPhotoUpload, setShowPhotoUpload] = useState(false); const [showPhotoUpload, setShowPhotoUpload] = useState(false);
const [selectedReviewForReply, setSelectedReviewForReply] = useState<Review | null>(null); const [selectedReviewForReply, setSelectedReviewForReply] = useState<Review | null>(null);
const [reviews, setReviews] = useState<Review[]>([]);
const [ratingStats, setRatingStats] = useState<ReviewStats>({
average: 0,
totalRatings: 0,
totalReviews: 0,
breakdown: {}
});
// Sample reviews data with full functionality useEffect(() => {
const [reviews, setReviews] = useState<Review[]>([ loadReviews();
{ loadStats();
id: '1', }, []);
itemId: 'item_001',
userId: 'user_001', const loadReviews = async () => {
userName: 'Ethan Blackwood', try {
userAvatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=center', setLoading(true);
rating: 3.5, const data = await reviewService.getReviews();
comment: 'There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which.', setReviews(data);
images: [ } catch (error) {
'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=200&h=200&fit=crop', console.error('Error loading reviews:', error);
'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=200&h=200&fit=crop', toast({
'https://images.unsplash.com/photo-1551632811-561732d1e306?w=200&h=200&fit=crop' title: 'Error',
], description: 'Failed to load reviews',
createdAt: '25 Oct 2023 at 12:27 pm', variant: 'destructive',
helpful: 16, });
isHelpful: false, } finally {
canReply: true, setLoading(false);
replies: [
{
id: 'reply_1',
authorId: 'owner_001',
authorName: 'Hotel Owner',
authorAvatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop&crop=center',
content: 'Thank you for your feedback! We appreciate your honest review and are working to improve our services.',
createdAt: '26 Oct 2023 at 10:15 am',
images: []
}
]
},
{
id: '2',
itemId: 'item_001',
userId: 'user_002',
userName: 'Gabriel North',
userAvatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=100&h=100&fit=crop&crop=center',
rating: 4.0,
comment: 'This is some content from a media component. You can replace this with any content and adjust it as needed.',
images: [],
createdAt: '25 Oct 2023 at 12:27 pm',
helpful: 16,
isHelpful: true,
canReply: true,
replies: []
},
{
id: '3',
itemId: 'item_001',
userId: 'user_003',
userName: 'Pranoti Deshpande',
userAvatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b5bc?w=100&h=100&fit=crop&crop=center',
rating: 3.5,
comment: 'There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don\'t look even slightly believable.',
images: [],
createdAt: '25 Oct 2023 at 12:27 pm',
helpful: 8,
isHelpful: false,
canReply: true,
replies: []
} }
]); };
// Rating statistics const loadStats = async () => {
const ratingStats = { try {
average: 4.3, const stats = await reviewService.getReviewStats();
totalRatings: 2525, setRatingStats(stats);
totalReviews: 293, } catch (error) {
breakdown: { console.error('Error loading stats:', error);
5: { count: 1138, percentage: 45 },
4: { count: 883, percentage: 35 },
3: { count: 379, percentage: 15 },
2: { count: 808, percentage: 32 },
1: { count: 1742, percentage: 69 }
} }
}; };
@@ -173,41 +111,57 @@ const Reviews = () => {
return stars; return stars;
}; };
const handleHelpfulClick = (reviewId: string) => { const handleHelpfulClick = async (reviewId: string) => {
setReviews(prevReviews => try {
prevReviews.map(review => await reviewService.markAsHelpful(reviewId);
review.id === reviewId setReviews(prevReviews =>
? { prevReviews.map(review =>
...review, review.id === reviewId
isHelpful: !review.isHelpful, ? {
helpful: review.isHelpful ? review.helpful - 1 : review.helpful + 1 ...review,
} isHelpful: !review.isHelpful,
: review helpful: review.isHelpful ? review.helpful - 1 : review.helpful + 1
) }
); : review
)
);
} catch (error) {
console.error('Error marking review as helpful:', error);
toast({
title: 'Error',
description: 'Failed to mark review as helpful',
variant: 'destructive',
});
}
}; };
const handleReplySubmit = (reviewId: string, content: string, images: string[] = []) => { const handleReplySubmit = async (reviewId: string, content: string, images: string[] = []) => {
const newReply: ReviewReply = { try {
id: `reply_${Date.now()}`, const newReply = await reviewService.createReply(reviewId, content, images);
authorId: user?.id || 'current_user',
authorName: user?.name || 'Business Owner',
authorAvatar: user?.avatar || 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100&h=100&fit=crop&crop=center',
content,
createdAt: new Date().toLocaleString(),
images
};
setReviews(prevReviews => setReviews(prevReviews =>
prevReviews.map(review => prevReviews.map(review =>
review.id === reviewId review.id === reviewId
? { ...review, replies: [...review.replies, newReply] } ? { ...review, replies: [...review.replies, newReply] }
: review : review
) )
); );
setReplyingToReview(null); toast({
setReplyContent(''); title: 'Success',
description: 'Reply posted successfully',
});
setReplyingToReview(null);
setReplyContent('');
} catch (error) {
console.error('Error posting reply:', error);
toast({
title: 'Error',
description: 'Failed to post reply',
variant: 'destructive',
});
}
}; };
const openReplyDialog = (review: Review) => { const openReplyDialog = (review: Review) => {

View File

@@ -0,0 +1,172 @@
import { supabase } from '@/lib/supabase';
const API_BASE_URL = 'https://karibeo.lesoluciones.net:8443/api';
export interface Review {
id: string;
itemId: string;
userId: string;
userName: string;
userAvatar: string;
rating: number;
comment: string;
images: string[];
createdAt: string;
helpful: number;
isHelpful: boolean;
replies: ReviewReply[];
canReply: boolean;
}
export interface ReviewReply {
id: string;
authorId: string;
authorName: string;
authorAvatar: string;
content: string;
createdAt: string;
images?: string[];
}
export interface ReviewStats {
average: number;
totalRatings: number;
totalReviews: number;
breakdown: {
[key: number]: {
count: number;
percentage: number;
};
};
}
class ReviewService {
private async getAuthToken(): Promise<string | null> {
const { data: { session } } = await supabase.auth.getSession();
return session?.access_token || null;
}
async getReviews(itemId?: string): Promise<Review[]> {
const token = await this.getAuthToken();
const url = itemId
? `${API_BASE_URL}/reviews?item_id=${itemId}`
: `${API_BASE_URL}/reviews`;
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to fetch reviews');
}
const data = await response.json();
return data.map((review: any) => ({
id: review.id,
itemId: review.item_id,
userId: review.user_id,
userName: review.user_name || 'Anonymous',
userAvatar: review.user_avatar || '',
rating: review.rating,
comment: review.comment,
images: review.images || [],
createdAt: new Date(review.created_at).toLocaleString(),
helpful: review.helpful_count || 0,
isHelpful: review.is_helpful || false,
replies: review.replies || [],
canReply: true,
}));
}
async createReview(data: {
itemId: string;
rating: number;
comment: string;
images?: string[];
}): Promise<Review> {
const token = await this.getAuthToken();
const response = await fetch(`${API_BASE_URL}/reviews`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
item_id: data.itemId,
rating: data.rating,
comment: data.comment,
images: data.images || [],
}),
});
if (!response.ok) {
throw new Error('Failed to create review');
}
return await response.json();
}
async createReply(reviewId: string, content: string, images?: string[]): Promise<ReviewReply> {
const token = await this.getAuthToken();
const response = await fetch(`${API_BASE_URL}/reviews/${reviewId}/replies`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
content,
images: images || [],
}),
});
if (!response.ok) {
throw new Error('Failed to create reply');
}
return await response.json();
}
async markAsHelpful(reviewId: string): Promise<void> {
const token = await this.getAuthToken();
const response = await fetch(`${API_BASE_URL}/reviews/${reviewId}/helpful`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to mark review as helpful');
}
}
async getReviewStats(itemId?: string): Promise<ReviewStats> {
const token = await this.getAuthToken();
const url = itemId
? `${API_BASE_URL}/reviews/stats?item_id=${itemId}`
: `${API_BASE_URL}/reviews/stats`;
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to fetch review stats');
}
return await response.json();
}
}
export const reviewService = new ReviewService();

View File

@@ -0,0 +1,226 @@
import { supabase } from '@/lib/supabase';
const API_BASE_URL = 'https://karibeo.lesoluciones.net:8443/api';
export interface TourismOffer {
id: string;
name: string;
description: string;
type: 'tour' | 'activity' | 'experience' | 'package';
price: number;
currency: string;
duration: string;
location: string;
images: string[];
availability: boolean;
rating: number;
reviewCount: number;
includes: string[];
excludes: string[];
schedule: string;
maxParticipants: number;
minParticipants: number;
category: string;
tags: string[];
createdAt: string;
}
export interface TourismCategory {
id: string;
name: string;
description: string;
icon: string;
offerCount: number;
}
export interface TourBooking {
id: string;
offerId: string;
offerName: string;
date: string;
participants: number;
totalPrice: number;
status: 'pending' | 'confirmed' | 'cancelled' | 'completed';
customerInfo: {
name: string;
email: string;
phone: string;
};
}
class TourismService {
private async getAuthToken(): Promise<string | null> {
const { data: { session } } = await supabase.auth.getSession();
return session?.access_token || null;
}
async getTourismOffers(filters?: {
type?: string;
category?: string;
minPrice?: number;
maxPrice?: number;
location?: string;
}): Promise<TourismOffer[]> {
const token = await this.getAuthToken();
const params = new URLSearchParams();
if (filters) {
if (filters.type) params.append('type', filters.type);
if (filters.category) params.append('category', filters.category);
if (filters.minPrice) params.append('min_price', filters.minPrice.toString());
if (filters.maxPrice) params.append('max_price', filters.maxPrice.toString());
if (filters.location) params.append('location', filters.location);
}
const response = await fetch(`${API_BASE_URL}/tourism/offers?${params}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to fetch tourism offers');
}
return await response.json();
}
async getTourismOffer(offerId: string): Promise<TourismOffer> {
const token = await this.getAuthToken();
const response = await fetch(`${API_BASE_URL}/tourism/offers/${offerId}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to fetch tourism offer');
}
return await response.json();
}
async createTourismOffer(data: Partial<TourismOffer>): Promise<TourismOffer> {
const token = await this.getAuthToken();
const response = await fetch(`${API_BASE_URL}/tourism/offers`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Failed to create tourism offer');
}
return await response.json();
}
async updateTourismOffer(offerId: string, data: Partial<TourismOffer>): Promise<TourismOffer> {
const token = await this.getAuthToken();
const response = await fetch(`${API_BASE_URL}/tourism/offers/${offerId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Failed to update tourism offer');
}
return await response.json();
}
async deleteTourismOffer(offerId: string): Promise<void> {
const token = await this.getAuthToken();
const response = await fetch(`${API_BASE_URL}/tourism/offers/${offerId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to delete tourism offer');
}
}
async getCategories(): Promise<TourismCategory[]> {
const token = await this.getAuthToken();
const response = await fetch(`${API_BASE_URL}/tourism/categories`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to fetch categories');
}
return await response.json();
}
async createTourBooking(data: {
offerId: string;
date: string;
participants: number;
customerInfo: {
name: string;
email: string;
phone: string;
};
}): Promise<TourBooking> {
const token = await this.getAuthToken();
const response = await fetch(`${API_BASE_URL}/tourism/bookings`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Failed to create booking');
}
return await response.json();
}
async getTourBookings(status?: string): Promise<TourBooking[]> {
const token = await this.getAuthToken();
const url = status
? `${API_BASE_URL}/tourism/bookings?status=${status}`
: `${API_BASE_URL}/tourism/bookings`;
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error('Failed to fetch bookings');
}
return await response.json();
}
}
export const tourismService = new TourismService();