update design
Seto committed
Oct 24, 2025 at 13:46 UTC
3dddcb8ac081289e43f0eea4fa02f5371ced7e83
13 files changed
+499
-482
assets/app-icon.jpg
Binary files a/assets/app-icon.jpg and b/assets/app-icon.jpg differ
src/App.tsx
+3
-3
@@ -4,7 +4,7 @@ import * as Tooltip from "@radix-ui/react-tooltip";
4
import Navbar from "./components/Navbar";
5
import AnimatedPage from "./components/AnimatedPage";
6
import Home from "./pages/Home";
7
-import Lessons from "./pages/Lessons";
7
+import Cut from "./pages/Cut";
8
import Profile from "./pages/Profile";
9
10
export default function App() {
@@ -24,10 +24,10 @@ export default function App() {
24
}
25
/>
26
<Route
27
- path="/lessons"
27
+ path="/cut"
28
element={
29
<AnimatedPage>
30
- <Lessons />
30
+ <Cut />
31
</AnimatedPage>
32
}
33
/>
src/LanguageContext.tsx
+1
-1
@@ -16,7 +16,7 @@ interface LanguageProviderProps {
16
}
17
18
export function LanguageProvider({ children }: LanguageProviderProps) {
19
- const [lang, setLang] = useState<SupportedLanguage>("fa");
19
+ const [lang, setLang] = useState<SupportedLanguage>("en");
20
const t = translations[lang];
21
22
return (
src/components/CutCard.tsx
new
+192
@@ -0,0 +1,192 @@
1
+import { useLang } from "../LanguageContext";
2
+
3
+interface CutCardProps {
4
+ name: string;
5
+ address: string;
6
+ rating: number;
7
+ reviewCount: number;
8
+ priceLevel: number;
9
+ distance: string;
10
+ openNow: boolean;
11
+ phone: string;
12
+ types: string[];
13
+ onViewDetails: () => void;
14
+}
15
+
16
+export default function CutCard({
17
+ name,
18
+ address,
19
+ rating,
20
+ reviewCount,
21
+ priceLevel,
22
+ distance,
23
+ openNow,
24
+ phone,
25
+ types,
26
+ onViewDetails,
27
+}: CutCardProps) {
28
+ const { lang } = useLang();
29
+ const isRTL = lang === "fa";
30
+ const dir = isRTL ? "rtl" : "ltr";
31
+
32
+ const getPriceLevelSymbol = (level: number) => {
33
+ return "$".repeat(level);
34
+ };
35
+
36
+ return (
37
+ <div
38
+ dir={dir}
39
+ className="bg-white shadow-md rounded-lg overflow-hidden hover:shadow-lg transition-shadow duration-200 border border-gray-200 cursor-pointer"
40
+ onClick={onViewDetails}
41
+ >
42
+ {/* Image placeholder */}
43
+ <div className="w-full h-40 bg-gradient-to-br from-blue-400 to-blue-600 flex items-center justify-center">
44
+ <div className="text-white text-6xl">💈</div>
45
+ </div>
46
+
47
+ {/* Content */}
48
+ <div className="p-4">
49
+ {/* Name and Types */}
50
+ <div className="mb-2">
51
+ <h3 className="font-bold text-lg text-gray-900 mb-1">{name}</h3>
52
+ <p className="text-sm text-gray-500">{types.join(" • ")}</p>
53
+ </div>
54
+
55
+ {/* Rating and Reviews */}
56
+ <div className="flex items-center gap-2 mb-2">
57
+ <div className="flex items-center gap-1">
58
+ <svg
59
+ className="w-4 h-4 fill-yellow-400 text-yellow-400"
60
+ viewBox="0 0 24 24"
61
+ xmlns="http://www.w3.org/2000/svg"
62
+ >
63
+ <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" />
64
+ </svg>
65
+ <span className="font-semibold text-sm text-gray-900">
66
+ {rating.toFixed(1)}
67
+ </span>
68
+ </div>
69
+ <span className="text-sm text-gray-500">({reviewCount})</span>
70
+ <span className="text-sm text-gray-400">•</span>
71
+ <span className="text-sm text-gray-600">
72
+ {getPriceLevelSymbol(priceLevel)}
73
+ </span>
74
+ </div>
75
+
76
+ {/* Address */}
77
+ <div className="flex items-start gap-2 mb-2">
78
+ <svg
79
+ className="w-4 h-4 text-gray-500 mt-0.5 flex-shrink-0"
80
+ viewBox="0 0 24 24"
81
+ fill="none"
82
+ stroke="currentColor"
83
+ strokeWidth="2"
84
+ strokeLinecap="round"
85
+ strokeLinejoin="round"
86
+ xmlns="http://www.w3.org/2000/svg"
87
+ >
88
+ <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
89
+ <circle cx="12" cy="10" r="3" />
90
+ </svg>
91
+ <p className="text-sm text-gray-600">{address}</p>
92
+ </div>
93
+
94
+ {/* Distance and Status */}
95
+ <div className="flex items-center gap-3 mb-3">
96
+ <span className="text-sm text-gray-600">{distance}</span>
97
+ <span className="text-sm text-gray-400">•</span>
98
+ <div className="flex items-center gap-1">
99
+ <svg
100
+ className="w-4 h-4 text-gray-500"
101
+ viewBox="0 0 24 24"
102
+ fill="none"
103
+ stroke="currentColor"
104
+ strokeWidth="2"
105
+ strokeLinecap="round"
106
+ strokeLinejoin="round"
107
+ xmlns="http://www.w3.org/2000/svg"
108
+ >
109
+ <circle cx="12" cy="12" r="10" />
110
+ <polyline points="12 6 12 12 16 14" />
111
+ </svg>
112
+ <span
113
+ className={`text-sm font-medium ${
114
+ openNow ? "text-green-600" : "text-red-600"
115
+ }`}
116
+ >
117
+ {openNow
118
+ ? lang === "fa"
119
+ ? "باز است"
120
+ : lang === "sv"
121
+ ? "Öppet nu"
122
+ : "Open now"
123
+ : lang === "fa"
124
+ ? "بسته است"
125
+ : lang === "sv"
126
+ ? "Stängt"
127
+ : "Closed"}
128
+ </span>
129
+ </div>
130
+ </div>
131
+
132
+ {/* Phone */}
133
+ <div className="flex items-center gap-2 mb-4">
134
+ <svg
135
+ className="w-4 h-4 text-gray-500"
136
+ viewBox="0 0 24 24"
137
+ fill="none"
138
+ stroke="currentColor"
139
+ strokeWidth="2"
140
+ strokeLinecap="round"
141
+ strokeLinejoin="round"
142
+ xmlns="http://www.w3.org/2000/svg"
143
+ >
144
+ <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" />
145
+ </svg>
146
+ <a
147
+ href={`tel:${phone}`}
148
+ className="text-sm text-blue-600 hover:underline"
149
+ onClick={(e) => e.stopPropagation()}
150
+ >
151
+ {phone}
152
+ </a>
153
+ </div>
154
+
155
+ {/* Action Buttons */}
156
+ <div className="flex gap-2">
157
+ <button
158
+ className="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition-colors text-sm"
159
+ onClick={(e) => {
160
+ e.stopPropagation();
161
+ onViewDetails();
162
+ }}
163
+ >
164
+ {lang === "fa"
165
+ ? "مشاهده جزئیات"
166
+ : lang === "sv"
167
+ ? "Se detaljer"
168
+ : "View Details"}
169
+ </button>
170
+ <button
171
+ 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
+ e.stopPropagation();
174
+ window.open(
175
+ `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(
176
+ address,
177
+ )}`,
178
+ "_blank",
179
+ );
180
+ }}
181
+ >
182
+ {lang === "fa"
183
+ ? "مسیریابی"
184
+ : lang === "sv"
185
+ ? "Vägbeskrivning"
186
+ : "Directions"}
187
+ </button>
188
+ </div>
189
+ </div>
190
+ </div>
191
+ );
192
+}
src/components/LessonCard.tsx
deleted
-41
@@ -1,41 +0,0 @@
1
-import * as Tooltip from "@radix-ui/react-tooltip";
2
-import { useLang } from "../LanguageContext";
3
-
4
-interface LessonCardProps {
5
- title: string;
6
- description: string;
7
- onStart: () => void;
8
-}
9
-
10
-export default function LessonCard({
11
- title,
12
- description,
13
- onStart,
14
-}: LessonCardProps) {
15
- const { t, lang } = useLang();
16
- const isRTL = lang === "fa";
17
- const dir = isRTL ? "rtl" : "ltr";
18
- const align = isRTL ? "text-right" : "text-left";
19
- return (
20
- <div
21
- dir={dir}
22
- className={`bg-white shadow rounded-xl p-6 flex flex-col gap-2 border hover:scale-105 transition-transform duration-200 ${align}`}
23
- >
24
- <h3 className="font-bold text-lg text-fuchsia-700">{title}</h3>
25
- <p className="text-slate-600">{description}</p>
26
- <Tooltip.Root>
27
- <Tooltip.Trigger asChild>
28
- <button
29
- className="mt-2 self-end bg-fuchsia-500 hover:bg-fuchsia-600 text-white px-4 py-1.5 rounded-lg font-medium transition-all focus:ring-2 focus:ring-fuchsia-300"
30
- onClick={onStart}
31
- >
32
- {t.startLesson}
33
- </button>
34
- </Tooltip.Trigger>
35
- <Tooltip.Content className="text-xs bg-white border px-2 py-1 rounded shadow">
36
- {t.startThisLesson}
37
- </Tooltip.Content>
38
- </Tooltip.Root>
39
- </div>
40
- );
41
-}
src/components/Navbar.tsx
+11
-25
@@ -1,12 +1,11 @@
1
import { Link, useLocation } from "react-router-dom";
2
import * as Tooltip from "@radix-ui/react-tooltip";
3
import { useLang } from "../LanguageContext";
4
-import { SupportedLanguage } from "../i18n";
4
import haptics from "./../utils/haptics";
5
6
interface NavItem {
7
path: string;
9
- key: "home" | "lessons" | "profile";
8
+ key: "home" | "cut" | "profile";
9
icon: React.JSX.Element;
10
}
11
@@ -18,7 +17,7 @@ const navItems: NavItem[] = [
17
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
18
<path
19
d="M3 10.5L12 4l9 6.5V20a1 1 0 0 1-1 1h-3a1 1 0 0 1-1-1v-4h-4v4a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V10.5z"
21
- stroke="#a21caf"
20
+ stroke="#0088E0"
21
strokeWidth="1.5"
22
strokeLinejoin="round"
23
/>
@@ -26,13 +25,13 @@ const navItems: NavItem[] = [
25
),
26
},
27
{
29
- path: "/lessons",
30
- key: "lessons",
28
+ path: "/cut",
29
+ key: "cut",
30
icon: (
31
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
32
<path
33
d="M4 19V6a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v13M9 6v13"
35
- stroke="#a21caf"
34
+ stroke="#0088E0"
35
strokeWidth="1.5"
36
strokeLinejoin="round"
37
/>
@@ -44,10 +43,10 @@ const navItems: NavItem[] = [
43
key: "profile",
44
icon: (
45
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
47
- <circle cx="12" cy="8" r="4" stroke="#a21caf" strokeWidth="1.5" />
46
+ <circle cx="12" cy="8" r="4" stroke="#0088E0" strokeWidth="1.5" />
47
<path
48
d="M4 20v-1a7 7 0 0 1 14 0v1"
50
- stroke="#a21caf"
49
+ stroke="#0088E0"
50
strokeWidth="1.5"
51
/>
52
</svg>
@@ -57,7 +56,7 @@ const navItems: NavItem[] = [
56
57
export default function Navbar() {
58
const location = useLocation();
60
- const { t, lang, setLang } = useLang();
59
+ const { t, lang } = useLang();
60
const isRTL = lang === "fa";
61
62
const handleNavClick = async () => {
@@ -72,20 +71,7 @@ export default function Navbar() {
71
dir={isRTL ? "rtl" : "ltr"}
72
>
73
<div className="max-w-2xl mx-auto flex flex-row justify-between items-center px-4 py-3 w-full">
75
- <span className="font-bold text-xl text-fuchsia-600">
76
- {t.appName}
77
- </span>
78
- <select
79
- className="rounded border px-2 py-1 bg-white text-sm text-fuchsia-700 cursor-pointer shadow"
80
- value={lang}
81
- onClick={handleNavClick}
82
- onChange={(e) => setLang(e.target.value as SupportedLanguage)}
83
- aria-label="Change language"
84
- >
85
- <option value="fa">فارسی</option>
86
- <option value="en">English</option>
87
- <option value="sv">Svenska</option>
88
- </select>
74
+ <span className="font-bold text-xl text-cut-blue">{t.appName}</span>
75
</div>
76
</div>
77
@@ -101,8 +87,8 @@ export default function Navbar() {
87
className={`flex flex-col items-center justify-center gap-1 px-3 py-1 rounded transition-all duration-200
88
${
89
location.pathname === item.path
104
- ? "bg-fuchsia-100 text-fuchsia-900 shadow font-semibold"
105
- : "hover:bg-fuchsia-50 text-slate-800"
90
+ ? "bg-cut-red-100 text-cut-blue-900 shadow font-semibold"
91
+ : "hover:bg-cut-red-50 text-slate-800"
92
}`}
93
style={{ minWidth: 60 }}
94
>
src/components/QuestionCard.tsx
+4
-4
@@ -62,7 +62,7 @@ export default function QuestionCard({
62
className={`bg-white/80 rounded-xl shadow-lg p-6 mb-4 flex flex-col gap-3 ${align}`}
63
>
64
<div className="flex items-center justify-between mb-2">
65
- <button className="text-fuchsia-600 hover:underline" onClick={onBack}>
65
+ <button className="text-blue-600 hover:underline" onClick={onBack}>
66
{t.back}
67
</button>
68
<div className="text-slate-400 text-xs">
@@ -70,7 +70,7 @@ export default function QuestionCard({
70
{lesson.questions.length}
71
</div>
72
</div>
73
- <div className="font-bold text-lg text-fuchsia-800 mb-2">
73
+ <div className="font-bold text-lg text-blue-800 mb-2">
74
{question.q[lang]}
75
</div>
76
<div className="flex flex-col gap-2 mt-2">
@@ -81,7 +81,7 @@ export default function QuestionCard({
81
${animateBtn === idx ? "wiggle" : ""}
82
${showResult && idx === question.answer ? "bg-green-200 border-green-400 font-bold" : ""}
83
${showResult && idx !== question.answer && idx === animateBtn ? "bg-red-200 border-red-400" : ""}
84
- hover:bg-fuchsia-100
84
+ hover:bg-blue-100
85
`}
86
disabled={showResult}
87
onClick={async () => {
@@ -108,7 +108,7 @@ export default function QuestionCard({
108
)}
109
{step === lesson.questions.length - 1 && showResult && (
110
<div className="mt-6 text-center">
111
- <span className="inline-block bg-fuchsia-200 text-fuchsia-900 px-4 py-2 rounded-xl animate-bounce shadow">
111
+ <span className="inline-block bg-blue-200 text-blue-900 px-4 py-2 rounded-xl animate-bounce shadow">
112
{t.lessonCompleted}
113
</span>
114
</div>
src/i18n.ts
+4
-4
@@ -1,7 +1,7 @@
1
export interface Translation {
2
appName: string;
3
home: string;
4
- lessons: string;
4
+ cut: string;
5
profile: string;
6
welcome: string;
7
homeDesc: string;
@@ -27,7 +27,7 @@ export const translations: Record<SupportedLanguage, Translation> = {
27
fa: {
28
appName: "رومی",
29
home: "خانه",
30
- lessons: "درسها",
30
+ cut: "برش",
31
profile: "پروفایل",
32
welcome: "به رومی خوش آمدید!",
33
homeDesc: "روشی سرگرمکننده برای یادگیری زبان فارسی.",
@@ -49,7 +49,7 @@ export const translations: Record<SupportedLanguage, Translation> = {
49
en: {
50
appName: "StreetCut",
51
home: "Home",
52
- lessons: "Lessons",
52
+ cut: "Cut",
53
profile: "Profile",
54
welcome: "Welcome to StreetCut!",
55
homeDesc: "A fun way to get a fresh look.",
@@ -71,7 +71,7 @@ export const translations: Record<SupportedLanguage, Translation> = {
71
sv: {
72
appName: "StreetCut",
73
home: "Hem",
74
- lessons: "Lektioner",
74
+ cut: "Cut",
75
profile: "Profil",
76
welcome: "Välkommen till StreetCut!",
77
homeDesc: "Ett roligt sätt att lära sig persiska.",
src/pages/Cut.tsx
new
+247
@@ -0,0 +1,247 @@
1
+import CutCard from "../components/CutCard";
2
+import { useLang } from "../LanguageContext";
3
+import haptics from "../utils/haptics";
4
+
5
+interface Barbershop {
6
+ id: number;
7
+ name: { [key: string]: string };
8
+ address: { [key: string]: string };
9
+ rating: number;
10
+ reviewCount: number;
11
+ priceLevel: number;
12
+ distance: { [key: string]: string };
13
+ openNow: boolean;
14
+ phone: string;
15
+ types: string[];
16
+ imageUrl?: string;
17
+}
18
+
19
+// Barbershop locations in Hagersten, Stockholm area
20
+const BARBERSHOPS: Barbershop[] = [
21
+ {
22
+ id: 1,
23
+ name: {
24
+ fa: "هگرستن استریت کات",
25
+ en: "Hagersten Street Cut",
26
+ sv: "Hagersten Street Cut",
27
+ },
28
+ address: {
29
+ fa: "هگرستنس گاتا 15، استکهلم",
30
+ en: "Hågerstensvägen 15, Stockholm",
31
+ sv: "Hågerstensvägen 15, Stockholm",
32
+ },
33
+ rating: 4.8,
34
+ reviewCount: 124,
35
+ priceLevel: 2,
36
+ distance: {
37
+ fa: "500 متر",
38
+ en: "500m",
39
+ sv: "500m",
40
+ },
41
+ openNow: true,
42
+ phone: "+46 8 123 4567",
43
+ types: ["Barbershop", "Hair Salon"],
44
+ },
45
+ {
46
+ id: 2,
47
+ name: {
48
+ fa: "آرایشگاه مردانه نورد",
49
+ en: "Barber Nord",
50
+ sv: "Barber Nord",
51
+ },
52
+ address: {
53
+ fa: "هگرستن تورگ 8، استکهلم",
54
+ en: "Hägerstens Torg 8, Stockholm",
55
+ sv: "Hägerstens Torg 8, Stockholm",
56
+ },
57
+ rating: 4.6,
58
+ reviewCount: 89,
59
+ priceLevel: 2,
60
+ distance: {
61
+ fa: "750 متر",
62
+ en: "750m",
63
+ sv: "750m",
64
+ },
65
+ openNow: true,
66
+ phone: "+46 8 234 5678",
67
+ types: ["Barbershop", "Beard Trimming"],
68
+ },
69
+ {
70
+ id: 3,
71
+ name: {
72
+ fa: "کلاسیک کات استوکهلم",
73
+ en: "Classic Cut Stockholm",
74
+ sv: "Classic Cut Stockholm",
75
+ },
76
+ address: {
77
+ fa: "لیلجهولمنس وگن 22، استکهلم",
78
+ en: "Liljeholmsvägen 22, Stockholm",
79
+ sv: "Liljeholmsvägen 22, Stockholm",
80
+ },
81
+ rating: 4.7,
82
+ reviewCount: 156,
83
+ priceLevel: 3,
84
+ distance: {
85
+ fa: "1.2 کیلومتر",
86
+ en: "1.2km",
87
+ sv: "1.2km",
88
+ },
89
+ openNow: true,
90
+ phone: "+46 8 345 6789",
91
+ types: ["Barbershop", "Hair Salon", "Grooming"],
92
+ },
93
+ {
94
+ id: 4,
95
+ name: {
96
+ fa: "آرایشگاه سوئدی",
97
+ en: "Swedish Barber House",
98
+ sv: "Svenska Barber House",
99
+ },
100
+ address: {
101
+ fa: "هگرستنس آله 45، استکهلم",
102
+ en: "Hägerstensåsens Allé 45, Stockholm",
103
+ sv: "Hägerstensåsens Allé 45, Stockholm",
104
+ },
105
+ rating: 4.5,
106
+ reviewCount: 72,
107
+ priceLevel: 2,
108
+ distance: {
109
+ fa: "900 متر",
110
+ en: "900m",
111
+ sv: "900m",
112
+ },
113
+ openNow: false,
114
+ phone: "+46 8 456 7890",
115
+ types: ["Barbershop", "Traditional Shave"],
116
+ },
117
+ {
118
+ id: 5,
119
+ name: {
120
+ fa: "استایل مستر",
121
+ en: "Style Master",
122
+ sv: "Style Master",
123
+ },
124
+ address: {
125
+ fa: "گران بری وگن 33، استکهلم",
126
+ en: "Granbergsvägen 33, Stockholm",
127
+ sv: "Granbergsvägen 33, Stockholm",
128
+ },
129
+ rating: 4.9,
130
+ reviewCount: 203,
131
+ priceLevel: 3,
132
+ distance: {
133
+ fa: "1.5 کیلومتر",
134
+ en: "1.5km",
135
+ sv: "1.5km",
136
+ },
137
+ openNow: true,
138
+ phone: "+46 8 567 8901",
139
+ types: ["Barbershop", "Hair Salon", "Styling"],
140
+ },
141
+ {
142
+ id: 6,
143
+ name: {
144
+ fa: "آرایشگاه مدرن هگرستن",
145
+ en: "Modern Barber Hagersten",
146
+ sv: "Modern Barber Hagersten",
147
+ },
148
+ address: {
149
+ fa: "هگرستنس گاتا 67، استکهلم",
150
+ en: "Hågerstensvägen 67, Stockholm",
151
+ sv: "Hågerstensvägen 67, Stockholm",
152
+ },
153
+ rating: 4.4,
154
+ reviewCount: 58,
155
+ priceLevel: 2,
156
+ distance: {
157
+ fa: "650 متر",
158
+ en: "650m",
159
+ sv: "650m",
160
+ },
161
+ openNow: true,
162
+ phone: "+46 8 678 9012",
163
+ types: ["Barbershop", "Modern Cuts"],
164
+ },
165
+ {
166
+ id: 7,
167
+ name: {
168
+ fa: "آرایشگاه کلاسیک",
169
+ en: "The Classic Barber",
170
+ sv: "Den Klassiska Frisören",
171
+ },
172
+ address: {
173
+ fa: "تیلوریس گاتا 12، استکهلم",
174
+ en: "Telefonvägen 12, Stockholm",
175
+ sv: "Telefonvägen 12, Stockholm",
176
+ },
177
+ rating: 4.8,
178
+ reviewCount: 145,
179
+ priceLevel: 2,
180
+ distance: {
181
+ fa: "1.8 کیلومتر",
182
+ en: "1.8km",
183
+ sv: "1.8km",
184
+ },
185
+ openNow: true,
186
+ phone: "+46 8 789 0123",
187
+ types: ["Barbershop", "Traditional Service"],
188
+ },
189
+ {
190
+ id: 8,
191
+ name: {
192
+ fa: "آرایشگاه سریع",
193
+ en: "Quick Cuts Barber",
194
+ sv: "Snabbklipp Frisör",
195
+ },
196
+ address: {
197
+ fa: "هگرستن تورگ 2، استکهلم",
198
+ en: "Hägerstens Torg 2, Stockholm",
199
+ sv: "Hägerstens Torg 2, Stockholm",
200
+ },
201
+ rating: 4.3,
202
+ reviewCount: 91,
203
+ priceLevel: 1,
204
+ distance: {
205
+ fa: "800 متر",
206
+ en: "800m",
207
+ sv: "800m",
208
+ },
209
+ openNow: false,
210
+ phone: "+46 8 890 1234",
211
+ types: ["Barbershop", "Walk-in"],
212
+ },
213
+];
214
+
215
+export default function Cut() {
216
+ const { t, lang } = useLang();
217
+ const isRTL = lang === "fa";
218
+ const dir = isRTL ? "rtl" : "ltr";
219
+ const align = isRTL ? "text-right" : "text-left";
220
+
221
+ return (
222
+ <section dir={dir} className={`pb-12 ` + align}>
223
+ <h2 className="text-2xl font-bold mb-2 text-blue-700">{t.cut}</h2>
224
+ <div className="grid gap-4">
225
+ {BARBERSHOPS.map((shop) => (
226
+ <CutCard
227
+ key={shop.id}
228
+ name={shop.name[lang]}
229
+ address={shop.address[lang]}
230
+ rating={shop.rating}
231
+ reviewCount={shop.reviewCount}
232
+ priceLevel={shop.priceLevel}
233
+ distance={shop.distance[lang]}
234
+ openNow={shop.openNow}
235
+ phone={shop.phone}
236
+ types={shop.types}
237
+ onViewDetails={async () => {
238
+ await haptics.cardTap();
239
+ // Could open a modal or navigate to details page
240
+ console.log("Selected shop:", shop);
241
+ }}
242
+ />
243
+ ))}
244
+ </div>
245
+ </section>
246
+ );
247
+}
src/pages/Home.tsx
+3
-3
@@ -25,16 +25,16 @@ export default function Home() {
25
🏵️
26
</text>
27
</svg>
28
- <h1 className="text-3xl md:text-4xl font-bold text-center mb-4 text-fuchsia-700">
28
+ <h1 className="text-3xl md:text-4xl font-bold text-center mb-4 text-blue-700">
29
{t.welcome}
30
</h1>
31
<p className="text-lg text-center mb-8 text-slate-600">{t.homeDesc}</p>
32
<Link
33
- to="/lessons"
33
+ to="/cut"
34
onClick={async () => {
35
await haptics.buttonPress();
36
}}
37
- className="bg-fuchsia-500 hover:bg-fuchsia-600 active:scale-95 transition transform text-white px-8 py-3 rounded-xl font-semibold shadow-lg hover:shadow-xl focus:ring-2 focus:ring-fuchsia-300 animate-pulse"
37
+ className="bg-blue-500 hover:bg-blue-600 active:scale-95 transition transform text-white px-8 py-3 rounded-xl font-semibold shadow-lg hover:shadow-xl focus:ring-2 focus:ring-blue-300 animate-pulse"
38
>
39
{t.getStarted}
40
</Link>
src/pages/Lessons.tsx
deleted
-392
@@ -1,392 +0,0 @@
1
-import { useState } from "react";
2
-import LessonCard from "../components/LessonCard";
3
-import QuestionCard from "../components/QuestionCard";
4
-import { useLang } from "../LanguageContext";
5
-import haptics from "./../utils/haptics";
6
-
7
-interface Choice {
8
- [key: string]: string;
9
-}
10
-
11
-interface Question {
12
- q: { [key: string]: string };
13
- choices: Choice[];
14
- answer: number;
15
-}
16
-
17
-interface Lesson {
18
- id: number;
19
- title: { [key: string]: string };
20
- description: { [key: string]: string };
21
- questions: Question[];
22
-}
23
-
24
-// Multilingual lesson data with Swedish (sv) translations
25
-const LESSONS: Lesson[] = [
26
- {
27
- id: 1,
28
- title: {
29
- fa: "سلام و احوالپرسی",
30
- en: "Greetings and Introductions",
31
- sv: "Hälsningar och introduktioner",
32
- },
33
- description: {
34
- fa: "کلمات و عبارات ابتدایی برای سلام، خداحافظی و معرفی.",
35
- en: "Basic words and phrases for greeting, farewell, and introduction.",
36
- sv: "Grundläggande ord och fraser för hälsning, avsked och introduktion.",
37
- },
38
- questions: [
39
- {
40
- q: {
41
- fa: "سلام به فارسی چیست؟",
42
- en: 'What is "Hello" in Persian?',
43
- sv: 'Vad är "Hej" på persiska?',
44
- },
45
- choices: [
46
- { fa: "سلام", en: "Salam", sv: "Salam" },
47
- { fa: "خداحافظ", en: "Goodbye", sv: "Adjö" },
48
- { fa: "ممنون", en: "Thank you", sv: "Tack" },
49
- ],
50
- answer: 0,
51
- },
52
- {
53
- q: {
54
- fa: "خداحافظ به فارسی چیست؟",
55
- en: 'What is "Goodbye" in Persian?',
56
- sv: 'Vad är "Adjö" på persiska?',
57
- },
58
- choices: [
59
- { fa: "متشکرم", en: "Thank you", sv: "Tack" },
60
- { fa: "خداحافظ", en: "Khodahafez", sv: "Khodahafez" },
61
- { fa: "بله", en: "Yes", sv: "Ja" },
62
- ],
63
- answer: 1,
64
- },
65
- ],
66
- },
67
- {
68
- id: 2,
69
- title: {
70
- fa: "اعداد",
71
- en: "Numbers",
72
- sv: "Siffror",
73
- },
74
- description: {
75
- fa: "یادگیری اعداد ۱ تا ۱۰ به فارسی.",
76
- en: "Learning numbers 1 to 10 in Persian.",
77
- sv: "Lär dig siffrorna 1 till 10 på persiska.",
78
- },
79
- questions: [
80
- {
81
- q: {
82
- fa: "عدد 3 به فارسی چیست؟",
83
- en: "What is the number 3 in Persian?",
84
- sv: "Vad är siffran 3 på persiska?",
85
- },
86
- choices: [
87
- { fa: "سه", en: "Se", sv: "Se" },
88
- { fa: "چهار", en: "Chahar", sv: "Chahar" },
89
- { fa: "دو", en: "Do", sv: "Do" },
90
- ],
91
- answer: 0,
92
- },
93
- {
94
- q: {
95
- fa: "عدد ۵ به فارسی چیست؟",
96
- en: "What is the number 5 in Persian?",
97
- sv: "Vad är siffran 5 på persiska?",
98
- },
99
- choices: [
100
- { fa: "نه", en: "Noh", sv: "Noh" },
101
- { fa: "پنج", en: "Panj", sv: "Panj" },
102
- { fa: "هشت", en: "Hasht", sv: "Hasht" },
103
- ],
104
- answer: 1,
105
- },
106
- ],
107
- },
108
- {
109
- id: 3,
110
- title: {
111
- fa: "رنگها",
112
- en: "Colors",
113
- sv: "Färger",
114
- },
115
- description: {
116
- fa: "کلمات مربوط به رنگهای اصلی.",
117
- en: "Words for basic colors.",
118
- sv: "Ord för grundläggande färger.",
119
- },
120
- questions: [
121
- {
122
- q: {
123
- fa: "رنگ آبی به فارسی چیست؟",
124
- en: 'What is "blue" in Persian?',
125
- sv: 'Vad är "blå" på persiska?',
126
- },
127
- choices: [
128
- { fa: "آبی", en: "Abi", sv: "Abi" },
129
- { fa: "قرمز", en: "Ghermez", sv: "Ghermez" },
130
- { fa: "سبز", en: "Sabz", sv: "Sabz" },
131
- ],
132
- answer: 0,
133
- },
134
- {
135
- q: {
136
- fa: "رنگ قرمز به فارسی چیست؟",
137
- en: 'What is "red" in Persian?',
138
- sv: 'Vad är "röd" på persiska?',
139
- },
140
- choices: [
141
- { fa: "زرد", en: "Zard", sv: "Zard" },
142
- { fa: "قرمز", en: "Ghermez", sv: "Ghermez" },
143
- { fa: "آبی", en: "Abi", sv: "Abi" },
144
- ],
145
- answer: 1,
146
- },
147
- ],
148
- },
149
- {
150
- id: 4,
151
- title: {
152
- fa: "روزهای هفته",
153
- en: "Days of the Week",
154
- sv: "Veckans dagar",
155
- },
156
- description: {
157
- fa: "نام روزهای هفته به فارسی.",
158
- en: "Names of the days of the week in Persian.",
159
- sv: "Namnen på veckans dagar på persiska.",
160
- },
161
- questions: [
162
- {
163
- q: {
164
- fa: "شنبه اولین روز هفته به فارسی است. درست یا غلط؟",
165
- en: 'Is "Shanbeh" the first day of the week in Persian? True or False?',
166
- sv: 'Är "Shanbeh" den första dagen i veckan på persiska? Sant eller falskt?',
167
- },
168
- choices: [
169
- { fa: "درست", en: "True", sv: "Sant" },
170
- { fa: "غلط", en: "False", sv: "Falskt" },
171
- ],
172
- answer: 0,
173
- },
174
- {
175
- q: {
176
- fa: "کدام یک روز سهشنبه است؟",
177
- en: "Which one is Tuesday?",
178
- sv: "Vilken dag är tisdag?",
179
- },
180
- choices: [
181
- { fa: "دوشنبه", en: "Doshanbeh", sv: "Doshanbeh" },
182
- { fa: "سهشنبه", en: "Seshanbeh", sv: "Seshanbeh" },
183
- { fa: "چهارشنبه", en: "Chaharshanbeh", sv: "Chaharshanbeh" },
184
- ],
185
- answer: 1,
186
- },
187
- ],
188
- },
189
- {
190
- id: 5,
191
- title: {
192
- fa: "میوهها",
193
- en: "Fruits",
194
- sv: "Frukter",
195
- },
196
- description: {
197
- fa: "نام میوههای رایج به فارسی.",
198
- en: "Names of common fruits in Persian.",
199
- sv: "Namn på vanliga frukter på persiska.",
200
- },
201
- questions: [
202
- {
203
- q: {
204
- fa: "سیب به انگلیسی چیست؟",
205
- en: 'What is "Sib" in English?',
206
- sv: 'Vad är "Sib" på engelska?',
207
- },
208
- choices: [
209
- { fa: "Apple", en: "Apple", sv: "Äpple" },
210
- { fa: "Banana", en: "Banana", sv: "Banan" },
211
- { fa: "Orange", en: "Orange", sv: "Apelsin" },
212
- ],
213
- answer: 0,
214
- },
215
- {
216
- q: {
217
- fa: 'کدام یک معنی "Banana" است؟',
218
- en: 'Which one means "Banana"?',
219
- sv: 'Vilken betyder "Banan"?',
220
- },
221
- choices: [
222
- { fa: "سیب", en: "Sib", sv: "Sib" },
223
- { fa: "پرتقال", en: "Porteghal", sv: "Porteghal" },
224
- { fa: "موز", en: "Mooz", sv: "Mooz" },
225
- ],
226
- answer: 2,
227
- },
228
- ],
229
- },
230
- {
231
- id: 6,
232
- title: {
233
- fa: "اعضای خانواده",
234
- en: "Family Members",
235
- sv: "Familjemedlemmar",
236
- },
237
- description: {
238
- fa: "نام اعضای خانواده به فارسی.",
239
- en: "Names of family members in Persian.",
240
- sv: "Namn på familjemedlemmar på persiska.",
241
- },
242
- questions: [
243
- {
244
- q: {
245
- fa: "پدر به انگلیسی چیست؟",
246
- en: 'What is "Pedar" in English?',
247
- sv: 'Vad är "Pedar" på engelska?',
248
- },
249
- choices: [
250
- { fa: "Father", en: "Father", sv: "Far" },
251
- { fa: "Mother", en: "Mother", sv: "Mor" },
252
- { fa: "Brother", en: "Brother", sv: "Bror" },
253
- ],
254
- answer: 0,
255
- },
256
- {
257
- q: {
258
- fa: 'کدام یک معنی "Mother" است؟',
259
- en: 'Which one means "Mother"?',
260
- sv: 'Vilken betyder "Mor"?',
261
- },
262
- choices: [
263
- { fa: "پدر", en: "Pedar", sv: "Pedar" },
264
- { fa: "مادر", en: "Madar", sv: "Madar" },
265
- { fa: "خواهر", en: "Khahar", sv: "Khahar" },
266
- ],
267
- answer: 1,
268
- },
269
- ],
270
- },
271
- {
272
- id: 7,
273
- title: {
274
- fa: "حیوانات",
275
- en: "Animals",
276
- sv: "Djur",
277
- },
278
- description: {
279
- fa: "نام حیوانات معمولی به فارسی.",
280
- en: "Names of common animals in Persian.",
281
- sv: "Namn på vanliga djur på persiska.",
282
- },
283
- questions: [
284
- {
285
- q: {
286
- fa: "سگ به انگلیسی چیست؟",
287
- en: 'What is "Sag" in English?',
288
- sv: 'Vad är "Sag" på engelska?',
289
- },
290
- choices: [
291
- { fa: "Dog", en: "Dog", sv: "Hund" },
292
- { fa: "Cat", en: "Cat", sv: "Katt" },
293
- { fa: "Bird", en: "Bird", sv: "Fågel" },
294
- ],
295
- answer: 0,
296
- },
297
- {
298
- q: {
299
- fa: 'کدام یک معنی "Cat" است؟',
300
- en: 'Which one means "Cat"?',
301
- sv: 'Vilken betyder "Katt"?',
302
- },
303
- choices: [
304
- { fa: "سگ", en: "Sag", sv: "Sag" },
305
- { fa: "گربه", en: "Gorbeh", sv: "Gorbeh" },
306
- { fa: "اسب", en: "Asb", sv: "Asb" },
307
- ],
308
- answer: 1,
309
- },
310
- ],
311
- },
312
- {
313
- id: 8,
314
- title: {
315
- fa: "ضمیرها",
316
- en: "Pronouns",
317
- sv: "Pronomen",
318
- },
319
- description: {
320
- fa: "ضمیرهای پایه در فارسی.",
321
- en: "Basic pronouns in Persian.",
322
- sv: "Grundläggande pronomen på persiska.",
323
- },
324
- questions: [
325
- {
326
- q: {
327
- fa: '"من" به انگلیسی چیست؟',
328
- en: 'What is "man" in English?',
329
- sv: 'Vad är "man" på engelska?',
330
- },
331
- choices: [
332
- { fa: "I", en: "I", sv: "Jag" },
333
- { fa: "You", en: "You", sv: "Du" },
334
- { fa: "He", en: "He", sv: "Han" },
335
- ],
336
- answer: 0,
337
- },
338
- {
339
- q: {
340
- fa: '"او" به انگلیسی چیست؟',
341
- en: 'What is "ou" in English?',
342
- sv: 'Vad är "ou" på engelska?',
343
- },
344
- choices: [
345
- { fa: "We", en: "We", sv: "Vi" },
346
- { fa: "They", en: "They", sv: "De" },
347
- { fa: "He/She", en: "He/She", sv: "Han/Hon" },
348
- ],
349
- answer: 2,
350
- },
351
- ],
352
- },
353
-];
354
-
355
-export default function Lessons() {
356
- const [activeLesson, setActiveLesson] = useState<Lesson | null>(null);
357
- const { t, lang } = useLang();
358
- const isRTL = lang === "fa";
359
- const dir = isRTL ? "rtl" : "ltr";
360
- const align = isRTL ? "text-right" : "text-left";
361
-
362
- return (
363
- <section dir={dir} className={`pb-12 ` + align}>
364
- <h2 className="text-2xl font-bold mb-2 text-fuchsia-700">{t.lessons}</h2>
365
- {!activeLesson ? (
366
- <div className="grid gap-4">
367
- {LESSONS.map((lesson) => (
368
- <LessonCard
369
- key={lesson.id}
370
- title={lesson.title[lang]}
371
- description={lesson.description[lang]}
372
- onStart={async () => {
373
- await haptics.cardTap();
374
- setActiveLesson(lesson);
375
- }}
376
- />
377
- ))}
378
- </div>
379
- ) : (
380
- <QuestionCard
381
- lesson={activeLesson}
382
- lang={lang}
383
- t={t}
384
- onBack={async () => {
385
- await haptics.buttonPress();
386
- setActiveLesson(null);
387
- }}
388
- />
389
- )}
390
- </section>
391
- );
392
-}
src/pages/Profile.tsx
+25
-3
@@ -1,7 +1,12 @@
1
import { useLang } from "../LanguageContext";
2
+import { SupportedLanguage } from "../i18n";
3
+import haptics from "./../utils/haptics";
4
5
export default function Profile() {
4
- const { t, lang } = useLang();
6
+ const handleNavClick = async () => {
7
+ await haptics.navigation();
8
+ };
9
+ const { t, lang, setLang } = useLang();
10
const isRTL = lang === "fa";
11
const dir = isRTL ? "rtl" : "ltr";
12
const align = isRTL ? "text-right" : "text-left";
@@ -11,9 +16,9 @@ export default function Profile() {
16
<img
17
src="https://api.dicebear.com/7.x/bottts/svg?seed=persian"
18
alt="Avatar"
14
- className="w-24 h-24 mb-4 rounded-full border-4 border-fuchsia-300"
19
+ className="w-24 h-24 mb-4 rounded-full border-4 border-blue-300"
20
/>
16
- <h2 className="text-xl font-bold mb-2 text-fuchsia-700">{t.profile}</h2>
21
+ <h2 className="text-xl font-bold mb-2 text-blue-700">{t.profile}</h2>
22
<div className="bg-white/80 shadow rounded-xl px-8 py-4 mt-2 w-full max-w-xs">
23
<div className="mb-2">
24
<span className="font-semibold">{t.user}:</span> {t.guest}
@@ -22,6 +27,23 @@ export default function Profile() {
27
<span className="font-semibold">{t.progress}:</span> 2/2 {t.completed}
28
</div>
29
</div>
30
+
31
+ <div className="bg-white/80 shadow rounded-xl px-8 py-4 mt-2 w-full max-w-xs">
32
+ <h2 className="text-lg font-bold mb-2 text-blue-700">App Settings</h2>
33
+ <div>
34
+ <select
35
+ className="rounded border px-2 py-1 bg-white text-sm text-blue-700 cursor-pointer shadow"
36
+ value={lang}
37
+ onClick={handleNavClick}
38
+ onChange={(e) => setLang(e.target.value as SupportedLanguage)}
39
+ aria-label="Change language"
40
+ >
41
+ <option value="fa">فارسی</option>
42
+ <option value="en">English</option>
43
+ <option value="sv">Svenska</option>
44
+ </select>
45
+ </div>
46
+ </div>
47
</section>
48
);
49
}
tailwind.config.js
+9
-6
@@ -1,11 +1,14 @@
1
/** @type {import('tailwindcss').Config} */
2
export default {
3
- content: [
4
- "./index.html",
5
- "./src/**/*.{js,jsx,ts,tsx}",
6
- ],
3
+ content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}"],
4
theme: {
8
- extend: {},
5
+ extend: {
6
+ colors: {
7
+ "cut-red": "#E24C3B",
8
+ "cut-white": "#FBF3EF",
9
+ "cut-blue": "#0088E0",
10
+ },
11
+ },
12
},
13
plugins: [],
11
-}
\ No newline at end of file
14
+};