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 +};