Approve tool use
This commit is contained in:
160
package-lock.json
generated
160
package-lock.json
generated
@@ -37,6 +37,9 @@
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@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",
|
||||
"@types/google.maps": "^3.58.1",
|
||||
"apexcharts": "^3.45.2",
|
||||
@@ -2592,6 +2595,103 @@
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"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": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.2.tgz",
|
||||
@@ -2960,12 +3060,17 @@
|
||||
"version": "22.16.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz",
|
||||
"integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"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": {
|
||||
"version": "15.7.13",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
|
||||
@@ -6750,6 +6864,12 @@
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
@@ -6831,7 +6951,6 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -7150,6 +7285,27 @@
|
||||
"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": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
|
||||
|
||||
@@ -40,6 +40,9 @@
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@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",
|
||||
"@types/google.maps": "^3.58.1",
|
||||
"apexcharts": "^3.45.2",
|
||||
|
||||
6
src/lib/supabase.ts
Normal file
6
src/lib/supabase.ts
Normal 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);
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { reviewService, ReviewStats, type Review, type ReviewReply } from '@/services/reviewService';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import {
|
||||
Star,
|
||||
ThumbsUp,
|
||||
@@ -23,114 +25,50 @@ import { Progress } from '@/components/ui/progress';
|
||||
import { ReviewReplyDialog } from '@/components/dashboard/ReviewReplyDialog';
|
||||
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 { user } = useAuth();
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [replyingToReview, setReplyingToReview] = useState<string | null>(null);
|
||||
const [replyContent, setReplyContent] = useState('');
|
||||
const [showPhotoUpload, setShowPhotoUpload] = useState(false);
|
||||
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
|
||||
const [reviews, setReviews] = useState<Review[]>([
|
||||
{
|
||||
id: '1',
|
||||
itemId: 'item_001',
|
||||
userId: 'user_001',
|
||||
userName: 'Ethan Blackwood',
|
||||
userAvatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?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.',
|
||||
images: [
|
||||
'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=200&h=200&fit=crop',
|
||||
'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=200&h=200&fit=crop',
|
||||
'https://images.unsplash.com/photo-1551632811-561732d1e306?w=200&h=200&fit=crop'
|
||||
],
|
||||
createdAt: '25 Oct 2023 at 12:27 pm',
|
||||
helpful: 16,
|
||||
isHelpful: false,
|
||||
canReply: true,
|
||||
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: []
|
||||
}
|
||||
]);
|
||||
useEffect(() => {
|
||||
loadReviews();
|
||||
loadStats();
|
||||
}, []);
|
||||
|
||||
// Rating statistics
|
||||
const ratingStats = {
|
||||
average: 4.3,
|
||||
totalRatings: 2525,
|
||||
totalReviews: 293,
|
||||
breakdown: {
|
||||
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 }
|
||||
const loadReviews = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await reviewService.getReviews();
|
||||
setReviews(data);
|
||||
} catch (error) {
|
||||
console.error('Error loading reviews:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to load reviews',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const stats = await reviewService.getReviewStats();
|
||||
setRatingStats(stats);
|
||||
} catch (error) {
|
||||
console.error('Error loading stats:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -173,7 +111,9 @@ const Reviews = () => {
|
||||
return stars;
|
||||
};
|
||||
|
||||
const handleHelpfulClick = (reviewId: string) => {
|
||||
const handleHelpfulClick = async (reviewId: string) => {
|
||||
try {
|
||||
await reviewService.markAsHelpful(reviewId);
|
||||
setReviews(prevReviews =>
|
||||
prevReviews.map(review =>
|
||||
review.id === reviewId
|
||||
@@ -185,18 +125,19 @@ const Reviews = () => {
|
||||
: 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 newReply: ReviewReply = {
|
||||
id: `reply_${Date.now()}`,
|
||||
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
|
||||
};
|
||||
const handleReplySubmit = async (reviewId: string, content: string, images: string[] = []) => {
|
||||
try {
|
||||
const newReply = await reviewService.createReply(reviewId, content, images);
|
||||
|
||||
setReviews(prevReviews =>
|
||||
prevReviews.map(review =>
|
||||
@@ -206,8 +147,21 @@ const Reviews = () => {
|
||||
)
|
||||
);
|
||||
|
||||
toast({
|
||||
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) => {
|
||||
|
||||
172
src/services/reviewService.ts
Normal file
172
src/services/reviewService.ts
Normal 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();
|
||||
226
src/services/tourismService.ts
Normal file
226
src/services/tourismService.ts
Normal 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();
|
||||
Reference in New Issue
Block a user