fix detail page

Seto committed Oct 24, 2025 at 14:11 UTC 2e5a1db89daea5908f465e1445b8342fe121313d
5 files changed +376 -11
src/components/BarbershopDetail.tsx new
+292
@@ -0,0 +1,292 @@
1 +import { useLang } from "../LanguageContext";
2 +
3 +interface BarbershopDetailProps {
4 + shop: {
5 + id: number;
6 + name: { [key: string]: string };
7 + address: { [key: string]: string };
8 + rating: number;
9 + reviewCount: number;
10 + priceLevel: number;
11 + distance: { [key: string]: string };
12 + openNow: boolean;
13 + phone: string;
14 + types: string[];
15 + imageUrl?: string;
16 + };
17 + onClose: () => void;
18 + onDirections: () => void;
19 +}
20 +
21 +export default function BarbershopDetail({
22 + shop,
23 + onClose,
24 + onDirections,
25 +}: BarbershopDetailProps) {
26 + const { lang } = useLang();
27 + const isRTL = lang === "fa";
28 + const dir = isRTL ? "rtl" : "ltr";
29 +
30 + const getPriceLevelSymbol = (level: number) => {
31 + return "$".repeat(level);
32 + };
33 +
34 + const getTranslation = (key: string) => {
35 + const translations: { [key: string]: { [lang: string]: string } } = {
36 + close: { fa: "بستن", en: "Close", sv: "Stäng" },
37 + call: { fa: "تماس", en: "Call", sv: "Ring" },
38 + directions: { fa: "مسیریابی", en: "Directions", sv: "Vägbeskrivning" },
39 + openNow: { fa: "باز است", en: "Open now", sv: "Öppet nu" },
40 + closed: { fa: "بسته است", en: "Closed", sv: "Stängt" },
41 + reviews: { fa: "نظرات", en: "reviews", sv: "recensioner" },
42 + priceLevel: { fa: "سطح قیمت", en: "Price Level", sv: "Prisnivå" },
43 + distanceAway: { fa: "فاصله", en: "away", sv: "bort" },
44 + about: { fa: "درباره", en: "About", sv: "Om" },
45 + services: { fa: "خدمات", en: "Services", sv: "Tjänster" },
46 + contact: { fa: "تماس", en: "Contact", sv: "Kontakt" },
47 + phoneNumber: {
48 + fa: "شماره تلفن",
49 + en: "Phone Number",
50 + sv: "Telefonnummer",
51 + },
52 + address: { fa: "آدرس", en: "Address", sv: "Adress" },
53 + openingHours: { fa: "ساعات کاری", en: "Opening Hours", sv: "Öppettider" },
54 + monday: { fa: "دوشنبه", en: "Monday", sv: "Måndag" },
55 + tuesday: { fa: "سه‌شنبه", en: "Tuesday", sv: "Tisdag" },
56 + wednesday: { fa: "چهارشنبه", en: "Wednesday", sv: "Onsdag" },
57 + thursday: { fa: "پنج‌شنبه", en: "Thursday", sv: "Torsdag" },
58 + friday: { fa: "جمعه", en: "Friday", sv: "Fredag" },
59 + saturday: { fa: "شنبه", en: "Saturday", sv: "Lördag" },
60 + sunday: { fa: "یکشنبه", en: "Sunday", sv: "Söndag" },
61 + };
62 + return translations[key]?.[lang] || key;
63 + };
64 +
65 + // Mock opening hours data
66 + const openingHours = [
67 + { day: getTranslation("monday"), hours: "09:00 - 18:00" },
68 + { day: getTranslation("tuesday"), hours: "09:00 - 18:00" },
69 + { day: getTranslation("wednesday"), hours: "09:00 - 18:00" },
70 + { day: getTranslation("thursday"), hours: "09:00 - 19:00" },
71 + { day: getTranslation("friday"), hours: "09:00 - 19:00" },
72 + { day: getTranslation("saturday"), hours: "10:00 - 16:00" },
73 + { day: getTranslation("sunday"), hours: getTranslation("closed") },
74 + ];
75 +
76 + return (
77 + <div
78 + className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4 animate-fadeIn"
79 + onClick={onClose}
80 + >
81 + <div
82 + dir={dir}
83 + className="bg-white rounded-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto shadow-2xl animate-slideUp"
84 + onClick={(e) => e.stopPropagation()}
85 + style={{ scrollBehavior: "smooth" }}
86 + >
87 + {/* Header Image */}
88 + <div className="relative w-full h-64 bg-gradient-to-br from-blue-400 to-blue-600 flex items-center justify-center">
89 + <div className="text-white text-8xl">💈</div>
90 + <button
91 + onClick={onClose}
92 + className="absolute top-4 right-4 bg-white bg-opacity-90 hover:bg-opacity-100 rounded-full p-2 transition-all shadow-lg"
93 + aria-label={getTranslation("close")}
94 + >
95 + <svg
96 + className="w-6 h-6 text-gray-800"
97 + viewBox="0 0 24 24"
98 + fill="none"
99 + stroke="currentColor"
100 + strokeWidth="2"
101 + strokeLinecap="round"
102 + strokeLinejoin="round"
103 + >
104 + <line x1="18" y1="6" x2="6" y2="18" />
105 + <line x1="6" y1="6" x2="18" y2="18" />
106 + </svg>
107 + </button>
108 + </div>
109 +
110 + {/* Content */}
111 + <div className="p-6">
112 + {/* Title and Status */}
113 + <div className="mb-4">
114 + <h2 className="text-3xl font-bold text-gray-900 mb-2">
115 + {shop.name[lang]}
116 + </h2>
117 + <div className="flex items-center gap-2 flex-wrap">
118 + <span className="text-sm text-gray-500">
119 + {shop.types.join(" • ")}
120 + </span>
121 + <span className="text-sm text-gray-400">•</span>
122 + <span
123 + className={`text-sm font-semibold ${
124 + shop.openNow ? "text-green-600" : "text-red-600"
125 + }`}
126 + >
127 + {shop.openNow
128 + ? getTranslation("openNow")
129 + : getTranslation("closed")}
130 + </span>
131 + </div>
132 + </div>
133 +
134 + {/* Rating and Info */}
135 + <div className="flex items-center gap-4 mb-6 pb-6 border-b">
136 + <div className="flex items-center gap-1">
137 + <svg
138 + className="w-5 h-5 fill-yellow-400 text-yellow-400"
139 + viewBox="0 0 24 24"
140 + >
141 + <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
142 + </svg>
143 + <span className="font-bold text-lg text-gray-900">
144 + {shop.rating.toFixed(1)}
145 + </span>
146 + </div>
147 + <span className="text-sm text-gray-600">
148 + ({shop.reviewCount} {getTranslation("reviews")})
149 + </span>
150 + <span className="text-sm text-gray-400">•</span>
151 + <span className="text-sm font-semibold text-gray-700">
152 + {getPriceLevelSymbol(shop.priceLevel)}
153 + </span>
154 + <span className="text-sm text-gray-400">•</span>
155 + <span className="text-sm text-gray-600">
156 + {shop.distance[lang]} {getTranslation("distanceAway")}
157 + </span>
158 + </div>
159 +
160 + {/* Action Buttons */}
161 + <div className="grid grid-cols-2 gap-3 mb-6">
162 + <button
163 + onClick={onDirections}
164 + className="flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg font-semibold transition-colors"
165 + >
166 + <svg
167 + className="w-5 h-5"
168 + viewBox="0 0 24 24"
169 + fill="none"
170 + stroke="currentColor"
171 + strokeWidth="2"
172 + strokeLinecap="round"
173 + strokeLinejoin="round"
174 + >
175 + <polygon points="3 11 22 2 13 21 11 13 3 11" />
176 + </svg>
177 + {getTranslation("directions")}
178 + </button>
179 + <a
180 + href={`tel:${shop.phone}`}
181 + className="flex items-center justify-center gap-2 bg-gray-100 hover:bg-gray-200 text-gray-700 px-6 py-3 rounded-lg font-semibold transition-colors"
182 + onClick={(e) => e.stopPropagation()}
183 + >
184 + <svg
185 + className="w-5 h-5"
186 + viewBox="0 0 24 24"
187 + fill="none"
188 + stroke="currentColor"
189 + strokeWidth="2"
190 + strokeLinecap="round"
191 + strokeLinejoin="round"
192 + >
193 + <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" />
194 + </svg>
195 + {getTranslation("call")}
196 + </a>
197 + </div>
198 +
199 + {/* Contact Information */}
200 + <div className="mb-6">
201 + <h3 className="text-lg font-bold text-gray-900 mb-3">
202 + {getTranslation("contact")}
203 + </h3>
204 +
205 + {/* Address */}
206 + <div className="flex items-start gap-3 mb-3">
207 + <svg
208 + className="w-5 h-5 text-gray-500 mt-0.5 flex-shrink-0"
209 + viewBox="0 0 24 24"
210 + fill="none"
211 + stroke="currentColor"
212 + strokeWidth="2"
213 + strokeLinecap="round"
214 + strokeLinejoin="round"
215 + >
216 + <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
217 + <circle cx="12" cy="10" r="3" />
218 + </svg>
219 + <div>
220 + <p className="text-sm font-semibold text-gray-700 mb-1">
221 + {getTranslation("address")}
222 + </p>
223 + <p className="text-sm text-gray-600">{shop.address[lang]}</p>
224 + </div>
225 + </div>
226 +
227 + {/* Phone */}
228 + <div className="flex items-start gap-3">
229 + <svg
230 + className="w-5 h-5 text-gray-500 mt-0.5 flex-shrink-0"
231 + viewBox="0 0 24 24"
232 + fill="none"
233 + stroke="currentColor"
234 + strokeWidth="2"
235 + strokeLinecap="round"
236 + strokeLinejoin="round"
237 + >
238 + <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" />
239 + </svg>
240 + <div>
241 + <p className="text-sm font-semibold text-gray-700 mb-1">
242 + {getTranslation("phoneNumber")}
243 + </p>
244 + <a
245 + href={`tel:${shop.phone}`}
246 + className="text-sm text-blue-600 hover:underline"
247 + onClick={(e) => e.stopPropagation()}
248 + >
249 + {shop.phone}
250 + </a>
251 + </div>
252 + </div>
253 + </div>
254 +
255 + {/* Opening Hours */}
256 + <div className="mb-6">
257 + <h3 className="text-lg font-bold text-gray-900 mb-3">
258 + {getTranslation("openingHours")}
259 + </h3>
260 + <div className="space-y-2">
261 + {openingHours.map((item, index) => (
262 + <div key={index} className="flex justify-between text-sm">
263 + <span className="text-gray-600">{item.day}</span>
264 + <span className="font-medium text-gray-900">
265 + {item.hours}
266 + </span>
267 + </div>
268 + ))}
269 + </div>
270 + </div>
271 +
272 + {/* Services */}
273 + <div className="pb-2">
274 + <h3 className="text-lg font-bold text-gray-900 mb-3">
275 + {getTranslation("services")}
276 + </h3>
277 + <div className="flex flex-wrap gap-2">
278 + {shop.types.map((type, index) => (
279 + <span
280 + key={index}
281 + className="bg-blue-100 text-blue-800 px-3 py-1.5 rounded-full text-sm font-medium hover:bg-blue-200 transition-colors"
282 + >
283 + {type}
284 + </span>
285 + ))}
286 + </div>
287 + </div>
288 + </div>
289 + </div>
290 + </div>
291 + );
292 +}
src/components/CutCard.tsx
+17 -6
@@ -1,4 +1,5 @@
1 import { useLang } from "../LanguageContext";
2 +import { openUrl } from "@tauri-apps/plugin-opener";
3
4 interface CutCardProps {
5 name: string;
@@ -169,14 +170,24 @@ export default function CutCard({
170 </button>
171 <button
172 className="flex-1 bg-gray-100 hover:bg-gray-200 text-gray-700 px-4 py-2 rounded-lg font-medium transition-colors text-sm"
172 - onClick={(e) => {
173 + onClick={async (e) => {
174 e.stopPropagation();
174 - window.open(
175 - `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
175 + try {
176 + // Try to open in native maps app
177 + const mapsUrl = `maps://?q=${encodeURIComponent(address)}`;
178 + const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
179 address,
177 - )}`,
178 - "_blank",
179 - );
180 + )}`;
181 +
182 + try {
183 + await openUrl(mapsUrl);
184 + } catch {
185 + // If maps:// doesn't work, try Google Maps URL
186 + await openUrl(googleMapsUrl);
187 + }
188 + } catch (error) {
189 + console.error("Failed to open maps:", error);
190 + }
191 }}
192 >
193 {lang === "fa"
src/index.css
+27
@@ -65,4 +65,31 @@
65 transform: rotate(0);
66 }
67 }
68 +
69 + /* Modal animations */
70 + .animate-fadeIn {
71 + animation: modalFadeIn 0.2s ease-out;
72 + }
73 + @keyframes modalFadeIn {
74 + from {
75 + opacity: 0;
76 + }
77 + to {
78 + opacity: 1;
79 + }
80 + }
81 +
82 + .animate-slideUp {
83 + animation: modalSlideUp 0.3s ease-out;
84 + }
85 + @keyframes modalSlideUp {
86 + from {
87 + opacity: 0;
88 + transform: translateY(20px);
89 + }
90 + to {
91 + opacity: 1;
92 + transform: translateY(0);
93 + }
94 + }
95 }
src/pages/Cut.tsx
+40 -2
@@ -1,6 +1,9 @@
1 +import { useState } from "react";
2 import CutCard from "../components/CutCard";
3 +import BarbershopDetail from "../components/BarbershopDetail";
4 import { useLang } from "../LanguageContext";
5 import haptics from "../utils/haptics";
6 +import { openUrl } from "@tauri-apps/plugin-opener";
7
8 interface Barbershop {
9 id: number;
@@ -213,11 +216,32 @@ const BARBERSHOPS: Barbershop[] = [
216 ];
217
218 export default function Cut() {
219 + const [selectedShop, setSelectedShop] = useState<Barbershop | null>(null);
220 const { t, lang } = useLang();
221 const isRTL = lang === "fa";
222 const dir = isRTL ? "rtl" : "ltr";
223 const align = isRTL ? "text-right" : "text-left";
224
225 + const handleDirections = async (shop: Barbershop) => {
226 + const address = shop.address[lang];
227 + // Try to open in native maps app using Tauri opener
228 + try {
229 + // For macOS, use maps:// URL scheme
230 + // For other platforms, fall back to Google Maps URL
231 + const mapsUrl = `maps://?q=${encodeURIComponent(address)}`;
232 + const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(address)}`;
233 +
234 + try {
235 + await openUrl(mapsUrl);
236 + } catch {
237 + // If maps:// doesn't work, try Google Maps URL
238 + await openUrl(googleMapsUrl);
239 + }
240 + } catch (error) {
241 + console.error("Failed to open maps:", error);
242 + }
243 + };
244 +
245 return (
246 <section dir={dir} className={`pb-12 ` + align}>
247 <h2 className="text-2xl font-bold mb-2 text-blue-700">{t.cut}</h2>
@@ -236,12 +260,26 @@ export default function Cut() {
260 types={shop.types}
261 onViewDetails={async () => {
262 await haptics.cardTap();
239 - // Could open a modal or navigate to details page
240 - console.log("Selected shop:", shop);
263 + setSelectedShop(shop);
264 }}
265 />
266 ))}
267 </div>
268 +
269 + {/* Detail Modal */}
270 + {selectedShop && (
271 + <BarbershopDetail
272 + shop={selectedShop}
273 + onClose={async () => {
274 + await haptics.buttonPress();
275 + setSelectedShop(null);
276 + }}
277 + onDirections={async () => {
278 + await haptics.buttonPress();
279 + await handleDirections(selectedShop);
280 + }}
281 + />
282 + )}
283 </section>
284 );
285 }
src/pages/Profile.tsx
-3
@@ -23,9 +23,6 @@ export default function Profile() {
23 <div className="mb-2">
24 <span className="font-semibold">{t.user}:</span> {t.guest}
25 </div>
26 - <div>
27 - <span className="font-semibold">{t.progress}:</span> 2/2 {t.completed}
28 - </div>
26 </div>
27
28 <div className="bg-white/80 shadow rounded-xl px-8 py-4 mt-2 w-full max-w-xs">