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">