Spaces:
Running
Running
Upload 36 files
#351
by
almortamoh
- opened
This view is limited to 50 files because it contains too many changes.
See the raw diff here.
- .gitignore +142 -31
- ANDROID_STUDIO_GUIDE.md +200 -0
- APK_BUILD_GUIDE.md +206 -0
- APK_README.md +200 -0
- BUILD_APK_SIMPLE.bat +84 -0
- BUILD_FROM_GITHUB.bat +128 -0
- BUILD_INSTRUCTIONS.md +265 -0
- CLOUD_BUILD_QUICK.md +123 -0
- GITHUB_SETUP_GUIDE.md +209 -0
- INSTALL_ANDROID_SDK.bat +84 -0
- PROJECT_SUMMARY.md +219 -0
- QUICK_APK_BUILD.bat +79 -0
- QUICK_START.md +107 -0
- README.md +258 -24
- README_GITHUB.md +124 -0
- STEP_BY_STEP_APK.md +185 -0
- Windows +1 -0
- angular.json +147 -0
- app.js +780 -0
- app/(public)/layout.tsx +1 -1
- app/(public)/page.tsx +43 -4
- app/(public)/projects/page.tsx +13 -0
- app/[namespace]/[repoId]/page.tsx +0 -28
- app/actions/projects.ts +40 -24
- app/api/{ask → ask-ai}/route.ts +123 -380
- app/api/auth/login-url/route.ts +0 -23
- app/api/auth/logout/route.ts +0 -25
- app/api/auth/route.ts +1 -21
- app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts +0 -190
- app/api/me/projects/[namespace]/[repoId]/images/route.ts +0 -113
- app/api/me/projects/[namespace]/[repoId]/route.ts +162 -112
- app/api/me/projects/[namespace]/[repoId]/save/route.ts +0 -64
- app/api/me/projects/route.ts +92 -73
- app/api/me/route.ts +1 -22
- app/auth/callback/page.tsx +42 -67
- app/layout.tsx +30 -50
- app/new/page.tsx +0 -14
- app/projects/[namespace]/[repoId]/page.tsx +40 -0
- app/projects/new/page.tsx +5 -0
- app/sitemap.ts +0 -28
- assets/deepseek.svg +0 -1
- assets/globals.css +0 -225
- assets/kimi.svg +0 -1
- assets/qwen.svg +0 -1
- auth.js +426 -0
- capacitor-simple.config.ts +49 -0
- capacitor.config.js +47 -0
- capacitor.config.ts +67 -0
- components.json +1 -1
- components/animated-blobs/index.tsx +0 -34
.gitignore
CHANGED
@@ -1,41 +1,152 @@
|
|
1 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
-
#
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
.
|
8 |
-
!.yarn/patches
|
9 |
-
!.yarn/plugins
|
10 |
-
!.yarn/releases
|
11 |
-
!.yarn/versions
|
12 |
|
13 |
-
#
|
14 |
-
|
15 |
|
16 |
-
#
|
17 |
-
|
18 |
-
|
19 |
|
20 |
-
#
|
21 |
-
|
22 |
|
23 |
-
#
|
24 |
-
.
|
25 |
-
*.pem
|
26 |
|
27 |
-
#
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
.
|
|
|
|
|
|
|
32 |
|
33 |
-
#
|
34 |
-
|
|
|
35 |
|
36 |
-
#
|
37 |
-
|
38 |
|
39 |
-
#
|
40 |
*.tsbuildinfo
|
41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Logs
|
2 |
+
logs
|
3 |
+
*.log
|
4 |
+
npm-debug.log*
|
5 |
+
yarn-debug.log*
|
6 |
+
yarn-error.log*
|
7 |
+
lerna-debug.log*
|
8 |
|
9 |
+
# Runtime data
|
10 |
+
pids
|
11 |
+
*.pid
|
12 |
+
*.seed
|
13 |
+
*.pid.lock
|
|
|
|
|
|
|
|
|
14 |
|
15 |
+
# Directory for instrumented libs generated by jscoverage/JSCover
|
16 |
+
lib-cov
|
17 |
|
18 |
+
# Coverage directory used by tools like istanbul
|
19 |
+
coverage
|
20 |
+
*.lcov
|
21 |
|
22 |
+
# nyc test coverage
|
23 |
+
.nyc_output
|
24 |
|
25 |
+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
26 |
+
.grunt
|
|
|
27 |
|
28 |
+
# Bower dependency directory (https://bower.io/)
|
29 |
+
bower_components
|
30 |
+
|
31 |
+
# node-waf configuration
|
32 |
+
.lock-wscript
|
33 |
+
|
34 |
+
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
35 |
+
build/Release
|
36 |
|
37 |
+
# Dependency directories
|
38 |
+
node_modules/
|
39 |
+
jspm_packages/
|
40 |
|
41 |
+
# TypeScript v1 declaration files
|
42 |
+
typings/
|
43 |
|
44 |
+
# TypeScript cache
|
45 |
*.tsbuildinfo
|
46 |
+
|
47 |
+
# Optional npm cache directory
|
48 |
+
.npm
|
49 |
+
|
50 |
+
# Optional eslint cache
|
51 |
+
.eslintcache
|
52 |
+
|
53 |
+
# Microbundle cache
|
54 |
+
.rpt2_cache/
|
55 |
+
.rts2_cache_cjs/
|
56 |
+
.rts2_cache_es/
|
57 |
+
.rts2_cache_umd/
|
58 |
+
|
59 |
+
# Optional REPL history
|
60 |
+
.node_repl_history
|
61 |
+
|
62 |
+
# Output of 'npm pack'
|
63 |
+
*.tgz
|
64 |
+
|
65 |
+
# Yarn Integrity file
|
66 |
+
.yarn-integrity
|
67 |
+
|
68 |
+
# dotenv environment variables file
|
69 |
+
.env
|
70 |
+
.env.test
|
71 |
+
|
72 |
+
# parcel-bundler cache (https://parceljs.org/)
|
73 |
+
.cache
|
74 |
+
.parcel-cache
|
75 |
+
|
76 |
+
# Next.js build output
|
77 |
+
.next
|
78 |
+
|
79 |
+
# Nuxt.js build / generate output
|
80 |
+
.nuxt
|
81 |
+
dist
|
82 |
+
|
83 |
+
# Gatsby files
|
84 |
+
.cache/
|
85 |
+
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
86 |
+
# https://nextjs.org/blog/next-9-1#public-directory-support
|
87 |
+
# public
|
88 |
+
|
89 |
+
# vuepress build output
|
90 |
+
.vuepress/dist
|
91 |
+
|
92 |
+
# Serverless directories
|
93 |
+
.serverless/
|
94 |
+
|
95 |
+
# FuseBox cache
|
96 |
+
.fusebox/
|
97 |
+
|
98 |
+
# DynamoDB Local files
|
99 |
+
.dynamodb/
|
100 |
+
|
101 |
+
# TernJS port file
|
102 |
+
.tern-port
|
103 |
+
|
104 |
+
# Capacitor
|
105 |
+
.capacitor/
|
106 |
+
capacitor.config.json
|
107 |
+
|
108 |
+
# Android
|
109 |
+
android/app/build/
|
110 |
+
android/build/
|
111 |
+
android/.gradle/
|
112 |
+
android/local.properties
|
113 |
+
android/app/release/
|
114 |
+
android/app/debug/
|
115 |
+
android/gradle/
|
116 |
+
android/gradlew
|
117 |
+
android/gradlew.bat
|
118 |
+
|
119 |
+
# iOS
|
120 |
+
ios/build/
|
121 |
+
ios/App/Pods/
|
122 |
+
ios/App/App.xcworkspace/xcuserdata/
|
123 |
+
ios/App/App.xcodeproj/xcuserdata/
|
124 |
+
ios/App/App.xcodeproj/project.xcworkspace/xcuserdata/
|
125 |
+
|
126 |
+
# Editor directories and files
|
127 |
+
.vscode/
|
128 |
+
.idea/
|
129 |
+
*.swp
|
130 |
+
*.swo
|
131 |
+
*~
|
132 |
+
|
133 |
+
# OS generated files
|
134 |
+
.DS_Store
|
135 |
+
.DS_Store?
|
136 |
+
._*
|
137 |
+
.Spotlight-V100
|
138 |
+
.Trashes
|
139 |
+
ehthumbs.db
|
140 |
+
Thumbs.db
|
141 |
+
|
142 |
+
# Local development
|
143 |
+
*.local
|
144 |
+
|
145 |
+
# Build outputs
|
146 |
+
build/
|
147 |
+
dist/
|
148 |
+
www/build/
|
149 |
+
|
150 |
+
# Temporary files
|
151 |
+
tmp/
|
152 |
+
temp/
|
ANDROID_STUDIO_GUIDE.md
ADDED
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 📱 دليل بناء APK باستخدام Android Studio
|
2 |
+
|
3 |
+
## 🎯 **الهدف:** تحويل المشروع إلى ملف APK قابل للتثبيت
|
4 |
+
|
5 |
+
---
|
6 |
+
|
7 |
+
## 📋 **الخطوات المفصلة:**
|
8 |
+
|
9 |
+
### **الخطوة 1: فتح المشروع** ⏱️ (2-3 دقائق)
|
10 |
+
|
11 |
+
1. **افتح Android Studio**
|
12 |
+
2. **اختر "Open an Existing Project"** أو **"Open"**
|
13 |
+
3. **انتقل إلى مجلد المشروع:**
|
14 |
+
```
|
15 |
+
E:\almada\android
|
16 |
+
```
|
17 |
+
4. **اختر مجلد `android`** (وليس المجلد الرئيسي)
|
18 |
+
5. **اضغط "OK"**
|
19 |
+
|
20 |
+
### **الخطوة 2: انتظار المزامنة** ⏱️ (5-10 دقائق)
|
21 |
+
|
22 |
+
عند فتح المشروع لأول مرة، سيقوم Android Studio بـ:
|
23 |
+
- 📥 **تحميل Gradle** (إذا لم يكن مثبت)
|
24 |
+
- 📦 **تحميل التبعيات** (Dependencies)
|
25 |
+
- 🔄 **مزامنة المشروع** (Sync)
|
26 |
+
|
27 |
+
**انتظر حتى تكتمل العملية!** ستظهر رسالة في الأسفل:
|
28 |
+
```
|
29 |
+
✅ Gradle sync finished
|
30 |
+
```
|
31 |
+
|
32 |
+
### **الخطوة 3: حل المشاكل المحتملة** ⏱️ (2-5 دقائق)
|
33 |
+
|
34 |
+
إذا ظهرت أي رسائل خطأ:
|
35 |
+
|
36 |
+
#### **مشكلة SDK:**
|
37 |
+
```
|
38 |
+
SDK location not found
|
39 |
+
```
|
40 |
+
**الحل:**
|
41 |
+
- اذهب إلى **File > Project Structure**
|
42 |
+
- اختر **SDK Location**
|
43 |
+
- تأكد من مسار Android SDK
|
44 |
+
|
45 |
+
#### **مشكلة Gradle:**
|
46 |
+
```
|
47 |
+
Gradle version not supported
|
48 |
+
```
|
49 |
+
**الحل:**
|
50 |
+
- اضغط على **"Update Gradle"** في الرسالة
|
51 |
+
- أو اذهب إلى **File > Project Structure > Project**
|
52 |
+
|
53 |
+
### **الخطوة 4: بناء APK** ⏱️ (3-5 دقائق)
|
54 |
+
|
55 |
+
1. **في شريط القوائم، اختر:**
|
56 |
+
```
|
57 |
+
Build > Build Bundle(s) / APK(s) > Build APK(s)
|
58 |
+
```
|
59 |
+
|
60 |
+
2. **انتظر اكتمال البناء** - ستظهر رسالة في الأسفل:
|
61 |
+
```
|
62 |
+
⏳ Building APK...
|
63 |
+
✅ APK(s) generated successfully
|
64 |
+
```
|
65 |
+
|
66 |
+
3. **عند اكتمال البناء، ستظهر نافذة:**
|
67 |
+
```
|
68 |
+
APK(s) generated successfully.
|
69 |
+
|
70 |
+
[locate] [analyze]
|
71 |
+
```
|
72 |
+
|
73 |
+
4. **اضغط "locate"** للذهاب إلى مجلد APK
|
74 |
+
|
75 |
+
### **الخطوة 5: العثور على ملف APK** ⏱️ (1 دقيقة)
|
76 |
+
|
77 |
+
ملف APK سيكون في:
|
78 |
+
```
|
79 |
+
E:\almada\android\app\build\outputs\apk\debug\app-debug.apk
|
80 |
+
```
|
81 |
+
|
82 |
+
**معلومات الملف:**
|
83 |
+
- **الاسم:** `app-debug.apk`
|
84 |
+
- **الحجم:** ~15-20 MB
|
85 |
+
- **النوع:** Debug APK (للاختبار)
|
86 |
+
|
87 |
+
---
|
88 |
+
|
89 |
+
## 🔧 **استكشاف الأخطاء:**
|
90 |
+
|
91 |
+
### **خطأ: "SDK not found"**
|
92 |
+
```bash
|
93 |
+
# الحل:
|
94 |
+
1. اذهب إلى File > Settings
|
95 |
+
2. اختر Appearance & Behavior > System Settings > Android SDK
|
96 |
+
3. تأكد من تثبيت Android SDK
|
97 |
+
```
|
98 |
+
|
99 |
+
### **خطأ: "Gradle sync failed"**
|
100 |
+
```bash
|
101 |
+
# الحل:
|
102 |
+
1. اضغط "Try Again"
|
103 |
+
2. أو اذهب إلى File > Sync Project with Gradle Files
|
104 |
+
```
|
105 |
+
|
106 |
+
### **خطأ: "Build failed"**
|
107 |
+
```bash
|
108 |
+
# الحل:
|
109 |
+
1. اذهب إلى Build > Clean Project
|
110 |
+
2. ثم Build > Rebuild Project
|
111 |
+
```
|
112 |
+
|
113 |
+
---
|
114 |
+
|
115 |
+
## 📱 **اختبار التطبيق:**
|
116 |
+
|
117 |
+
### **الطريقة 1: على الكمبيوتر (محاكي)**
|
118 |
+
1. **إنشاء محاكي:**
|
119 |
+
- اذهب إلى **Tools > AVD Manager**
|
120 |
+
- اضغط **"Create Virtual Device"**
|
121 |
+
- اختر جهاز (مثل Pixel 4)
|
122 |
+
- اختر نظام Android (API 30+)
|
123 |
+
|
124 |
+
2. **تشغيل التطبيق:**
|
125 |
+
- اضغط **Run** (الزر الأخضر)
|
126 |
+
- اختر المحاكي
|
127 |
+
- انتظر تشغيل التطبيق
|
128 |
+
|
129 |
+
### **الطريقة 2: على الهاتف الحقيقي**
|
130 |
+
1. **تفعيل Developer Options:**
|
131 |
+
- اذهب إلى **Settings > About Phone**
|
132 |
+
- اضغط على **Build Number** 7 مرات
|
133 |
+
- ارجع إلى Settings وادخل **Developer Options**
|
134 |
+
- فعل **USB Debugging**
|
135 |
+
|
136 |
+
2. **توصيل الهاتف:**
|
137 |
+
- وصل الهاتف بـ USB
|
138 |
+
- اختر **File Transfer** في الهاتف
|
139 |
+
- في Android Studio، اختر جهازك من القائمة
|
140 |
+
- اضغط **Run**
|
141 |
+
|
142 |
+
### **الطريقة 3: تثبيت APK يدوياً**
|
143 |
+
1. **نسخ APK إلى الهاتف**
|
144 |
+
2. **في الهاتف:**
|
145 |
+
- اذهب إلى **Settings > Security**
|
146 |
+
- فعل **"Install from Unknown Sources"**
|
147 |
+
- افتح ملف APK واضغط **Install**
|
148 |
+
|
149 |
+
---
|
150 |
+
|
151 |
+
## 🎯 **بيانات التجربة:**
|
152 |
+
|
153 |
+
بعد تثبيت التطبيق، استخدم:
|
154 |
+
- **رقم الهاتف:** `777123456`
|
155 |
+
- **رمز PIN:** `1234`
|
156 |
+
|
157 |
+
---
|
158 |
+
|
159 |
+
## 📊 **معلومات التطبيق:**
|
160 |
+
|
161 |
+
| المعلومة | القيمة |
|
162 |
+
|---------|--------|
|
163 |
+
| **اسم التطبيق** | محفظتي الموحدة |
|
164 |
+
| **Package Name** | com.almada.unifiedwallet |
|
165 |
+
| **الإصدار** | 1.0.0 |
|
166 |
+
| **حجم APK** | ~15-20 MB |
|
167 |
+
| **الحد الأدنى** | Android 7.0 (API 24) |
|
168 |
+
|
169 |
+
---
|
170 |
+
|
171 |
+
## 🎉 **النجاح!**
|
172 |
+
|
173 |
+
عند اكتمال جميع الخطوات، ستحصل على:
|
174 |
+
- ✅ **ملف APK** جاهز للتثبيت
|
175 |
+
- ✅ **تطبيق يعمل** على الأندرويد
|
176 |
+
- ✅ **واجهة عربية** كاملة
|
177 |
+
- ✅ **جميع الميزات** متاحة
|
178 |
+
|
179 |
+
---
|
180 |
+
|
181 |
+
## 💡 **نصائح مهمة:**
|
182 |
+
|
183 |
+
### **ل��بناء الناجح:**
|
184 |
+
- تأكد من **اتصال الإنترنت** أثناء أول مزامنة
|
185 |
+
- **لا تغلق** Android Studio أثناء التحميل
|
186 |
+
- **انتظر** اكتمال جميع العمليات
|
187 |
+
|
188 |
+
### **للاختبار:**
|
189 |
+
- اختبر على **أجهزة مختلفة** إن أمكن
|
190 |
+
- تأكد من **جميع الميزات** تعمل
|
191 |
+
- اختبر **تسجيل الدخول** والتنقل
|
192 |
+
|
193 |
+
### **للمشاكل:**
|
194 |
+
- راجع **Build Output** في الأسفل
|
195 |
+
- استخدم **"Clean Project"** عند المشاكل
|
196 |
+
- أعد تشغيل **Android Studio** إذا لزم الأمر
|
197 |
+
|
198 |
+
---
|
199 |
+
|
200 |
+
**🚀 مبروك! تطبيقك جاهز للعالم!**
|
APK_BUILD_GUIDE.md
ADDED
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# دليل بناء APK - محفظتي الموحدة
|
2 |
+
|
3 |
+
## 🎯 **الوضع الحالي**
|
4 |
+
|
5 |
+
✅ **تم إنجازه:**
|
6 |
+
- ✅ إعداد مشروع Capacitor كامل
|
7 |
+
- ✅ تثبيت Java JDK 11
|
8 |
+
- ✅ إضافة منصة الأندرويد
|
9 |
+
- ✅ نسخ ملفات التطبيق إلى مجلد www
|
10 |
+
- ✅ إضافة الأذونات المطلوبة
|
11 |
+
- ✅ إعداد ملفات التكوين
|
12 |
+
|
13 |
+
❌ **المطلوب لإكمال البناء:**
|
14 |
+
- ❌ تثبيت Android SDK
|
15 |
+
- ❌ إعداد متغيرات البيئة
|
16 |
+
- ❌ بناء APK
|
17 |
+
|
18 |
+
---
|
19 |
+
|
20 |
+
## 📋 **خطوات إكمال بناء APK**
|
21 |
+
|
22 |
+
### **الطريقة 1: استخدام Android Studio (الأسهل)**
|
23 |
+
|
24 |
+
#### 1. تحميل وتثبيت Android Studio:
|
25 |
+
```
|
26 |
+
https://developer.android.com/studio
|
27 |
+
```
|
28 |
+
|
29 |
+
#### 2. فتح المشروع:
|
30 |
+
```bash
|
31 |
+
cd E:\almada\android
|
32 |
+
# ثم فتح المجلد في Android Studio
|
33 |
+
```
|
34 |
+
|
35 |
+
#### 3. بناء APK:
|
36 |
+
- في Android Studio: **Build > Build Bundle(s) / APK(s) > Build APK(s)**
|
37 |
+
- انتظار اكتمال البناء
|
38 |
+
- ستجد APK في: `android/app/build/outputs/apk/debug/`
|
39 |
+
|
40 |
+
---
|
41 |
+
|
42 |
+
### **الطريقة 2: سطر الأوامر (متقدم)**
|
43 |
+
|
44 |
+
#### 1. تثبيت Android SDK:
|
45 |
+
```bash
|
46 |
+
# تحميل Command Line Tools من:
|
47 |
+
# https://developer.android.com/studio#command-tools
|
48 |
+
|
49 |
+
# استخراج إلى مجلد مثل:
|
50 |
+
# C:\Android\cmdline-tools\latest\
|
51 |
+
```
|
52 |
+
|
53 |
+
#### 2. إعداد متغيرات البيئة:
|
54 |
+
```bash
|
55 |
+
# إضافة إلى متغيرات البيئة:
|
56 |
+
ANDROID_HOME=C:\Android
|
57 |
+
ANDROID_SDK_ROOT=C:\Android
|
58 |
+
PATH=%PATH%;%ANDROID_HOME%\cmdline-tools\latest\bin
|
59 |
+
PATH=%PATH%;%ANDROID_HOME%\platform-tools
|
60 |
+
```
|
61 |
+
|
62 |
+
#### 3. تثبيت SDK Components:
|
63 |
+
```bash
|
64 |
+
sdkmanager "platform-tools" "platforms;android-33" "build-tools;33.0.0"
|
65 |
+
```
|
66 |
+
|
67 |
+
#### 4. بناء APK:
|
68 |
+
```bash
|
69 |
+
cd E:\almada\android
|
70 |
+
.\gradlew assembleDebug
|
71 |
+
```
|
72 |
+
|
73 |
+
---
|
74 |
+
|
75 |
+
### **الطريقة 3: استخدام Ionic CLI (الأبسط)**
|
76 |
+
|
77 |
+
#### 1. تثبيت Android Studio أولاً (للحصول على SDK)
|
78 |
+
|
79 |
+
#### 2. استخدام Ionic:
|
80 |
+
```bash
|
81 |
+
cd E:\almada
|
82 |
+
ionic cap build android
|
83 |
+
```
|
84 |
+
|
85 |
+
---
|
86 |
+
|
87 |
+
## 📱 **ملفات APK المتوقعة**
|
88 |
+
|
89 |
+
بعد البناء الناجح ستجد:
|
90 |
+
|
91 |
+
### **APK للتطوير:**
|
92 |
+
```
|
93 |
+
android/app/build/outputs/apk/debug/app-debug.apk
|
94 |
+
```
|
95 |
+
|
96 |
+
### **APK للإنتاج:**
|
97 |
+
```
|
98 |
+
android/app/build/outputs/apk/release/app-release.apk
|
99 |
+
```
|
100 |
+
|
101 |
+
---
|
102 |
+
|
103 |
+
## 🔧 **إعدادات إضافية للتطبيق**
|
104 |
+
|
105 |
+
### **الأذونات المضافة:**
|
106 |
+
```xml
|
107 |
+
<!-- الأذونات الأساسية -->
|
108 |
+
<uses-permission android:name="android.permission.INTERNET" />
|
109 |
+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
110 |
+
<uses-permission android:name="android.permission.CAMERA" />
|
111 |
+
<uses-permission android:name="android.permission.VIBRATE" />
|
112 |
+
|
113 |
+
<!-- أذونات SMS (للمستقبل) -->
|
114 |
+
<uses-permission android:name="android.permission.READ_SMS" />
|
115 |
+
<uses-permission android:name="android.permission.RECEIVE_SMS" />
|
116 |
+
|
117 |
+
<!-- أذونات البصمة -->
|
118 |
+
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
119 |
+
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
120 |
+
```
|
121 |
+
|
122 |
+
### **معلومات التطبيق:**
|
123 |
+
- **اسم التطبيق:** محفظتي الموحدة
|
124 |
+
- **Package ID:** com.almada.unifiedwallet
|
125 |
+
- **الإصدار:** 1.0.0
|
126 |
+
|
127 |
+
---
|
128 |
+
|
129 |
+
## 🚀 **اختبار التطبيق**
|
130 |
+
|
131 |
+
### **تثبيت APK على الهاتف:**
|
132 |
+
```bash
|
133 |
+
# تفعيل Developer Options و USB Debugging
|
134 |
+
# ثم:
|
135 |
+
adb install app-debug.apk
|
136 |
+
```
|
137 |
+
|
138 |
+
### **أو نسخ APK إلى الهاتف وتثبيته يدوياً**
|
139 |
+
|
140 |
+
---
|
141 |
+
|
142 |
+
## 📊 **الميزات المتاحة في التطبيق**
|
143 |
+
|
144 |
+
### **الصفحة الرئيسية:**
|
145 |
+
- تسجيل دخول برقم هاتف + PIN
|
146 |
+
- عرض المحافظ الـ6 (جوالي، ONE Cash، إلخ)
|
147 |
+
- عرض الأرصدة الموحدة
|
148 |
+
|
149 |
+
### **الميزات المتقدمة:**
|
150 |
+
- واجهة عربية كاملة (RTL)
|
151 |
+
- تصميم متجاوب
|
152 |
+
- رسوم متحركة سلسة
|
153 |
+
- دعم الوضع الليلي
|
154 |
+
|
155 |
+
### **الأمان:**
|
156 |
+
- تشفير البيانات محلياً
|
157 |
+
- حماية PIN
|
158 |
+
- جلسات آمنة
|
159 |
+
|
160 |
+
---
|
161 |
+
|
162 |
+
## 🎯 **الخطوات التالية بعد البناء**
|
163 |
+
|
164 |
+
### **للاختبار:**
|
165 |
+
1. تثبيت APK على الهاتف
|
166 |
+
2. اختبار تسجيل الدخول (777123456 / 1234)
|
167 |
+
3. اختبار جميع الميزات
|
168 |
+
4. التأكد من الأداء
|
169 |
+
|
170 |
+
### **للتطوير:**
|
171 |
+
1. إضافة ميزات قراءة SMS
|
172 |
+
2. تطوير نظام الإشعارات
|
173 |
+
3. إضافة المزيد من المحافظ
|
174 |
+
4. تحسين الأمان
|
175 |
+
|
176 |
+
### **للنشر:**
|
177 |
+
1. إنشاء حساب Google Play Developer
|
178 |
+
2. إعداد التوقيع للإنتاج
|
179 |
+
3. رفع التطبيق للمراجعة
|
180 |
+
4. التسويق والترويج
|
181 |
+
|
182 |
+
---
|
183 |
+
|
184 |
+
## 💡 **نصائح مهمة**
|
185 |
+
|
186 |
+
### **للبناء الناجح:**
|
187 |
+
- تأكد من تثبيت Java JDK 11+ ✅
|
188 |
+
- تأكد من تثبيت Android SDK
|
189 |
+
- تأكد من إعداد متغيرات البيئة
|
190 |
+
- استخدم Android Studio للسهولة
|
191 |
+
|
192 |
+
### **للاختبار:**
|
193 |
+
- اختبر على أجهزة مختلفة
|
194 |
+
- اختبر جميع الميزات
|
195 |
+
- تأكد من الأداء والاستقرار
|
196 |
+
|
197 |
+
### **للأمان:**
|
198 |
+
- لا تشارك ملفات التوقيع
|
199 |
+
- استخدم ProGuard للحماية
|
200 |
+
- اختبر الأمان بعناية
|
201 |
+
|
202 |
+
---
|
203 |
+
|
204 |
+
**🎉 مبروك! تطبيقك جاهز للبناء والاختبار!**
|
205 |
+
|
206 |
+
للمساعدة في أي خطوة، راجع الوثائق أو تواصل مع فريق التطوير.
|
APK_README.md
ADDED
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 📱 محفظتي الموحدة - تطبيق APK جاهز!
|
2 |
+
|
3 |
+
## 🎉 **تهانينا! تطبيقك جاهز للبناء**
|
4 |
+
|
5 |
+
تم إعداد مشروع **محفظتي الموحدة** بنجاح كتطبيق أندرويد باستخدام تقنية **Capacitor**.
|
6 |
+
|
7 |
+
---
|
8 |
+
|
9 |
+
## 🚀 **طرق بناء APK**
|
10 |
+
|
11 |
+
### **الطريقة الأسهل: Android Studio**
|
12 |
+
1. حمل وثبت [Android Studio](https://developer.android.com/studio)
|
13 |
+
2. افتح مجلد `android` في Android Studio
|
14 |
+
3. اختر **Build > Build APK**
|
15 |
+
4. انتظر اكتمال البناء
|
16 |
+
5. ستجد APK في: `android/app/build/outputs/apk/debug/`
|
17 |
+
|
18 |
+
### **الطريقة السريعة: ملف BAT**
|
19 |
+
1. شغل `INSTALL_ANDROID_SDK.bat` (مرة واحدة فقط)
|
20 |
+
2. اتبع التعليمات لتثبيت Android SDK
|
21 |
+
3. شغل `QUICK_APK_BUILD.bat`
|
22 |
+
4. انتظر اكتمال البناء
|
23 |
+
|
24 |
+
### **الطريقة اليدوية: سطر الأوامر**
|
25 |
+
```bash
|
26 |
+
# تأكد من تثبيت Java و Android SDK
|
27 |
+
java -version
|
28 |
+
sdkmanager --version
|
29 |
+
|
30 |
+
# بناء APK
|
31 |
+
cd android
|
32 |
+
gradlew assembleDebug
|
33 |
+
```
|
34 |
+
|
35 |
+
---
|
36 |
+
|
37 |
+
## 📋 **متطلبات البناء**
|
38 |
+
|
39 |
+
### **مثبت بالفعل:**
|
40 |
+
✅ **Node.js** و **npm**
|
41 |
+
✅ **Capacitor** و **التبعيات**
|
42 |
+
✅ **Java JDK 11**
|
43 |
+
✅ **مشروع Android** جاهز
|
44 |
+
|
45 |
+
### **مطلوب تثبيته:**
|
46 |
+
❌ **Android SDK** (عبر Android Studio أو Command Line Tools)
|
47 |
+
|
48 |
+
---
|
49 |
+
|
50 |
+
## 📱 **معلومات التطبيق**
|
51 |
+
|
52 |
+
| المعلومة | القيمة |
|
53 |
+
|---------|--------|
|
54 |
+
| **اسم التطبيق** | محفظتي الموحدة |
|
55 |
+
| **Package ID** | com.almada.unifiedwallet |
|
56 |
+
| **الإصدار** | 1.0.0 |
|
57 |
+
| **الحد الأدنى للأندرويد** | API 24 (Android 7.0) |
|
58 |
+
|
59 |
+
---
|
60 |
+
|
61 |
+
## 🔑 **بيانات التجربة**
|
62 |
+
|
63 |
+
للدخول إلى التطبيق بعد التثبيت:
|
64 |
+
- **رقم الهاتف:** `777123456`
|
65 |
+
- **رمز PIN:** `1234`
|
66 |
+
|
67 |
+
---
|
68 |
+
|
69 |
+
## 🎯 **الميزات المتاحة**
|
70 |
+
|
71 |
+
### **الأساسية:**
|
72 |
+
- ✅ تسجيل دخول آمن برقم الهاتف + PIN
|
73 |
+
- ✅ عرض 6 محافظ يمنية (جوالي، ONE Cash، إلخ)
|
74 |
+
- ✅ واجهة عربية كاملة (RTL)
|
75 |
+
- ✅ تصميم متجاوب وجذاب
|
76 |
+
|
77 |
+
### **المتقدمة:**
|
78 |
+
- ✅ رسوم متحركة سلسة
|
79 |
+
- ✅ دعم الوضع الليلي
|
80 |
+
- ✅ حفظ البيانات محلياً
|
81 |
+
- ✅ أمان متعدد الطبقات
|
82 |
+
|
83 |
+
### **المستقبلية (جاهزة للتطوير):**
|
84 |
+
- 🔄 قراءة رسائل SMS تلقائياً
|
85 |
+
- 🔄 مصادقة بيومترية (بصمة/وجه)
|
86 |
+
- 🔄 إشعارات ذكية
|
87 |
+
- 🔄 تحليل الإنفاق
|
88 |
+
|
89 |
+
---
|
90 |
+
|
91 |
+
## 📂 **هيكل الملفات**
|
92 |
+
|
93 |
+
```
|
94 |
+
almada/
|
95 |
+
├── 📱 android/ # مشروع الأندرويد
|
96 |
+
├── 🌐 www/ # ملفات التطبيق
|
97 |
+
├── 📄 capacitor.config.ts # إعدادات Capacitor
|
98 |
+
├── 📦 package.json # تبعيات المشروع
|
99 |
+
├── 🔨 QUICK_APK_BUILD.bat # بناء سريع
|
100 |
+
├── ⚙️ INSTALL_ANDROID_SDK.bat # تثبيت SDK
|
101 |
+
└── 📖 APK_BUILD_GUIDE.md # دليل مفصل
|
102 |
+
```
|
103 |
+
|
104 |
+
---
|
105 |
+
|
106 |
+
## 🛠️ **استكشاف الأخطاء**
|
107 |
+
|
108 |
+
### **خطأ: Java غير موجود**
|
109 |
+
```bash
|
110 |
+
# تحقق من تثبيت Java
|
111 |
+
java -version
|
112 |
+
|
113 |
+
# إذا لم يكن مثبت، حمل من:
|
114 |
+
# https://adoptium.net/
|
115 |
+
```
|
116 |
+
|
117 |
+
### **خطأ: Android SDK غير موجود**
|
118 |
+
```bash
|
119 |
+
# شغل ملف التثبيت
|
120 |
+
INSTALL_ANDROID_SDK.bat
|
121 |
+
|
122 |
+
# أو ثبت Android Studio
|
123 |
+
```
|
124 |
+
|
125 |
+
### **خطأ: Gradle Build فشل**
|
126 |
+
```bash
|
127 |
+
# نظف المشروع
|
128 |
+
cd android
|
129 |
+
gradlew clean
|
130 |
+
|
131 |
+
# أعد البناء
|
132 |
+
gradlew assembleDebug
|
133 |
+
```
|
134 |
+
|
135 |
+
---
|
136 |
+
|
137 |
+
## 📲 **تثبيت APK على الهاتف**
|
138 |
+
|
139 |
+
### **الطريقة 1: USB**
|
140 |
+
```bash
|
141 |
+
# فعل USB Debugging في الهاتف
|
142 |
+
# ثم:
|
143 |
+
adb install app-debug.apk
|
144 |
+
```
|
145 |
+
|
146 |
+
### **الطريقة 2: يدوياً**
|
147 |
+
1. انسخ ملف APK إلى الهاتف
|
148 |
+
2. فعل "مصادر غير معروفة" في الإعدادات
|
149 |
+
3. اضغط على ملف APK لتثبيته
|
150 |
+
|
151 |
+
---
|
152 |
+
|
153 |
+
## 🎯 **الخطوات التالية**
|
154 |
+
|
155 |
+
### **للاختبار:**
|
156 |
+
1. 📱 ثبت APK على الهاتف
|
157 |
+
2. 🔐 جرب تسجيل الدخول
|
158 |
+
3. 💳 استكشف المحافظ
|
159 |
+
4. 🎨 جرب الميزات المختلفة
|
160 |
+
|
161 |
+
### **للتطوير:**
|
162 |
+
1. 📨 أضف قراءة SMS
|
163 |
+
2. 🔔 طور الإشعارات
|
164 |
+
3. 📊 أضف تحليل البيانات
|
165 |
+
4. 🛡️ حسن الأمان
|
166 |
+
|
167 |
+
### **للنشر:**
|
168 |
+
1. 🏪 أنشئ حساب Google Play
|
169 |
+
2. 🔏 أعد التوقيع للإنتاج
|
170 |
+
3. 📤 ارفع للمراجعة
|
171 |
+
4. 📢 سوق التطبيق
|
172 |
+
|
173 |
+
---
|
174 |
+
|
175 |
+
## 💡 **نصائح مهمة**
|
176 |
+
|
177 |
+
### **للبناء الناجح:**
|
178 |
+
- استخدم **Android Studio** للسهولة
|
179 |
+
- تأكد من **اتصال الإنترنت** أثناء البناء
|
180 |
+
- **أعد تشغيل** Command Prompt بعد تثبيت SDK
|
181 |
+
|
182 |
+
### **للاختبار:**
|
183 |
+
- اختبر على **أجهزة مختلفة**
|
184 |
+
- تأكد من **جميع الميزات**
|
185 |
+
- راقب **الأداء والاستقرار**
|
186 |
+
|
187 |
+
---
|
188 |
+
|
189 |
+
## 📞 **الدعم والمساعدة**
|
190 |
+
|
191 |
+
للحصول على المساعدة:
|
192 |
+
1. راجع `APK_BUILD_GUIDE.md` للتفاصيل
|
193 |
+
2. تحقق من `BUILD_INSTRUCTIONS.md` للتعليمات الكاملة
|
194 |
+
3. تواصل مع فريق التطوير
|
195 |
+
|
196 |
+
---
|
197 |
+
|
198 |
+
**🎊 مبروك! تطبيقك جاهز للعالم!**
|
199 |
+
|
200 |
+
*تطبيق محفظتي الموحدة - المدى للخدمات البرمجية التسويقية والإعلانية*
|
BUILD_APK_SIMPLE.bat
ADDED
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@echo off
|
2 |
+
echo ========================================
|
3 |
+
echo محفظتي الموحدة - بناء APK مبسط
|
4 |
+
echo ========================================
|
5 |
+
echo.
|
6 |
+
|
7 |
+
echo 🔍 التحقق من المتطلبات...
|
8 |
+
|
9 |
+
:: التحقق من Java
|
10 |
+
java -version >nul 2>&1
|
11 |
+
if %errorlevel% neq 0 (
|
12 |
+
echo ❌ Java غير مثبت!
|
13 |
+
echo يرجى تثبيت Java JDK من: https://adoptium.net/
|
14 |
+
pause
|
15 |
+
exit /b 1
|
16 |
+
)
|
17 |
+
echo ✅ Java متوفر
|
18 |
+
|
19 |
+
:: التحقق من Node.js
|
20 |
+
node --version >nul 2>&1
|
21 |
+
if %errorlevel% neq 0 (
|
22 |
+
echo ❌ Node.js غير مثبت!
|
23 |
+
echo يرجى تثبيت Node.js من: https://nodejs.org/
|
24 |
+
pause
|
25 |
+
exit /b 1
|
26 |
+
)
|
27 |
+
echo ✅ Node.js متوفر
|
28 |
+
|
29 |
+
echo.
|
30 |
+
echo 📦 تثبيت أدوات البناء...
|
31 |
+
call npm install -g @ionic/cli @capacitor/cli
|
32 |
+
|
33 |
+
echo.
|
34 |
+
echo 🔄 مزامنة المشروع...
|
35 |
+
call cap sync android
|
36 |
+
|
37 |
+
echo.
|
38 |
+
echo 🏗️ بناء APK...
|
39 |
+
echo هذه العملية قد تستغرق 5-10 دقائق...
|
40 |
+
echo يرجى الانتظار...
|
41 |
+
|
42 |
+
cd android
|
43 |
+
call gradlew assembleDebug
|
44 |
+
|
45 |
+
if %errorlevel% equ 0 (
|
46 |
+
echo.
|
47 |
+
echo ========================================
|
48 |
+
echo 🎉 تم بناء APK بنجاح!
|
49 |
+
echo ========================================
|
50 |
+
echo.
|
51 |
+
echo 📱 ملف APK متوفر في:
|
52 |
+
echo android\app\build\outputs\apk\debug\app-debug.apk
|
53 |
+
echo.
|
54 |
+
echo 📋 معلومات التطبيق:
|
55 |
+
echo - الاسم: محفظتي الموحدة
|
56 |
+
echo - الحجم: ~15-20 MB
|
57 |
+
echo - النوع: Debug APK
|
58 |
+
echo.
|
59 |
+
echo 🔑 بيانات التجربة:
|
60 |
+
echo - رقم الهاتف: 777123456
|
61 |
+
echo - رمز PIN: 1234
|
62 |
+
echo.
|
63 |
+
echo 📲 لتثبيت التطبيق:
|
64 |
+
echo 1. انسخ ملف APK إلى هاتفك
|
65 |
+
echo 2. فعل "مصادر غير معروفة" في إعدادات الأمان
|
66 |
+
echo 3. اضغط على ملف APK لتثبيته
|
67 |
+
echo.
|
68 |
+
|
69 |
+
:: فتح مجلد APK
|
70 |
+
explorer "app\build\outputs\apk\debug\"
|
71 |
+
|
72 |
+
) else (
|
73 |
+
echo.
|
74 |
+
echo ❌ فشل في بناء APK!
|
75 |
+
echo.
|
76 |
+
echo 🔧 الحلول المقترحة:
|
77 |
+
echo 1. تأكد من تثبيت Android SDK
|
78 |
+
echo 2. استخدم Android Studio للبناء
|
79 |
+
echo 3. راجع رسائل الخطأ أعلاه
|
80 |
+
echo.
|
81 |
+
)
|
82 |
+
|
83 |
+
echo.
|
84 |
+
pause
|
BUILD_FROM_GITHUB.bat
ADDED
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@echo off
|
2 |
+
echo ========================================
|
3 |
+
echo بناء APK من GitHub - محفظتي الموحدة
|
4 |
+
echo ========================================
|
5 |
+
echo.
|
6 |
+
|
7 |
+
echo [1/6] التحقق من الأدوات المطلوبة...
|
8 |
+
|
9 |
+
:: التحقق من Git
|
10 |
+
git --version >nul 2>&1
|
11 |
+
if %errorlevel% neq 0 (
|
12 |
+
echo ❌ Git غير مثبت!
|
13 |
+
echo يرجى تثبيت Git من: https://git-scm.com/
|
14 |
+
pause
|
15 |
+
exit /b 1
|
16 |
+
)
|
17 |
+
echo ✅ Git متوفر
|
18 |
+
|
19 |
+
:: التحقق من Node.js
|
20 |
+
node --version >nul 2>&1
|
21 |
+
if %errorlevel% neq 0 (
|
22 |
+
echo ❌ Node.js غير مثبت!
|
23 |
+
echo يرجى تثبيت Node.js من: https://nodejs.org/
|
24 |
+
pause
|
25 |
+
exit /b 1
|
26 |
+
)
|
27 |
+
echo ✅ Node.js متوفر
|
28 |
+
|
29 |
+
:: التحقق من Java
|
30 |
+
java -version >nul 2>&1
|
31 |
+
if %errorlevel% neq 0 (
|
32 |
+
echo ❌ Java غير مثبت!
|
33 |
+
echo يرجى تثبيت Java JDK من: https://adoptium.net/
|
34 |
+
pause
|
35 |
+
exit /b 1
|
36 |
+
)
|
37 |
+
echo ✅ Java متوفر
|
38 |
+
|
39 |
+
echo.
|
40 |
+
echo [2/6] استنساخ المشروع من GitHub...
|
41 |
+
if exist "almada-unified-wallet" (
|
42 |
+
echo مجلد المشروع موجود، سيتم حذفه وإعادة الاستنساخ...
|
43 |
+
rmdir /s /q "almada-unified-wallet"
|
44 |
+
)
|
45 |
+
|
46 |
+
git clone https://github.com/moh77544/---.git almada-unified-wallet
|
47 |
+
if %errorlevel% neq 0 (
|
48 |
+
echo ❌ فشل في استنساخ المشروع!
|
49 |
+
pause
|
50 |
+
exit /b 1
|
51 |
+
)
|
52 |
+
echo ✅ تم استنساخ المشروع
|
53 |
+
|
54 |
+
echo.
|
55 |
+
echo [3/6] الانتقال إلى مجلد المشروع...
|
56 |
+
cd almada-unified-wallet
|
57 |
+
|
58 |
+
echo.
|
59 |
+
echo [4/6] تثبيت التبعيات...
|
60 |
+
call npm install
|
61 |
+
if %errorlevel% neq 0 (
|
62 |
+
echo ❌ فشل في تثبيت التبعيات!
|
63 |
+
pause
|
64 |
+
exit /b 1
|
65 |
+
)
|
66 |
+
|
67 |
+
call npm install -g @ionic/cli @capacitor/cli
|
68 |
+
echo ✅ تم تثبيت التبعيات
|
69 |
+
|
70 |
+
echo.
|
71 |
+
echo [5/6] إعداد ملفات الويب...
|
72 |
+
if not exist "www" mkdir www
|
73 |
+
copy index.html www\ >nul 2>&1
|
74 |
+
copy styles.css www\ >nul 2>&1
|
75 |
+
copy app.js www\ >nul 2>&1
|
76 |
+
copy auth.js www\ >nul 2>&1
|
77 |
+
copy wallets.js www\ >nul 2>&1
|
78 |
+
copy notifications.js www\ >nul 2>&1
|
79 |
+
copy demo.html www\ >nul 2>&1
|
80 |
+
copy src\manifest.json www\ >nul 2>&1
|
81 |
+
echo ✅ تم إعداد ملفات الويب
|
82 |
+
|
83 |
+
echo.
|
84 |
+
echo [6/6] مزامنة وبناء APK...
|
85 |
+
call npx cap sync android
|
86 |
+
if %errorlevel% neq 0 (
|
87 |
+
echo ❌ فشل في مزامنة Capacitor!
|
88 |
+
pause
|
89 |
+
exit /b 1
|
90 |
+
)
|
91 |
+
|
92 |
+
cd android
|
93 |
+
call gradlew assembleDebug
|
94 |
+
if %errorlevel% neq 0 (
|
95 |
+
echo ❌ فشل في بناء APK!
|
96 |
+
echo تأكد من تثبيت Android SDK
|
97 |
+
pause
|
98 |
+
exit /b 1
|
99 |
+
)
|
100 |
+
|
101 |
+
echo.
|
102 |
+
echo ========================================
|
103 |
+
echo 🎉 تم بناء APK بنجاح!
|
104 |
+
echo ========================================
|
105 |
+
echo.
|
106 |
+
echo 📱 ملف APK متوفر في:
|
107 |
+
echo %cd%\app\build\outputs\apk\debug\app-debug.apk
|
108 |
+
echo.
|
109 |
+
echo 📋 معلومات التطبيق:
|
110 |
+
echo - الاسم: محفظتي الموحدة
|
111 |
+
echo - الحجم: ~15-20 MB
|
112 |
+
echo - النوع: Debug APK
|
113 |
+
echo.
|
114 |
+
echo 🔑 بيانات التجربة:
|
115 |
+
echo - رقم الهاتف: 777123456
|
116 |
+
echo - رمز PIN: 1234
|
117 |
+
echo.
|
118 |
+
echo 📲 لتثبيت التطبيق:
|
119 |
+
echo 1. انسخ ملف APK إلى هاتفك
|
120 |
+
echo 2. فعل "مصادر غير معروفة" في إعدادات الأمان
|
121 |
+
echo 3. اضغط على ملف APK لتثبيته
|
122 |
+
echo.
|
123 |
+
|
124 |
+
:: فتح مجلد APK
|
125 |
+
explorer "app\build\outputs\apk\debug\"
|
126 |
+
|
127 |
+
echo.
|
128 |
+
pause
|
BUILD_INSTRUCTIONS.md
ADDED
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# تعليمات بناء تطبيق محفظتي الموحدة
|
2 |
+
|
3 |
+
## متطلبات النظام
|
4 |
+
|
5 |
+
### الأدوات المطلوبة:
|
6 |
+
- **Node.js** (الإصدار 18 أو أحدث)
|
7 |
+
- **npm** أو **yarn**
|
8 |
+
- **Ionic CLI** (الإصدار 7 أو أحدث)
|
9 |
+
- **Angular CLI** (الإصدار 17 أو أحدث)
|
10 |
+
- **Capacitor CLI** (الإصدار 5 أو أحدث)
|
11 |
+
|
12 |
+
### للأندرويد:
|
13 |
+
- **Android Studio** (أحدث إصدار)
|
14 |
+
- **Android SDK** (API Level 24 أو أحدث)
|
15 |
+
- **Java JDK** (الإصدار 11 أو أحدث)
|
16 |
+
|
17 |
+
### لـ iOS:
|
18 |
+
- **Xcode** (أحدث إصدار)
|
19 |
+
- **iOS SDK** (iOS 13 أو أحدث)
|
20 |
+
- **macOS** (مطلوب لبناء تطبيقات iOS)
|
21 |
+
|
22 |
+
## خطوات التثبيت
|
23 |
+
|
24 |
+
### 1. تثبيت الأدوات العامة
|
25 |
+
```bash
|
26 |
+
# تثبيت Node.js من https://nodejs.org
|
27 |
+
|
28 |
+
# تثبيت Ionic CLI
|
29 |
+
npm install -g @ionic/cli
|
30 |
+
|
31 |
+
# تثبيت Angular CLI
|
32 |
+
npm install -g @angular/cli
|
33 |
+
|
34 |
+
# تثبيت Capacitor CLI
|
35 |
+
npm install -g @capacitor/cli
|
36 |
+
```
|
37 |
+
|
38 |
+
### 2. إعداد المشروع
|
39 |
+
```bash
|
40 |
+
# الانتقال إلى مجلد المشروع
|
41 |
+
cd almada
|
42 |
+
|
43 |
+
# تثبيت التبعيات
|
44 |
+
npm install
|
45 |
+
|
46 |
+
# أو باستخدام yarn
|
47 |
+
yarn install
|
48 |
+
```
|
49 |
+
|
50 |
+
### 3. إعداد Capacitor
|
51 |
+
```bash
|
52 |
+
# تهيئة Capacitor
|
53 |
+
npx cap init "محفظتي الموحدة" "com.almada.unifiedwallet"
|
54 |
+
|
55 |
+
# إضافة منصات
|
56 |
+
npx cap add android
|
57 |
+
npx cap add ios
|
58 |
+
```
|
59 |
+
|
60 |
+
## بناء التطبيق
|
61 |
+
|
62 |
+
### 1. بناء تطبيق الويب
|
63 |
+
```bash
|
64 |
+
# بناء للتطوير
|
65 |
+
ionic build
|
66 |
+
|
67 |
+
# بناء للإنتاج
|
68 |
+
ionic build --prod
|
69 |
+
```
|
70 |
+
|
71 |
+
### 2. بناء تطبيق الأندرويد
|
72 |
+
|
73 |
+
#### أ. إعداد Android Studio
|
74 |
+
```bash
|
75 |
+
# نسخ الملفات إلى مجلد الأندرويد
|
76 |
+
npx cap copy android
|
77 |
+
|
78 |
+
# مزامنة المشروع
|
79 |
+
npx cap sync android
|
80 |
+
|
81 |
+
# فتح في Android Studio
|
82 |
+
npx cap open android
|
83 |
+
```
|
84 |
+
|
85 |
+
#### ب. بناء APK من سطر الأوامر
|
86 |
+
```bash
|
87 |
+
# بناء APK للتطوير
|
88 |
+
cd android
|
89 |
+
./gradlew assembleDebug
|
90 |
+
|
91 |
+
# بناء APK للإنتاج
|
92 |
+
./gradlew assembleRelease
|
93 |
+
|
94 |
+
# بناء AAB للنشر في Google Play
|
95 |
+
./gradlew bundleRelease
|
96 |
+
```
|
97 |
+
|
98 |
+
#### ج. بناء APK من Android Studio
|
99 |
+
1. افتح Android Studio
|
100 |
+
2. اختر **Build > Build Bundle(s) / APK(s) > Build APK(s)**
|
101 |
+
3. انتظر حتى اكتمال البناء
|
102 |
+
4. ستجد ملف APK في: `android/app/build/outputs/apk/`
|
103 |
+
|
104 |
+
### 3. بناء تطبيق iOS
|
105 |
+
|
106 |
+
#### أ. إعداد Xcode
|
107 |
+
```bash
|
108 |
+
# نسخ الملفات إلى مجلد iOS
|
109 |
+
npx cap copy ios
|
110 |
+
|
111 |
+
# مزامنة المشروع
|
112 |
+
npx cap sync ios
|
113 |
+
|
114 |
+
# فتح في Xcode
|
115 |
+
npx cap open ios
|
116 |
+
```
|
117 |
+
|
118 |
+
#### ب. بناء IPA من Xcode
|
119 |
+
1. افتح Xcode
|
120 |
+
2. اختر جهاز أو محاكي
|
121 |
+
3. اختر **Product > Archive**
|
122 |
+
4. بعد اكتمال الأرشفة، اختر **Distribute App**
|
123 |
+
5. اتبع التعليمات لإنشاء ملف IPA
|
124 |
+
|
125 |
+
## تشغيل التطبيق للتطوير
|
126 |
+
|
127 |
+
### 1. تشغيل في المتصفح
|
128 |
+
```bash
|
129 |
+
# تشغيل خادم التطوير
|
130 |
+
ionic serve
|
131 |
+
|
132 |
+
# تشغيل مع إعادة التحميل التلقائي
|
133 |
+
ionic serve --lab
|
134 |
+
```
|
135 |
+
|
136 |
+
### 2. تشغيل على الأندرويد
|
137 |
+
```bash
|
138 |
+
# تشغيل على جهاز أو محاكي
|
139 |
+
ionic cap run android
|
140 |
+
|
141 |
+
# تشغيل مع إعادة التحميل المباشر
|
142 |
+
ionic cap run android --livereload --external
|
143 |
+
```
|
144 |
+
|
145 |
+
### 3. تشغيل على iOS
|
146 |
+
```bash
|
147 |
+
# تشغيل على جهاز أو محاكي
|
148 |
+
ionic cap run ios
|
149 |
+
|
150 |
+
# تشغيل مع إعادة التحميل المباشر
|
151 |
+
ionic cap run ios --livereload --external
|
152 |
+
```
|
153 |
+
|
154 |
+
## إعدادات الإنتاج
|
155 |
+
|
156 |
+
### 1. تحديث المتغيرات
|
157 |
+
قم بتحديث ملف `src/environments/environment.prod.ts`:
|
158 |
+
```typescript
|
159 |
+
export const environment = {
|
160 |
+
production: true,
|
161 |
+
apiUrl: 'https://api.almada.com',
|
162 |
+
// ... باقي الإعدادات
|
163 |
+
};
|
164 |
+
```
|
165 |
+
|
166 |
+
### 2. إعداد الأيقونات والشاشات
|
167 |
+
```bash
|
168 |
+
# إنشاء الأيقونات والشاشات تلقائياً
|
169 |
+
npm install -g cordova-res
|
170 |
+
cordova-res android --skip-config --copy
|
171 |
+
cordova-res ios --skip-config --copy
|
172 |
+
```
|
173 |
+
|
174 |
+
### 3. توقيع التطبيق للأندرويد
|
175 |
+
```bash
|
176 |
+
# إنشاء مفتاح التوقيع
|
177 |
+
keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000
|
178 |
+
|
179 |
+
# إضافة إعدادات التوقيع في android/app/build.gradle
|
180 |
+
```
|
181 |
+
|
182 |
+
### 4. إعداد التوقيع لـ iOS
|
183 |
+
1. افتح Xcode
|
184 |
+
2. اذهب إلى **Signing & Capabilities**
|
185 |
+
3. اختر **Team** و **Bundle Identifier**
|
186 |
+
4. تأكد من إعداد **Provisioning Profile**
|
187 |
+
|
188 |
+
## اختبار التطبيق
|
189 |
+
|
190 |
+
### 1. اختبار الوحدة
|
191 |
+
```bash
|
192 |
+
# تشغيل اختبارات الوحدة
|
193 |
+
npm test
|
194 |
+
|
195 |
+
# تشغيل مع المراقبة
|
196 |
+
npm test -- --watch
|
197 |
+
```
|
198 |
+
|
199 |
+
### 2. اختبار النهاية إلى النهاية
|
200 |
+
```bash
|
201 |
+
# تشغيل اختبارات e2e
|
202 |
+
npm run e2e
|
203 |
+
```
|
204 |
+
|
205 |
+
### 3. اختبار على الأجهزة
|
206 |
+
```bash
|
207 |
+
# تثبيت على جهاز أندرويد
|
208 |
+
adb install android/app/build/outputs/apk/debug/app-debug.apk
|
209 |
+
|
210 |
+
# عرض سجلات الأندرويد
|
211 |
+
adb logcat
|
212 |
+
```
|
213 |
+
|
214 |
+
## نشر التطبيق
|
215 |
+
|
216 |
+
### 1. Google Play Store
|
217 |
+
1. إنشاء حساب مطور في Google Play Console
|
218 |
+
2. رفع ملف AAB
|
219 |
+
3. ملء معلومات التطبيق
|
220 |
+
4. إرسال للمراجعة
|
221 |
+
|
222 |
+
### 2. Apple App Store
|
223 |
+
1. إنشاء حساب مطور في App Store Connect
|
224 |
+
2. رفع التطبيق عبر Xcode أو Application Loader
|
225 |
+
3. ملء معلومات التطبيق
|
226 |
+
4. إرسال للمراجعة
|
227 |
+
|
228 |
+
## استكشاف الأخطاء
|
229 |
+
|
230 |
+
### مشاكل شائعة:
|
231 |
+
|
232 |
+
#### 1. خطأ في بناء الأندرويد
|
233 |
+
```bash
|
234 |
+
# تنظيف المشروع
|
235 |
+
cd android
|
236 |
+
./gradlew clean
|
237 |
+
|
238 |
+
# إعادة بناء
|
239 |
+
./gradlew build
|
240 |
+
```
|
241 |
+
|
242 |
+
#### 2. مشاكل Capacitor
|
243 |
+
```bash
|
244 |
+
# إعادة مزامنة
|
245 |
+
npx cap sync
|
246 |
+
|
247 |
+
# تحديث Capacitor
|
248 |
+
npm update @capacitor/core @capacitor/cli
|
249 |
+
```
|
250 |
+
|
251 |
+
#### 3. مشاكل الأذونات
|
252 |
+
تأكد من إضافة الأذونات المطلوبة في:
|
253 |
+
- `android/app/src/main/AndroidManifest.xml` للأندرويد
|
254 |
+
- `ios/App/App/Info.plist` لـ iOS
|
255 |
+
|
256 |
+
## الدعم والمساعدة
|
257 |
+
|
258 |
+
للحصول على المساعدة:
|
259 |
+
1. راجع [وثائق Ionic](https://ionicframework.com/docs)
|
260 |
+
2. راجع [وثائق Capacitor](https://capacitorjs.com/docs)
|
261 |
+
3. تواصل مع فريق التطوير
|
262 |
+
|
263 |
+
---
|
264 |
+
|
265 |
+
**ملاحظة**: تأكد من تحديث جميع التبعيات بانتظام للحصول على أحدث الميزات وإصلاحات الأمان.
|
CLOUD_BUILD_QUICK.md
ADDED
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# ☁️ البناء السحابي السريع - 15 دقيقة فقط!
|
2 |
+
|
3 |
+
## 🚀 **الهدف:** APK جاهز من السحابة في 15 دقيقة!
|
4 |
+
|
5 |
+
---
|
6 |
+
|
7 |
+
## ⚡ **الخطوات السريعة:**
|
8 |
+
|
9 |
+
### **1️⃣ إنشاء حساب GitHub** (2 دقيقة)
|
10 |
+
```
|
11 |
+
🌐 اذهب إلى: https://github.com
|
12 |
+
📝 اضغط "Sign up" وأنشئ حساب
|
13 |
+
✅ تأكد من البريد الإلكتروني
|
14 |
+
```
|
15 |
+
|
16 |
+
### **2️⃣ إنشاء Repository** (1 دقيقة)
|
17 |
+
```
|
18 |
+
➕ اضغط "New repository"
|
19 |
+
📝 الاسم: almada-unified-wallet
|
20 |
+
📄 الوصف: محفظتي الموحدة
|
21 |
+
🌍 اختر "Public"
|
22 |
+
✅ اضغط "Create repository"
|
23 |
+
```
|
24 |
+
|
25 |
+
### **3️⃣ رفع الملفات** (5 دقائق)
|
26 |
+
```
|
27 |
+
📁 اضغط "uploading an existing file"
|
28 |
+
📤 اسحب وأفلت هذه الملفات:
|
29 |
+
✅ .github/workflows/build-apk.yml
|
30 |
+
✅ index.html
|
31 |
+
✅ styles.css
|
32 |
+
✅ app.js
|
33 |
+
✅ auth.js
|
34 |
+
✅ wallets.js
|
35 |
+
✅ notifications.js
|
36 |
+
✅ package.json
|
37 |
+
✅ capacitor.config.ts
|
38 |
+
✅ مجلد android كامل
|
39 |
+
💬 رسالة: "Initial commit"
|
40 |
+
✅ اضغط "Commit changes"
|
41 |
+
```
|
42 |
+
|
43 |
+
### **4️⃣ تشغيل البناء** (10 دقائق)
|
44 |
+
```
|
45 |
+
🔧 اذهب إلى تبويب "Actions"
|
46 |
+
🚀 اضغط "Run workflow"
|
47 |
+
⏰ انتظر 10-15 دقيقة
|
48 |
+
✅ ابحث عن علامة ✅ خضراء
|
49 |
+
```
|
50 |
+
|
51 |
+
### **5️⃣ تحميل APK** (1 دقيقة)
|
52 |
+
```
|
53 |
+
📱 اضغط على Build الناجح
|
54 |
+
📦 في الأسفل: "Artifacts"
|
55 |
+
⬇️ حمل: almada-unified-wallet-apk
|
56 |
+
📱 استخرج APK وثبته!
|
57 |
+
```
|
58 |
+
|
59 |
+
---
|
60 |
+
|
61 |
+
## 🎯 **النتيجة:**
|
62 |
+
|
63 |
+
✅ **ملف APK** جاهز للتثبيت
|
64 |
+
✅ **بناء تلقائي** عند كل تحديث
|
65 |
+
✅ **مجاني تماماً** مع GitHub
|
66 |
+
✅ **رابط تحميل** مباشر
|
67 |
+
|
68 |
+
---
|
69 |
+
|
70 |
+
## 📱 **معلومات APK:**
|
71 |
+
|
72 |
+
| المعلومة | القيمة |
|
73 |
+
|---------|--------|
|
74 |
+
| **الاسم** | app-debug.apk |
|
75 |
+
| **الحجم** | ~15-20 MB |
|
76 |
+
| **بيانات التجربة** | 777123456 / 1234 |
|
77 |
+
| **متوافق مع** | Android 7.0+ |
|
78 |
+
|
79 |
+
---
|
80 |
+
|
81 |
+
## 🔄 **للتحديثات المستقبلية:**
|
82 |
+
|
83 |
+
```
|
84 |
+
1. عدل أي ملف في GitHub
|
85 |
+
2. Commit التغييرات
|
86 |
+
3. APK جديد سيُبنى تلقائياً!
|
87 |
+
```
|
88 |
+
|
89 |
+
---
|
90 |
+
|
91 |
+
## 🆘 **مشاكل شائعة:**
|
92 |
+
|
93 |
+
### **❌ Build فشل:**
|
94 |
+
```
|
95 |
+
✅ تحقق من رفع جميع الملفات
|
96 |
+
✅ راجع logs في Actions
|
97 |
+
✅ تأكد من package.json صحيح
|
98 |
+
```
|
99 |
+
|
100 |
+
### **❌ لا يوجد APK:**
|
101 |
+
```
|
102 |
+
✅ انتظر اكتمال Build (علامة ✅)
|
103 |
+
✅ ابحث في "Artifacts"
|
104 |
+
✅ حدث الصفحة
|
105 |
+
```
|
106 |
+
|
107 |
+
---
|
108 |
+
|
109 |
+
## 🎊 **مبروك!**
|
110 |
+
|
111 |
+
عند اكتمال هذه الخطوات:
|
112 |
+
- 📱 **APK جاهز** للتثبيت
|
113 |
+
- ☁️ **بناء سحابي** مجاني
|
114 |
+
- 🔄 **تحديثات تلقائية**
|
115 |
+
- 🌍 **متاح للعالم**
|
116 |
+
|
117 |
+
---
|
118 |
+
|
119 |
+
## 📞 **تحتاج مساعدة؟**
|
120 |
+
|
121 |
+
راجع الدليل المفصل: `GITHUB_SETUP_GUIDE.md`
|
122 |
+
|
123 |
+
**🚀 تطبيقك على بُعد 15 دقيقة من الواقع!**
|
GITHUB_SETUP_GUIDE.md
ADDED
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# ☁️ دليل إعداد البناء السحابي - GitHub Actions
|
2 |
+
|
3 |
+
## 🎯 **الهدف:** بناء APK تلقائياً في السحابة مجاناً!
|
4 |
+
|
5 |
+
---
|
6 |
+
|
7 |
+
## 📋 **الخطوات المطلوبة:**
|
8 |
+
|
9 |
+
### **الخطوة 1: إنشاء حساب GitHub** ⏱️ (2 دقيقة)
|
10 |
+
|
11 |
+
1. **اذهب إلى:** https://github.com
|
12 |
+
2. **اضغط "Sign up"** وأنشئ حساب جديد
|
13 |
+
3. **تأكد من البريد الإلكتروني**
|
14 |
+
|
15 |
+
### **الخطوة 2: إنشاء Repository جديد** ⏱️ (1 دقيقة)
|
16 |
+
|
17 |
+
1. **اضغط "New repository"** (الزر الأخضر)
|
18 |
+
2. **اسم المستودع:** `almada-unified-wallet`
|
19 |
+
3. **الوصف:** `محفظتي الموحدة - تطبيق المحافظ الإلكترونية اليمنية`
|
20 |
+
4. **اختر "Public"** (مجاني)
|
21 |
+
5. **فعل "Add a README file"**
|
22 |
+
6. **اضغط "Create repository"**
|
23 |
+
|
24 |
+
### **الخطوة 3: رفع الملفات** ⏱️ (5 دقائق)
|
25 |
+
|
26 |
+
#### **الطريقة الأسهل: عبر الموقع**
|
27 |
+
|
28 |
+
1. **في صفحة Repository، اضغط "uploading an existing file"**
|
29 |
+
2. **اسحب وأفلت الملفات التالية:**
|
30 |
+
```
|
31 |
+
📁 .github/workflows/build-apk.yml
|
32 |
+
📄 index.html
|
33 |
+
📄 styles.css
|
34 |
+
📄 app.js
|
35 |
+
📄 auth.js
|
36 |
+
📄 wallets.js
|
37 |
+
📄 notifications.js
|
38 |
+
📄 demo.html
|
39 |
+
📄 package.json
|
40 |
+
📄 capacitor.config.ts
|
41 |
+
📄 .gitignore
|
42 |
+
📁 src/manifest.json
|
43 |
+
📁 android/ (كامل)
|
44 |
+
```
|
45 |
+
3. **اكتب رسالة:** `Initial commit - محفظتي الموحدة`
|
46 |
+
4. **اضغط "Commit changes"**
|
47 |
+
|
48 |
+
#### **الطريقة المتقدمة: Git Command Line**
|
49 |
+
|
50 |
+
```bash
|
51 |
+
# في مجلد المشروع
|
52 |
+
git init
|
53 |
+
git add .
|
54 |
+
git commit -m "Initial commit - محفظتي الموحدة"
|
55 |
+
git branch -M main
|
56 |
+
git remote add origin https://github.com/USERNAME/almada-unified-wallet.git
|
57 |
+
git push -u origin main
|
58 |
+
```
|
59 |
+
|
60 |
+
### **الخطوة 4: تشغيل البناء التلقائي** ⏱️ (10-15 دقيقة)
|
61 |
+
|
62 |
+
1. **اذهب إلى تبويب "Actions"** في Repository
|
63 |
+
2. **ستجد workflow اسمه:** `🚀 Build APK - محفظتي الموحدة`
|
64 |
+
3. **اضغط "Run workflow"** إذا لم يبدأ تلقائياً
|
65 |
+
4. **انتظر اكتمال البناء** (10-15 دقيقة)
|
66 |
+
|
67 |
+
### **الخطوة 5: تحميل APK** ⏱️ (1 دقيقة)
|
68 |
+
|
69 |
+
عند اكتمال البناء:
|
70 |
+
|
71 |
+
1. **اضغط على Build الناجح** (علامة ✅ خضراء)
|
72 |
+
2. **في الأسفل، ستجد "Artifacts"**
|
73 |
+
3. **اضغط على:** `almada-unified-wallet-apk`
|
74 |
+
4. **حمل ملف ZIP واستخرج APK منه**
|
75 |
+
|
76 |
+
---
|
77 |
+
|
78 |
+
## 🎯 **البدائل السحابية الأخرى:**
|
79 |
+
|
80 |
+
### **Ionic Appflow (مجاني للمشاريع الصغيرة):**
|
81 |
+
|
82 |
+
```bash
|
83 |
+
# تثبيت Ionic CLI
|
84 |
+
npm install -g @ionic/cli
|
85 |
+
|
86 |
+
# تسجيل الدخول
|
87 |
+
ionic login
|
88 |
+
|
89 |
+
# ربط المشروع
|
90 |
+
ionic link
|
91 |
+
|
92 |
+
# بناء في السحابة
|
93 |
+
ionic capacitor build android --prod
|
94 |
+
```
|
95 |
+
|
96 |
+
### **CodeMagic (مجاني 500 دقيقة/شهر):**
|
97 |
+
|
98 |
+
1. اذهب إلى: https://codemagic.io
|
99 |
+
2. ربط حساب GitHub
|
100 |
+
3. اختر Repository
|
101 |
+
4. إعداد workflow للأندرويد
|
102 |
+
5. بناء تلقائي
|
103 |
+
|
104 |
+
---
|
105 |
+
|
106 |
+
## 📱 **ما ستحصل عليه:**
|
107 |
+
|
108 |
+
### **من GitHub Actions:**
|
109 |
+
- ✅ **بناء تلقائي** عند كل تحديث
|
110 |
+
- ✅ **APK مجاني** بدون حدود
|
111 |
+
- ✅ **تاريخ الإصدارات** كامل
|
112 |
+
- ✅ **رابط تحميل** مباشر
|
113 |
+
|
114 |
+
### **معلومات APK:**
|
115 |
+
- 📱 **الاسم:** `app-debug.apk`
|
116 |
+
- 💾 **الحجم:** ~15-20 MB
|
117 |
+
- 🔧 **النوع:** Debug APK
|
118 |
+
- 📲 **جاهز للتثبيت** على أي هاتف أندرويد
|
119 |
+
|
120 |
+
---
|
121 |
+
|
122 |
+
## 🔄 **التحديثات المستقبلية:**
|
123 |
+
|
124 |
+
### **لإضافة ميزات جديدة:**
|
125 |
+
1. **عدل الملفات** في Repository
|
126 |
+
2. **Commit التغييرات**
|
127 |
+
3. **APK جديد** سيُبنى تلقائياً!
|
128 |
+
|
129 |
+
### **لإنشاء Release:**
|
130 |
+
```bash
|
131 |
+
# إنشاء tag جديد
|
132 |
+
git tag v1.0.1
|
133 |
+
git push origin v1.0.1
|
134 |
+
|
135 |
+
# سيُنشئ Release تلقائياً مع APK
|
136 |
+
```
|
137 |
+
|
138 |
+
---
|
139 |
+
|
140 |
+
## 🆘 **حل المشاكل:**
|
141 |
+
|
142 |
+
### **مشكلة: Build فشل**
|
143 |
+
```
|
144 |
+
الحل:
|
145 |
+
1. تحقق من logs في Actions
|
146 |
+
2. تأكد من رفع جميع الملفات
|
147 |
+
3. تحقق من package.json
|
148 |
+
```
|
149 |
+
|
150 |
+
### **مشكلة: لا يوجد Artifacts**
|
151 |
+
```
|
152 |
+
الحل:
|
153 |
+
1. تأكد من نجاح Build (علامة ✅)
|
154 |
+
2. انتظر اكتمال جميع الخطوات
|
155 |
+
3. حدث الصفحة
|
156 |
+
```
|
157 |
+
|
158 |
+
### **مشكلة: APK لا يعمل**
|
159 |
+
```
|
160 |
+
الحل:
|
161 |
+
1. تأكد من تفعيل "مصادر غير معروفة"
|
162 |
+
2. تحقق من توافق إصدار الأندرويد
|
163 |
+
3. أعد تحميل APK
|
164 |
+
```
|
165 |
+
|
166 |
+
---
|
167 |
+
|
168 |
+
## 💡 **نصائح مهمة:**
|
169 |
+
|
170 |
+
### **للنجاح:**
|
171 |
+
- 📁 **ارفع جميع الملفات** المطلوبة
|
172 |
+
- 🌐 **تأكد من اتصال الإنترنت** أثناء البناء
|
173 |
+
- ⏰ **انتظر اكتمال** جميع الخطوات
|
174 |
+
|
175 |
+
### **للأمان:**
|
176 |
+
- 🔒 **لا تشارك** معلومات حساسة في Repository العام
|
177 |
+
- 🔑 **استخدم Secrets** للمعلومات الحساسة
|
178 |
+
- 🛡️ **راجع الأذونات** بانتظام
|
179 |
+
|
180 |
+
### **للتطوير:**
|
181 |
+
- 📝 **اكتب وصف واضح** للـ commits
|
182 |
+
- 🏷️ **استخدم tags** للإصدارات
|
183 |
+
- 📚 **حدث README** بانتظام
|
184 |
+
|
185 |
+
---
|
186 |
+
|
187 |
+
## 🎉 **النتيجة النهائية:**
|
188 |
+
|
189 |
+
عند اكتمال هذه الخطوات، ستحصل على:
|
190 |
+
|
191 |
+
- ☁️ **نظام بناء سحابي** مجاني
|
192 |
+
- 🔄 **APK تلقائي** عند كل تحديث
|
193 |
+
- 📱 **رابط تحميل** مباشر
|
194 |
+
- 🌍 **متاح للعالم** عبر GitHub
|
195 |
+
|
196 |
+
---
|
197 |
+
|
198 |
+
## 📞 **تحتاج مساعدة؟**
|
199 |
+
|
200 |
+
إذا واجهت أي مشكلة:
|
201 |
+
1. **راجع logs** في GitHub Actions
|
202 |
+
2. **تحقق من الملفات** المرفوعة
|
203 |
+
3. **تواصل للمساعدة**
|
204 |
+
|
205 |
+
**🚀 مبروك! تطبيقك سيُبنى في السحابة!**
|
206 |
+
|
207 |
+
---
|
208 |
+
|
209 |
+
**💡 ملاحظة:** GitHub Actions مجاني للمشاريع العامة مع 2000 دقيقة/شهر للمشاريع الخاصة.
|
INSTALL_ANDROID_SDK.bat
ADDED
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@echo off
|
2 |
+
echo ========================================
|
3 |
+
echo تثبيت Android SDK - محفظتي الموحدة
|
4 |
+
echo ========================================
|
5 |
+
echo.
|
6 |
+
|
7 |
+
echo هذا الملف سيساعدك في تثبيت Android SDK بدون Android Studio
|
8 |
+
echo.
|
9 |
+
|
10 |
+
echo [الخطوة 1] إنشاء مجلد Android SDK...
|
11 |
+
if not exist "C:\Android" mkdir "C:\Android"
|
12 |
+
if not exist "C:\Android\cmdline-tools" mkdir "C:\Android\cmdline-tools"
|
13 |
+
echo ✅ تم إنشاء المجلدات
|
14 |
+
|
15 |
+
echo.
|
16 |
+
echo [الخطوة 2] تحميل Command Line Tools...
|
17 |
+
echo يرجى تحميل Command Line Tools من:
|
18 |
+
echo https://developer.android.com/studio#command-tools
|
19 |
+
echo.
|
20 |
+
echo اختر: "Command line tools only" > Windows
|
21 |
+
echo.
|
22 |
+
echo بعد التحميل:
|
23 |
+
echo 1. استخرج الملف المضغوط
|
24 |
+
echo 2. انسخ محتويات مجلد cmdline-tools إلى:
|
25 |
+
echo C:\Android\cmdline-tools\latest\
|
26 |
+
echo.
|
27 |
+
pause
|
28 |
+
|
29 |
+
echo.
|
30 |
+
echo [الخطوة 3] إعداد متغيرات البيئة...
|
31 |
+
echo سيتم إضافة متغيرات البيئة التالية:
|
32 |
+
echo ANDROID_HOME=C:\Android
|
33 |
+
echo ANDROID_SDK_ROOT=C:\Android
|
34 |
+
echo.
|
35 |
+
|
36 |
+
setx ANDROID_HOME "C:\Android" /M >nul 2>&1
|
37 |
+
setx ANDROID_SDK_ROOT "C:\Android" /M >nul 2>&1
|
38 |
+
|
39 |
+
echo ✅ تم إعداد متغيرات البيئة
|
40 |
+
|
41 |
+
echo.
|
42 |
+
echo [الخطوة 4] إضافة إلى PATH...
|
43 |
+
set "newPath=C:\Android\cmdline-tools\latest\bin;C:\Android\platform-tools"
|
44 |
+
|
45 |
+
for /f "tokens=2*" %%a in ('reg query "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /v PATH 2^>nul') do set "currentPath=%%b"
|
46 |
+
|
47 |
+
if not defined currentPath set "currentPath="
|
48 |
+
|
49 |
+
echo %currentPath% | find /i "%newPath%" >nul
|
50 |
+
if %errorlevel% neq 0 (
|
51 |
+
setx PATH "%currentPath%;%newPath%" /M >nul 2>&1
|
52 |
+
echo ✅ تم إضافة Android SDK إلى PATH
|
53 |
+
) else (
|
54 |
+
echo ✅ Android SDK موجود بالفعل في PATH
|
55 |
+
)
|
56 |
+
|
57 |
+
echo.
|
58 |
+
echo [الخطوة 5] إعادة تشغيل Command Prompt...
|
59 |
+
echo يرجى إغلاق هذه النافذة وفتح Command Prompt جديد
|
60 |
+
echo ثم تشغيل الأوامر التالية:
|
61 |
+
echo.
|
62 |
+
echo sdkmanager --version
|
63 |
+
echo sdkmanager "platform-tools"
|
64 |
+
echo sdkmanager "platforms;android-33"
|
65 |
+
echo sdkmanager "build-tools;33.0.0"
|
66 |
+
echo.
|
67 |
+
|
68 |
+
echo ========================================
|
69 |
+
echo 📋 ملخص ما تم:
|
70 |
+
echo ========================================
|
71 |
+
echo ✅ إنشاء مجلد C:\Android
|
72 |
+
echo ✅ إعداد ANDROID_HOME
|
73 |
+
echo ✅ إعداد ANDROID_SDK_ROOT
|
74 |
+
echo ✅ إضافة إلى PATH
|
75 |
+
echo.
|
76 |
+
echo 📝 المطلوب منك:
|
77 |
+
echo 1. تحميل Command Line Tools
|
78 |
+
echo 2. استخراج إلى C:\Android\cmdline-tools\latest\
|
79 |
+
echo 3. إعادة تشغيل Command Prompt
|
80 |
+
echo 4. تشغيل أوامر sdkmanager
|
81 |
+
echo.
|
82 |
+
echo بعد ذلك يمكنك تشغيل QUICK_APK_BUILD.bat
|
83 |
+
echo ========================================
|
84 |
+
pause
|
PROJECT_SUMMARY.md
ADDED
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 📊 ملخص مشروع محفظتي الموحدة - التحويل إلى APK
|
2 |
+
|
3 |
+
## 🎯 **الهدف المحقق**
|
4 |
+
تم تحويل تطبيق **محفظتي الموحدة** من تطبيق ويب إلى **تطبيق أندرويد APK** جاهز للتثبيت والاستخدام.
|
5 |
+
|
6 |
+
---
|
7 |
+
|
8 |
+
## ✅ **ما تم إنجازه بنجاح**
|
9 |
+
|
10 |
+
### **1. إعداد البيئة التقنية**
|
11 |
+
- ✅ تحويل المشروع إلى **Capacitor** (تقنية التطبيقات الهجينة)
|
12 |
+
- ✅ إعداد **package.json** مع جميع التبعيات المطلوبة
|
13 |
+
- ✅ تثبيت **Java JDK 11** للبناء
|
14 |
+
- ✅ إعداد **capacitor.config.ts** للتكوين
|
15 |
+
|
16 |
+
### **2. إعداد مشروع الأندرويد**
|
17 |
+
- ✅ إضافة منصة الأندرويد: `cap add android`
|
18 |
+
- ✅ إعداد **AndroidManifest.xml** مع الأذونات المطلوبة
|
19 |
+
- ✅ تكوين **strings.xml** باللغة العربية
|
20 |
+
- ✅ نسخ ملفات التطبيق إلى مجلد `www`
|
21 |
+
|
22 |
+
### **3. إضافة الأذونات المطلوبة**
|
23 |
+
```xml
|
24 |
+
<!-- الأذونات الأساسية -->
|
25 |
+
<uses-permission android:name="android.permission.INTERNET" />
|
26 |
+
<uses-permission android:name="android.permission.CAMERA" />
|
27 |
+
<uses-permission android:name="android.permission.VIBRATE" />
|
28 |
+
|
29 |
+
<!-- أذونات SMS (للمستقبل) -->
|
30 |
+
<uses-permission android:name="android.permission.READ_SMS" />
|
31 |
+
<uses-permission android:name="android.permission.RECEIVE_SMS" />
|
32 |
+
|
33 |
+
<!-- أذونات البصمة -->
|
34 |
+
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
35 |
+
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
36 |
+
```
|
37 |
+
|
38 |
+
### **4. تحسين التطبيق للجوال**
|
39 |
+
- ✅ إضافة **Capacitor Core** للتفاعل مع ميزات الجهاز
|
40 |
+
- ✅ إعداد **SplashScreen** (شاشة البداية)
|
41 |
+
- ✅ تكوين **StatusBar** (شريط الحالة)
|
42 |
+
- ✅ إعداد **LocalNotifications** (الإشعارات المحلية)
|
43 |
+
|
44 |
+
### **5. إنشاء ملفات التعليمات**
|
45 |
+
- ✅ **APK_BUILD_GUIDE.md** - دليل بناء مفصل
|
46 |
+
- ✅ **QUICK_APK_BUILD.bat** - ملف بناء سريع
|
47 |
+
- ✅ **INSTALL_ANDROID_SDK.bat** - تثبيت Android SDK
|
48 |
+
- ✅ **APK_README.md** - دليل المستخدم النهائي
|
49 |
+
|
50 |
+
---
|
51 |
+
|
52 |
+
## 📱 **معلومات التطبيق النهائي**
|
53 |
+
|
54 |
+
| المعلومة | القيمة |
|
55 |
+
|---------|--------|
|
56 |
+
| **اسم التطبيق** | محفظتي الموحدة |
|
57 |
+
| **Package ID** | com.almada.unifiedwallet |
|
58 |
+
| **الإصدار** | 1.0.0 |
|
59 |
+
| **التقنية** | Capacitor + HTML/CSS/JS |
|
60 |
+
| **الحد الأدنى للأندرويد** | API 24 (Android 7.0) |
|
61 |
+
| **حجم APK المتوقع** | ~15-20 MB |
|
62 |
+
|
63 |
+
---
|
64 |
+
|
65 |
+
## 🔧 **الحالة الحالية**
|
66 |
+
|
67 |
+
### **جاهز للبناء:**
|
68 |
+
- ✅ جميع الملفات معدة
|
69 |
+
- ✅ التبعيات مثبتة
|
70 |
+
- ✅ Java JDK متوفر
|
71 |
+
- ✅ مشروع الأندرويد جاهز
|
72 |
+
|
73 |
+
### **المطلوب لإكمال البناء:**
|
74 |
+
- ❌ **Android SDK** (يحتاج تثبيت)
|
75 |
+
- ❌ تشغيل أمر البناء: `gradlew assembleDebug`
|
76 |
+
|
77 |
+
---
|
78 |
+
|
79 |
+
## 🚀 **طرق إكمال البناء**
|
80 |
+
|
81 |
+
### **الطريقة الأسهل:**
|
82 |
+
1. تثبيت [Android Studio](https://developer.android.com/studio)
|
83 |
+
2. فتح مجلد `android` في Android Studio
|
84 |
+
3. **Build > Build APK**
|
85 |
+
|
86 |
+
### **الطريقة السريعة:**
|
87 |
+
1. تشغيل `INSTALL_ANDROID_SDK.bat`
|
88 |
+
2. تشغيل `QUICK_APK_BUILD.bat`
|
89 |
+
|
90 |
+
### **الطريقة اليدوية:**
|
91 |
+
```bash
|
92 |
+
# تثبيت Android SDK
|
93 |
+
# ثم:
|
94 |
+
cd android
|
95 |
+
gradlew assembleDebug
|
96 |
+
```
|
97 |
+
|
98 |
+
---
|
99 |
+
|
100 |
+
## 📂 **هيكل المشروع النهائي**
|
101 |
+
|
102 |
+
```
|
103 |
+
almada/
|
104 |
+
├── 📱 android/ # مشروع الأندرويد الكامل
|
105 |
+
│ ├── app/ # تطبيق الأندرويد
|
106 |
+
│ ├── gradle/ # إعدادات Gradle
|
107 |
+
│ └── build.gradle # ملف البناء
|
108 |
+
├── 🌐 www/ # ملفات التطبيق
|
109 |
+
│ ├── index.html # الصفحة الرئيسية
|
110 |
+
│ ├── styles.css # التصميمات
|
111 |
+
│ ├── app.js # المنطق الرئيسي
|
112 |
+
│ ├── auth.js # نظام المصادقة
|
113 |
+
│ ├── wallets.js # إدارة المحافظ
|
114 |
+
│ └── notifications.js # الإشعارات
|
115 |
+
├── 📄 capacitor.config.ts # إعدادات Capacitor
|
116 |
+
├── 📦 package.json # تبعيات المشروع
|
117 |
+
├── 🔨 QUICK_APK_BUILD.bat # بناء سريع
|
118 |
+
├── ⚙️ INSTALL_ANDROID_SDK.bat # تثبيت SDK
|
119 |
+
├── 📖 APK_BUILD_GUIDE.md # دليل البناء المفصل
|
120 |
+
├── 📋 APK_README.md # دليل المستخدم
|
121 |
+
└── 📊 PROJECT_SUMMARY.md # هذا الملف
|
122 |
+
```
|
123 |
+
|
124 |
+
---
|
125 |
+
|
126 |
+
## 🎯 **الميزات المتاحة في التطبيق**
|
127 |
+
|
128 |
+
### **الأساسية:**
|
129 |
+
- 🔐 تس��يل دخول آمن (رقم هاتف + PIN)
|
130 |
+
- 💳 عرض 6 محافظ يمنية
|
131 |
+
- 🏠 واجهة رئيسية موحدة
|
132 |
+
- 🌙 دعم الوضع الليلي
|
133 |
+
|
134 |
+
### **التقنية:**
|
135 |
+
- 📱 واجهة أصلية للأندرويد
|
136 |
+
- 🔄 عمل بدون إنترنت
|
137 |
+
- 💾 حفظ البيانات محلياً
|
138 |
+
- 🎨 رسوم متحركة سلسة
|
139 |
+
|
140 |
+
### **الأمان:**
|
141 |
+
- 🛡️ تشفير البيانات
|
142 |
+
- 🔒 حماية PIN
|
143 |
+
- ⏰ انتهاء الجلسات
|
144 |
+
- 🚫 حماية من المحاولات المتكررة
|
145 |
+
|
146 |
+
---
|
147 |
+
|
148 |
+
## 🔮 **الميزات المستقبلية (جاهزة للتطوير)**
|
149 |
+
|
150 |
+
### **قراءة SMS:**
|
151 |
+
- 📨 استخراج أرصدة المحافظ من الرسائل
|
152 |
+
- 🔄 تحديث الأرصدة تلقائياً
|
153 |
+
- 📊 تحليل أنماط الإنفاق
|
154 |
+
|
155 |
+
### **المصادقة البيومترية:**
|
156 |
+
- 👆 بصمة الإصبع
|
157 |
+
- 👁️ التعرف على الوجه
|
158 |
+
- 🗣️ التعرف على الصوت
|
159 |
+
|
160 |
+
### **الإشعارات الذكية:**
|
161 |
+
- 💰 تنبيهات الرصيد المنخفض
|
162 |
+
- 💸 تنبيهات الإنفاق الزائد
|
163 |
+
- 📅 تذكير دفع الفواتير
|
164 |
+
|
165 |
+
---
|
166 |
+
|
167 |
+
## 💰 **نموذج الربح المقترح**
|
168 |
+
|
169 |
+
### **الإعلانات:**
|
170 |
+
- 📺 إعلانات مستهدفة بناءً على أنماط الإنفاق
|
171 |
+
- 🏪 شراكات مع المتاجر والخدمات
|
172 |
+
- 💳 عروض خاصة للمحافظ
|
173 |
+
|
174 |
+
### **الاشتراكات:**
|
175 |
+
- 🆓 **مجاني:** الميزات الأساسية
|
176 |
+
- 💎 **مميز:** تحليل متقدم + بدون إعلانات
|
177 |
+
- 🏢 **تجاري:** ميزات للشركات
|
178 |
+
|
179 |
+
### **العمولات:**
|
180 |
+
- 🤝 شراكات مع مقدمي الخدمات
|
181 |
+
- 🎁 كاش باك من المشتريات
|
182 |
+
- 💰 عمولات التحويلات
|
183 |
+
|
184 |
+
---
|
185 |
+
|
186 |
+
## 📈 **التوقعات**
|
187 |
+
|
188 |
+
### **السنة الأولى:**
|
189 |
+
- 👥 **10,000 مستخدم** نشط
|
190 |
+
- 💰 **400,000 ر.ي** دخل متوقع
|
191 |
+
- 📱 **50,000 تحميل** من Google Play
|
192 |
+
|
193 |
+
### **خطة النمو:**
|
194 |
+
1. **الشهر 1-3:** إطلاق وتسويق أولي
|
195 |
+
2. **الشهر 4-6:** إضافة ميزات SMS
|
196 |
+
3. **الشهر 7-9:** تطوير الذكاء الاصطناعي
|
197 |
+
4. **الشهر 10-12:** توسع إقليمي
|
198 |
+
|
199 |
+
---
|
200 |
+
|
201 |
+
## 🎊 **الخلاصة**
|
202 |
+
|
203 |
+
### **تم إنجازه:**
|
204 |
+
✅ **تطبيق ويب** محول إلى **تطبيق أندرويد**
|
205 |
+
✅ **جميع الملفات** جاهزة للبناء
|
206 |
+
✅ **التعليمات** مفصلة وواضحة
|
207 |
+
✅ **الأذونات** معدة للميزات المستقبلية
|
208 |
+
|
209 |
+
### **الخطوة التالية:**
|
210 |
+
🔨 **بناء APK** باستخدام إحدى الطرق المذكورة
|
211 |
+
|
212 |
+
### **النتيجة المتوقعة:**
|
213 |
+
📱 **تطبيق أندرويد** جاهز للتثبيت والاستخدام
|
214 |
+
|
215 |
+
---
|
216 |
+
|
217 |
+
**🎉 مبروك! مشروعك جاهز للانطلاق إلى العالم!**
|
218 |
+
|
219 |
+
*تطبيق محفظتي الموحدة - المدى للخدمات البرمجية التسويقية والإعلانية*
|
QUICK_APK_BUILD.bat
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@echo off
|
2 |
+
echo ========================================
|
3 |
+
echo محفظتي الموحدة - بناء APK سريع
|
4 |
+
echo ========================================
|
5 |
+
echo.
|
6 |
+
|
7 |
+
echo [1/5] التحقق من Java...
|
8 |
+
java -version
|
9 |
+
if %errorlevel% neq 0 (
|
10 |
+
echo ❌ Java غير مثبت! يرجى تثبيت Java JDK 11+
|
11 |
+
pause
|
12 |
+
exit /b 1
|
13 |
+
)
|
14 |
+
echo ✅ Java متوفر
|
15 |
+
|
16 |
+
echo.
|
17 |
+
echo [2/5] التحقق من Android SDK...
|
18 |
+
if not exist "%ANDROID_HOME%" (
|
19 |
+
echo ❌ Android SDK غير مثبت!
|
20 |
+
echo يرجى تثبيت Android Studio أو SDK Tools
|
21 |
+
echo أو تعيين متغير ANDROID_HOME
|
22 |
+
pause
|
23 |
+
exit /b 1
|
24 |
+
)
|
25 |
+
echo ✅ Android SDK متوفر
|
26 |
+
|
27 |
+
echo.
|
28 |
+
echo [3/5] نسخ ملفات التطبيق...
|
29 |
+
if not exist "www" mkdir www
|
30 |
+
copy index.html www\ >nul 2>&1
|
31 |
+
copy styles.css www\ >nul 2>&1
|
32 |
+
copy app.js www\ >nul 2>&1
|
33 |
+
copy auth.js www\ >nul 2>&1
|
34 |
+
copy wallets.js www\ >nul 2>&1
|
35 |
+
copy notifications.js www\ >nul 2>&1
|
36 |
+
copy demo.html www\ >nul 2>&1
|
37 |
+
copy src\manifest.json www\ >nul 2>&1
|
38 |
+
echo ✅ تم نسخ الملفات
|
39 |
+
|
40 |
+
echo.
|
41 |
+
echo [4/5] مزامنة Capacitor...
|
42 |
+
call cap sync android
|
43 |
+
if %errorlevel% neq 0 (
|
44 |
+
echo ❌ فشل في مزامنة Capacitor
|
45 |
+
pause
|
46 |
+
exit /b 1
|
47 |
+
)
|
48 |
+
echo ✅ تم مزامنة Capacitor
|
49 |
+
|
50 |
+
echo.
|
51 |
+
echo [5/5] بناء APK...
|
52 |
+
cd android
|
53 |
+
call gradlew assembleDebug
|
54 |
+
if %errorlevel% neq 0 (
|
55 |
+
echo ❌ فشل في بناء APK
|
56 |
+
echo يرجى مراجعة الأخطاء أعلاه
|
57 |
+
pause
|
58 |
+
exit /b 1
|
59 |
+
)
|
60 |
+
|
61 |
+
echo.
|
62 |
+
echo ========================================
|
63 |
+
echo 🎉 تم بناء APK بنجاح!
|
64 |
+
echo ========================================
|
65 |
+
echo.
|
66 |
+
echo 📱 ملف APK متوفر في:
|
67 |
+
echo android\app\build\outputs\apk\debug\app-debug.apk
|
68 |
+
echo.
|
69 |
+
echo 📋 الخطوات التالية:
|
70 |
+
echo 1. نسخ APK إلى الهاتف
|
71 |
+
echo 2. تفعيل "مصادر غير معروفة" في الإعدادات
|
72 |
+
echo 3. تثبيت APK
|
73 |
+
echo 4. اختبار التطبيق
|
74 |
+
echo.
|
75 |
+
echo 🔑 بيانات التجربة:
|
76 |
+
echo رقم الهاتف: 777123456
|
77 |
+
echo رمز PIN: 1234
|
78 |
+
echo.
|
79 |
+
pause
|
QUICK_START.md
ADDED
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# البدء السريع - محفظتي الموحدة
|
2 |
+
|
3 |
+
## 🚀 تشغيل التطبيق في 5 دقائق
|
4 |
+
|
5 |
+
### المتطلبات الأساسية
|
6 |
+
- Node.js (الإصدار 18+)
|
7 |
+
- npm أو yarn
|
8 |
+
|
9 |
+
### خطوات سريعة
|
10 |
+
|
11 |
+
#### 1. تثبيت الأدوات
|
12 |
+
```bash
|
13 |
+
npm install -g @ionic/cli @angular/cli @capacitor/cli
|
14 |
+
```
|
15 |
+
|
16 |
+
#### 2. إعداد المشروع
|
17 |
+
```bash
|
18 |
+
cd almada
|
19 |
+
npm install
|
20 |
+
```
|
21 |
+
|
22 |
+
#### 3. تشغيل التطبيق
|
23 |
+
```bash
|
24 |
+
ionic serve
|
25 |
+
```
|
26 |
+
|
27 |
+
#### 4. فتح المتصفح
|
28 |
+
انتقل إلى: http://localhost:8100
|
29 |
+
|
30 |
+
### 📱 بناء تطبيق الجوال
|
31 |
+
|
32 |
+
#### للأندرويد:
|
33 |
+
```bash
|
34 |
+
ionic build --prod
|
35 |
+
ionic cap add android
|
36 |
+
ionic cap open android
|
37 |
+
```
|
38 |
+
|
39 |
+
#### لـ iOS:
|
40 |
+
```bash
|
41 |
+
ionic build --prod
|
42 |
+
ionic cap add ios
|
43 |
+
ionic cap open ios
|
44 |
+
```
|
45 |
+
|
46 |
+
## 🔑 بيانات التجربة
|
47 |
+
|
48 |
+
- **رقم الهاتف**: 777123456
|
49 |
+
- **رمز PIN**: 1234
|
50 |
+
|
51 |
+
## 📋 الميزات المتاحة
|
52 |
+
|
53 |
+
✅ تسجيل دخول آمن
|
54 |
+
✅ عرض المحافظ والأرصدة
|
55 |
+
✅ تحويل الأموال
|
56 |
+
✅ إشعارات فورية
|
57 |
+
✅ مصادقة بيومترية
|
58 |
+
✅ واجهة عربية كاملة
|
59 |
+
|
60 |
+
## 🛠️ استكشاف الأخطاء
|
61 |
+
|
62 |
+
### مشكلة: خطأ في npm install
|
63 |
+
```bash
|
64 |
+
npm cache clean --force
|
65 |
+
rm -rf node_modules
|
66 |
+
npm install
|
67 |
+
```
|
68 |
+
|
69 |
+
### مشكلة: خطأ في ionic serve
|
70 |
+
```bash
|
71 |
+
ionic repair
|
72 |
+
ionic serve --verbose
|
73 |
+
```
|
74 |
+
|
75 |
+
### مشكلة: خطأ في Capacitor
|
76 |
+
```bash
|
77 |
+
npx cap sync
|
78 |
+
npx cap doctor
|
79 |
+
```
|
80 |
+
|
81 |
+
## 📚 روابط مفيدة
|
82 |
+
|
83 |
+
- [تعليمات البناء الكاملة](BUILD_INSTRUCTIONS.md)
|
84 |
+
- [وثائق Ionic](https://ionicframework.com/docs)
|
85 |
+
- [وثائق Angular](https://angular.io/docs)
|
86 |
+
- [وثائق Capacitor](https://capacitorjs.com/docs)
|
87 |
+
|
88 |
+
## 💡 نصائح
|
89 |
+
|
90 |
+
1. **للتطوير السريع**: استخدم `ionic serve --lab`
|
91 |
+
2. **لاختبار الجوال**: استخدم `ionic cap run android --livereload`
|
92 |
+
3. **للتصحيح**: افتح Developer Tools في المتصفح
|
93 |
+
4. **للأداء**: استخدم `ionic build --prod` للإنتاج
|
94 |
+
|
95 |
+
## 🎯 الخطوات التالية
|
96 |
+
|
97 |
+
1. جرب تسجيل الدخول
|
98 |
+
2. استكشف المحافظ المختلفة
|
99 |
+
3. جرب التحويلات
|
100 |
+
4. اختبر الإشعارات
|
101 |
+
5. جرب المصادقة البيومترية (في التطبيق الأصلي)
|
102 |
+
|
103 |
+
---
|
104 |
+
|
105 |
+
**🎉 مبروك! تطبيقك جاهز للاستخدام**
|
106 |
+
|
107 |
+
للمساعدة أو الاستفسارات، راجع الوثائق الكاملة أو تواصل مع فريق التطوير.
|
README.md
CHANGED
@@ -1,28 +1,262 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
|
22 |
-
#
|
|
|
23 |
|
24 |
-
|
|
|
|
|
25 |
|
26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
|
28 |
-
|
|
|
1 |
+
# محفظتي الموحدة - تطبيق المحافظ الإلكترونية اليمنية الموحد
|
2 |
+
|
3 |
+
## نظرة عامة
|
4 |
+
|
5 |
+
**محفظتي الموحدة** هو تطبيق جوال متطور مبني بتقنية Ionic/Angular يجمع جميع المحافظ الإلكترونية اليمنية في واجهة موحدة، مما يتيح للمستخدمين إدارة جميع محافظهم من مكان واحد باستخدام رقم هاتف موحد.
|
6 |
+
|
7 |
+
## 📱 **التطبيق متاح الآن كـ:**
|
8 |
+
- **تطبيق أندرويد** (APK)
|
9 |
+
- **تطبيق iOS** (IPA)
|
10 |
+
- **تطبيق ويب** (PWA)
|
11 |
+
- **تطبيق سطح المكتب** (عبر Electron)
|
12 |
+
|
13 |
+
## المحافظ المدعومة
|
14 |
+
|
15 |
+
التطبيق يدعم المحافظ الإلكترونية اليمنية التالية:
|
16 |
+
|
17 |
+
1. **جوالي (Jawali)** - من WeCash YE
|
18 |
+
2. **ONE Cash** - المحفظة الرقمية الأولى في اليمن
|
19 |
+
3. **Cash** - من Tamkeen Financial
|
20 |
+
4. **Jaib Digital Wallet** - من AHD Financial
|
21 |
+
5. **mFloos** - من Alkuraimi Islamic Microfinance Bank
|
22 |
+
6. **Mobile Money Wallet** - من CAC Bank
|
23 |
+
|
24 |
+
## الميزات الرئيسية
|
25 |
+
|
26 |
+
### 🔐 نظام أمان متقدم
|
27 |
+
- تسجيل دخول برقم الهاتف ورمز PIN
|
28 |
+
- مصادقة بيومترية (بصمة الإصبع)
|
29 |
+
- حماية من المحاولات المتكررة
|
30 |
+
- جلسات آمنة مع انتهاء صلاحية تلقائي
|
31 |
+
|
32 |
+
### 💸 إدارة المحافظ
|
33 |
+
- عرض جميع المحافظ في واجهة موحدة
|
34 |
+
- عرض الأرصدة الإجمالية والفردية
|
35 |
+
- تحديث الأرصدة في الوقت الفعلي
|
36 |
+
- إخفاء/إظهار الأرصدة للخصوصية
|
37 |
+
|
38 |
+
### 🔄 التحويلات والمدفوعات
|
39 |
+
- تحويل الأموال بين المحافظ المختلفة
|
40 |
+
- دفع الفواتير (كهرباء، مياه، إنترنت)
|
41 |
+
- شحن أرصدة الهواتف
|
42 |
+
- مسح رموز QR للدفع
|
43 |
+
|
44 |
+
### 📱 واجهة مستخدم عصرية
|
45 |
+
- تصميم متجاوب يعمل على جميع الأجهزة
|
46 |
+
- واجهة باللغة العربية مع دعم RTL
|
47 |
+
- رسوم متحركة سلسة
|
48 |
+
- تجربة مستخدم بديهية
|
49 |
+
|
50 |
+
### 🔔 نظام إشعارات متطور
|
51 |
+
- إشعارات فورية للمعاملات
|
52 |
+
- تنبيهات أمنية
|
53 |
+
- إشعارات النظام
|
54 |
+
- إدارة الإشعارات المقروءة وغير المقروءة
|
55 |
+
|
56 |
+
## التقنيات المستخدمة
|
57 |
+
|
58 |
+
### Frontend Framework
|
59 |
+
- **Ionic 7** - إطار عمل التطبيقات الهجينة
|
60 |
+
- **Angular 17** - إطار عمل الواجهة الأمامية
|
61 |
+
- **TypeScript** - لغة البرمجة الأساسية
|
62 |
+
- **SCSS** - معالج CSS المتقدم
|
63 |
+
|
64 |
+
### Mobile Development
|
65 |
+
- **Capacitor 5** - منصة التطبيقات الأصلية
|
66 |
+
- **Cordova Plugins** - الوصول لميزات الجهاز
|
67 |
+
- **PWA** - تطبيق ويب تقدمي
|
68 |
+
|
69 |
+
### Backend & Storage
|
70 |
+
- **Ionic Storage** - تخزين البيانات المحلية
|
71 |
+
- **RxJS** - إدارة البيانات التفاعلية
|
72 |
+
- **HTTP Client** - التواصل مع APIs
|
73 |
+
|
74 |
+
### UI/UX
|
75 |
+
- **Ionic Components** - مكونات واجهة المستخدم
|
76 |
+
- **Ionicons** - مكتبة الأيقونات
|
77 |
+
- **Google Fonts** - خط Tajawal العربي
|
78 |
+
- **CSS Animations** - الرسوم المتحركة
|
79 |
+
|
80 |
+
### Development Tools
|
81 |
+
- **Angular CLI** - أدوات التطوير
|
82 |
+
- **Capacitor CLI** - أدوات البناء للجوال
|
83 |
+
- **ESLint** - فحص جودة الكود
|
84 |
+
- **Prettier** - تنسيق الكود
|
85 |
+
|
86 |
+
## هيكل المشروع
|
87 |
+
|
88 |
+
```
|
89 |
+
almada/
|
90 |
+
├── src/ # مجلد المصدر الرئيسي
|
91 |
+
│ ├── app/ # تطبيق Angular
|
92 |
+
│ │ ├── pages/ # صفحات التطبيق
|
93 |
+
│ │ │ ├── login/ # صفحة تسجيل الدخول
|
94 |
+
│ │ │ ├── home/ # الصفحة الرئيسية
|
95 |
+
│ │ │ ├── wallets/ # صفحة المحافظ
|
96 |
+
│ │ │ ├── transfer/ # صفحة التحويلات
|
97 |
+
│ │ │ └── ... # باقي الصفحات
|
98 |
+
│ │ ├── services/ # الخدمات
|
99 |
+
│ │ │ ├── auth.service.ts # خدمة المصادقة
|
100 |
+
│ │ │ ├── wallet.service.ts # خدمة المحافظ
|
101 |
+
│ │ │ └── ... # باقي الخدمات
|
102 |
+
│ │ ├── guards/ # حراس الحما��ة
|
103 |
+
│ │ └── components/ # المكونات المشتركة
|
104 |
+
│ ├── assets/ # الملفات الثابتة
|
105 |
+
│ ├── theme/ # ملفات الثيم
|
106 |
+
│ └── environments/ # إعدادات البيئة
|
107 |
+
├── android/ # مشروع الأندرويد
|
108 |
+
├── ios/ # مشروع iOS
|
109 |
+
├── capacitor.config.ts # إعدادات Capacitor
|
110 |
+
├── ionic.config.json # إعدادات Ionic
|
111 |
+
├── angular.json # إعدادات Angular
|
112 |
+
├── package.json # تبعيات المشروع
|
113 |
+
├── BUILD_INSTRUCTIONS.md # تعليمات البناء
|
114 |
+
└── README.md # هذا الملف
|
115 |
+
```
|
116 |
+
|
117 |
+
## كيفية التشغيل
|
118 |
+
|
119 |
+
### 1. تشغيل للتطوير
|
120 |
+
|
121 |
+
```bash
|
122 |
+
# استنساخ المشروع
|
123 |
+
git clone [repository-url]
|
124 |
+
cd almada
|
125 |
+
|
126 |
+
# تثبيت التبعيات
|
127 |
+
npm install
|
128 |
+
|
129 |
+
# تشغيل خادم التطوير
|
130 |
+
ionic serve
|
131 |
+
|
132 |
+
# فتح المتصفح على
|
133 |
+
http://localhost:8100
|
134 |
+
```
|
135 |
+
|
136 |
+
### 2. بناء التطبيق للجوال
|
137 |
+
|
138 |
+
```bash
|
139 |
+
# بناء المشروع
|
140 |
+
ionic build --prod
|
141 |
+
|
142 |
+
# إضافة منصة الأندرويد
|
143 |
+
ionic cap add android
|
144 |
+
|
145 |
+
# إضافة منصة iOS
|
146 |
+
ionic cap add ios
|
147 |
|
148 |
+
# بناء APK للأندرويد
|
149 |
+
ionic cap build android
|
150 |
|
151 |
+
# بناء IPA لـ iOS
|
152 |
+
ionic cap build ios
|
153 |
+
```
|
154 |
|
155 |
+
### 3. تشغيل على الأجهزة
|
156 |
+
|
157 |
+
```bash
|
158 |
+
# تشغيل على الأندرويد
|
159 |
+
ionic cap run android
|
160 |
+
|
161 |
+
# تشغيل على iOS
|
162 |
+
ionic cap run ios
|
163 |
+
|
164 |
+
# تشغيل في المتصفح مع إعادة التحميل
|
165 |
+
ionic serve --lab
|
166 |
+
```
|
167 |
+
|
168 |
+
راجع ملف [BUILD_INSTRUCTIONS.md](BUILD_INSTRUCTIONS.md) للتفاصيل الكاملة.
|
169 |
+
|
170 |
+
## بيانات التجربة
|
171 |
+
|
172 |
+
للاختبار، يمكن استخدام البيانات التالية:
|
173 |
+
|
174 |
+
- **رقم الهاتف**: أي رقم يمني صحيح (9 أرقام)
|
175 |
+
- **رمز PIN**: أي رمز من 4-6 أرقام
|
176 |
+
- **مثال**: 777123456 / 1234
|
177 |
+
|
178 |
+
## الاستخدام
|
179 |
+
|
180 |
+
### تسجيل الدخول
|
181 |
+
1. أدخل رقم الهاتف (9 أرقام)
|
182 |
+
2. أدخل رمز PIN (4-6 أرقام)
|
183 |
+
3. أو استخدم المصادقة البيومترية
|
184 |
+
|
185 |
+
### إدارة المحافظ
|
186 |
+
- عرض جميع المحافظ والأرصدة
|
187 |
+
- تحديث الأرصدة
|
188 |
+
- إخفاء/إظهار الأرصدة
|
189 |
+
|
190 |
+
### التحويلات
|
191 |
+
1. اختر المحفظة المرسلة
|
192 |
+
2. أدخل تفاصيل التحويل
|
193 |
+
3. أكد بـ PIN
|
194 |
+
|
195 |
+
### الإشعارات
|
196 |
+
- عرض الإشعارات من الأيقونة في الأعلى
|
197 |
+
- وضع علامة مقروء
|
198 |
+
- حذف الإشعارات
|
199 |
+
|
200 |
+
## الأمان
|
201 |
+
|
202 |
+
التطبيق يتضمن عدة طبقات أمان:
|
203 |
+
|
204 |
+
- **تشفير البيانات**: جميع البيانات الحساسة مشفرة
|
205 |
+
- **جلسات آمنة**: انتهاء صلاحية تلقائي للجلسات
|
206 |
+
- **حماية من الهجمات**: حماية من المحاولات المتكررة
|
207 |
+
- **مصادقة متعددة**: PIN + بصمة
|
208 |
+
- **تخزين آمن**: استخدام Local Storage بشكل آمن
|
209 |
+
|
210 |
+
## ملاحظات مهمة
|
211 |
+
|
212 |
+
⚠️ **هذا تطبيق تجريبي لأغراض العرض فقط**
|
213 |
+
|
214 |
+
- جميع البيانات والمعاملات محاكاة
|
215 |
+
- لا يؤثر على الحسابات الحقيقية
|
216 |
+
- البيانات محفوظة محلياً في المتصفح
|
217 |
+
- يتطلب متصفح حديث للمصادقة البيومترية
|
218 |
+
|
219 |
+
## التطوير المستقبلي
|
220 |
+
|
221 |
+
### الميزات المخطط لها
|
222 |
+
- [ ] دعم المزيد من المحافظ
|
223 |
+
- [ ] تطبيق جوال أصلي
|
224 |
+
- [ ] تكامل مع APIs الحقيقية
|
225 |
+
- [ ] نظام إحصائيات متقدم
|
226 |
+
- [ ] دعم العملات المتعددة
|
227 |
+
- [ ] نظام النسخ الاحتياطي
|
228 |
+
|
229 |
+
### التحسينات التقنية
|
230 |
+
- [ ] PWA (Progressive Web App)
|
231 |
+
- [ ] وضع عدم الاتصال
|
232 |
+
- [ ] تحسين الأداء
|
233 |
+
- [ ] اختبارات تلقائية
|
234 |
+
- [ ] CI/CD Pipeline
|
235 |
+
|
236 |
+
## المساهمة
|
237 |
+
|
238 |
+
نرحب بالمساهمات! يرجى:
|
239 |
+
|
240 |
+
1. Fork المشروع
|
241 |
+
2. إنشاء branch للميزة الجديدة
|
242 |
+
3. Commit التغييرات
|
243 |
+
4. Push إلى Branch
|
244 |
+
5. فتح Pull Request
|
245 |
+
|
246 |
+
## الترخيص
|
247 |
+
|
248 |
+
هذا المشروع مرخص تحت رخصة MIT - انظر ملف [LICENSE](LICENSE) للتفاصيل.
|
249 |
+
|
250 |
+
## التواصل
|
251 |
+
|
252 |
+
**المدى للخدمات البرمجية التسويقية والإعلانية**
|
253 |
+
- المدير العام: المهندس/ محمد المرتضى
|
254 |
+
- © 2025 جميع الحقوق محفوظة
|
255 |
+
|
256 |
+
## الدعم
|
257 |
+
|
258 |
+
للدعم التقني أو الاستفسارات، يرجى فتح issue في الم��تودع أو التواصل مع فريق التطوير.
|
259 |
+
|
260 |
+
---
|
261 |
|
262 |
+
**شكراً لاستخدام محفظتي الموحدة! 🚀**
|
README_GITHUB.md
ADDED
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 📱 محفظتي الموحدة - تطبيق المحافظ الإلكترونية اليمنية
|
2 |
+
|
3 |
+
[](https://github.com/USERNAME/almada-unified-wallet/actions/workflows/build-apk.yml)
|
4 |
+
[](https://github.com/USERNAME/almada-unified-wallet/releases/latest)
|
5 |
+
|
6 |
+
## 🎯 **نظرة عامة**
|
7 |
+
|
8 |
+
**محفظتي الموحدة** هو تطبيق أندرويد متطور يجمع جميع المحافظ الإلكترونية اليمنية في واجهة موحدة، مما يتيح للمستخدمين إدارة جميع محافظهم من مكان واحد.
|
9 |
+
|
10 |
+
## 📱 **تحميل التطبيق**
|
11 |
+
|
12 |
+
### 🚀 **أحدث إصدار:**
|
13 |
+
[](https://github.com/USERNAME/almada-unified-wallet/releases/latest/download/app-debug.apk)
|
14 |
+
|
15 |
+
### 🔄 **البناء التلقائي:**
|
16 |
+
- يتم بناء APK تلقائياً عند كل تحديث
|
17 |
+
- متوفر في قسم [Actions](https://github.com/USERNAME/almada-unified-wallet/actions)
|
18 |
+
- تحميل مباشر من [Releases](https://github.com/USERNAME/almada-unified-wallet/releases)
|
19 |
+
|
20 |
+
## 🎯 **الميزات**
|
21 |
+
|
22 |
+
### ✅ **المتاحة حالياً:**
|
23 |
+
- 🔐 تسجيل دخول آمن برقم الهاتف + PIN
|
24 |
+
- 💳 عرض 6 محافظ يمنية رئيسية:
|
25 |
+
- جوالي (Jawali)
|
26 |
+
- ONE Cash
|
27 |
+
- Cash
|
28 |
+
- Aman
|
29 |
+
- Tadawul
|
30 |
+
- Al-Kuraimi
|
31 |
+
- 🏠 واجهة موحدة لجميع المحافظ
|
32 |
+
- 🌙 دعم الوضع الليلي
|
33 |
+
- 🔄 واجهة عربية كاملة (RTL)
|
34 |
+
- 📱 تصميم متجاوب لجميع أحجام الشاشات
|
35 |
+
|
36 |
+
### 🔄 **قيد التطوير:**
|
37 |
+
- 📨 قراءة رسائل SMS لاستخراج الأرصدة
|
38 |
+
- 👆 مصادقة بيومترية (بصمة/وجه)
|
39 |
+
- 🔔 إشعارات ذكية ومخصصة
|
40 |
+
- 📊 تحليل أنماط الإنفاق والتوفير
|
41 |
+
- 💰 اقتراحات التوفير الذكية
|
42 |
+
|
43 |
+
## 🔑 **بيانات التجربة**
|
44 |
+
|
45 |
+
للدخول إلى التطبيق:
|
46 |
+
- **رقم الهاتف:** `777123456`
|
47 |
+
- **رمز PIN:** `1234`
|
48 |
+
|
49 |
+
## 🛠️ **التقنيات المستخدمة**
|
50 |
+
|
51 |
+
- **Frontend:** HTML5, CSS3, JavaScript (ES6+)
|
52 |
+
- **Mobile Framework:** Capacitor 5
|
53 |
+
- **Build System:** Gradle
|
54 |
+
- **CI/CD:** GitHub Actions
|
55 |
+
- **Platform:** Android (API 24+)
|
56 |
+
|
57 |
+
## 📲 **التثبيت**
|
58 |
+
|
59 |
+
### **من GitHub Releases:**
|
60 |
+
1. اذهب إلى [Releases](https://github.com/USERNAME/almada-unified-wallet/releases)
|
61 |
+
2. حمل أحدث ملف APK
|
62 |
+
3. في الهاتف: فعل "مصادر غير معروفة"
|
63 |
+
4. ثبت التطبيق
|
64 |
+
|
65 |
+
### **من GitHub Actions:**
|
66 |
+
1. اذهب إلى [Actions](https://github.com/USERNAME/almada-unified-wallet/actions)
|
67 |
+
2. اختر آخر build ناجح
|
68 |
+
3. حمل APK من Artifacts
|
69 |
+
|
70 |
+
## 🏗️ **البناء المحلي**
|
71 |
+
|
72 |
+
```bash
|
73 |
+
# استنساخ المشروع
|
74 |
+
git clone https://github.com/USERNAME/almada-unified-wallet.git
|
75 |
+
cd almada-unified-wallet
|
76 |
+
|
77 |
+
# تثبيت التبعيات
|
78 |
+
npm install
|
79 |
+
|
80 |
+
# بناء للأندرويد
|
81 |
+
npx cap sync android
|
82 |
+
cd android
|
83 |
+
./gradlew assembleDebug
|
84 |
+
```
|
85 |
+
|
86 |
+
## 📊 **معلومات التطبيق**
|
87 |
+
|
88 |
+
| المعلومة | القيمة |
|
89 |
+
|---------|--------|
|
90 |
+
| **اسم التطبيق** | محفظتي الموحدة |
|
91 |
+
| **Package ID** | com.almada.unifiedwallet |
|
92 |
+
| **الإصدار** | 1.0.0 |
|
93 |
+
| **حجم APK** | ~15-20 MB |
|
94 |
+
| **الحد الأدنى** | Android 7.0 (API 24) |
|
95 |
+
| **اللغة** | العربية (RTL) |
|
96 |
+
|
97 |
+
## 🤝 **المساهمة**
|
98 |
+
|
99 |
+
نرحب بالمساهمات! يرجى:
|
100 |
+
1. Fork المشروع
|
101 |
+
2. إنشاء branch للميزة الجديدة
|
102 |
+
3. Commit التغييرات
|
103 |
+
4. Push إلى Branch
|
104 |
+
5. فتح Pull Request
|
105 |
+
|
106 |
+
## 📄 **الترخيص**
|
107 |
+
|
108 |
+
هذا المشروع مرخص تحت رخصة MIT - راجع ملف [LICENSE](LICENSE) للتفاصيل.
|
109 |
+
|
110 |
+
## 📞 **التواصل**
|
111 |
+
|
112 |
+
- **الشركة:** المدى للخدمات البرمجية التسويقية والإعلانية
|
113 |
+
- **الموقع:** https://almada.com
|
114 |
+
- **البريد الإلكتروني:** info@almada.com
|
115 |
+
|
116 |
+
## 🎉 **شكر خاص**
|
117 |
+
|
118 |
+
شكر خاص لجميع مطوري المحافظ الإلكترونية اليمنية والمجتمع التقني اليمني.
|
119 |
+
|
120 |
+
---
|
121 |
+
|
122 |
+
**🚀 مبروك! تطبيقك متاح للعالم!**
|
123 |
+
|
124 |
+
*Made with ❤️ in Yemen*
|
STEP_BY_STEP_APK.md
ADDED
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 📱 دليل بناء APK خطوة بخطوة - مصور
|
2 |
+
|
3 |
+
## 🎯 **الهدف:** الحصول على ملف APK جاهز للتثبيت في 15 دقيقة
|
4 |
+
|
5 |
+
---
|
6 |
+
|
7 |
+
## 🚀 **الطريقة الأسرع: ملف BAT التلقائي**
|
8 |
+
|
9 |
+
### **الخطوة 1: تشغيل الملف التلقائي**
|
10 |
+
1. **اذهب إلى مجلد المشروع:** `E:\almada`
|
11 |
+
2. **اضغط مرتين على:** `BUILD_APK_SIMPLE.bat`
|
12 |
+
3. **انتظر اكتمال العملية** (5-10 دقائق)
|
13 |
+
|
14 |
+
### **إذا نجحت العملية:**
|
15 |
+
- ✅ ستفتح نافذة تحتوي على ملف APK
|
16 |
+
- ✅ الملف سيكون: `app-debug.apk`
|
17 |
+
- ✅ انسخه إلى هاتفك وثبته
|
18 |
+
|
19 |
+
### **إذا فشلت العملية:**
|
20 |
+
- ❌ انتقل للطريقة الثانية أدناه
|
21 |
+
|
22 |
+
---
|
23 |
+
|
24 |
+
## 🏗️ **الطريقة الثانية: Android Studio (مضمونة 100%)**
|
25 |
+
|
26 |
+
### **الخطوة 1: فتح Android Studio**
|
27 |
+
```
|
28 |
+
1. افتح Android Studio
|
29 |
+
2. اختر "Open an Existing Project"
|
30 |
+
3. انتقل إلى: E:\almada\android
|
31 |
+
4. اختر مجلد android واضغط OK
|
32 |
+
```
|
33 |
+
|
34 |
+
### **الخطوة 2: انتظار التحميل**
|
35 |
+
```
|
36 |
+
⏳ انتظر رسالة: "Gradle sync finished"
|
37 |
+
📦 قد يستغرق 5-10 دقائق في المرة الأولى
|
38 |
+
🌐 تأكد من اتصال الإنترنت
|
39 |
+
```
|
40 |
+
|
41 |
+
### **الخطوة 3: بناء APK**
|
42 |
+
```
|
43 |
+
1. في القائمة العلوية: Build
|
44 |
+
2. اختر: Build Bundle(s) / APK(s)
|
45 |
+
3. اختر: Build APK(s)
|
46 |
+
4. انتظر رسالة: "APK(s) generated successfully"
|
47 |
+
```
|
48 |
+
|
49 |
+
### **الخطوة 4: العثور على APK**
|
50 |
+
```
|
51 |
+
📂 المسار: E:\almada\android\app\build\outputs\apk\debug\
|
52 |
+
📱 الملف: app-debug.apk
|
53 |
+
💾 الحجم: ~15-20 MB
|
54 |
+
```
|
55 |
+
|
56 |
+
---
|
57 |
+
|
58 |
+
## 📲 **تثبيت التطبيق على الهاتف**
|
59 |
+
|
60 |
+
### **الطريقة 1: نسخ مباشر**
|
61 |
+
1. **انسخ ملف APK** إلى هاتفك (عبر USB أو البلوتوث)
|
62 |
+
2. **في الهاتف:** Settings > Security > Install from Unknown Sources ✅
|
63 |
+
3. **اضغط على ملف APK** واختر Install
|
64 |
+
4. **انتظر اكتمال التثبيت**
|
65 |
+
|
66 |
+
### **الطريقة 2: ADB (للمتقدمين)**
|
67 |
+
```bash
|
68 |
+
# وصل الهاتف بـ USB وفعل USB Debugging
|
69 |
+
adb install app-debug.apk
|
70 |
+
```
|
71 |
+
|
72 |
+
---
|
73 |
+
|
74 |
+
## 🔑 **اختبار التطبيق**
|
75 |
+
|
76 |
+
بعد التثبيت:
|
77 |
+
1. **افتح التطبيق:** "محفظتي الموحدة"
|
78 |
+
2. **أدخل البيانات:**
|
79 |
+
- رقم الهاتف: `777123456`
|
80 |
+
- رمز PIN: `1234`
|
81 |
+
3. **استكشف الميزات:**
|
82 |
+
- عرض المحافظ
|
83 |
+
- التنقل بين الصفحات
|
84 |
+
- اختبار الواجهة العربية
|
85 |
+
|
86 |
+
---
|
87 |
+
|
88 |
+
## 🆘 **حل المشاكل الشائعة**
|
89 |
+
|
90 |
+
### **مشكلة: "App not installed"**
|
91 |
+
```
|
92 |
+
الحل:
|
93 |
+
1. تأكد من تفعيل "Install from Unknown Sources"
|
94 |
+
2. احذف أي إصدار قديم من التطبيق
|
95 |
+
3. أعد تشغيل الهاتف وحاول مرة أخرى
|
96 |
+
```
|
97 |
+
|
98 |
+
### **مشكلة: "Parse error"**
|
99 |
+
```
|
100 |
+
الحل:
|
101 |
+
1. تأكد من أن ملف APK غير تالف
|
102 |
+
2. أعد تحميل/نسخ الملف
|
103 |
+
3. تأكد من توافق إصدار الأندرويد (7.0+)
|
104 |
+
```
|
105 |
+
|
106 |
+
### **مشكلة: "Gradle build failed"**
|
107 |
+
```
|
108 |
+
الحل:
|
109 |
+
1. تأكد من اتصال الإنترنت
|
110 |
+
2. في Android Studio: Build > Clean Project
|
111 |
+
3. ثم: Build > Rebuild Project
|
112 |
+
```
|
113 |
+
|
114 |
+
---
|
115 |
+
|
116 |
+
## 📊 **معلومات التطبيق النهائي**
|
117 |
+
|
118 |
+
| المعلومة | القيمة |
|
119 |
+
|---------|--------|
|
120 |
+
| **اسم التطبيق** | محفظتي الموحدة |
|
121 |
+
| **Package Name** | com.almada.unifiedwallet |
|
122 |
+
| **الإصدار** | 1.0.0 |
|
123 |
+
| **حجم APK** | ~15-20 MB |
|
124 |
+
| **الحد الأدنى** | Android 7.0 (API 24) |
|
125 |
+
| **اللغة** | العربية (RTL) |
|
126 |
+
| **النوع** | Debug APK |
|
127 |
+
|
128 |
+
---
|
129 |
+
|
130 |
+
## 🎯 **الميزات المتاحة**
|
131 |
+
|
132 |
+
### **الحالية:**
|
133 |
+
- ✅ تسجيل دخول آمن
|
134 |
+
- ✅ عرض 6 محافظ يمنية
|
135 |
+
- ✅ واجهة عربية كاملة
|
136 |
+
- ✅ تصميم متجاوب
|
137 |
+
- ✅ حفظ البيانات محلياً
|
138 |
+
|
139 |
+
### **المستقبلية:**
|
140 |
+
- 🔄 قراءة رسائل SMS
|
141 |
+
- 🔄 مصادقة بيومترية
|
142 |
+
- 🔄 إشعارات ذكية
|
143 |
+
- 🔄 تحليل الإنفاق
|
144 |
+
|
145 |
+
---
|
146 |
+
|
147 |
+
## 💡 **نصائح مهمة**
|
148 |
+
|
149 |
+
### **للبناء الناجح:**
|
150 |
+
- 🌐 تأكد من **اتصال الإنترنت** القوي
|
151 |
+
- ⏰ **لا تقاطع** عملية التحميل
|
152 |
+
- 💾 تأكد من **مساحة كافية** (5+ GB)
|
153 |
+
|
154 |
+
### **للاختبار:**
|
155 |
+
- 📱 اختبر على **أجهزة مختلفة**
|
156 |
+
- 🔄 اختبر **جميع الميزات**
|
157 |
+
- 📊 راقب **الأداء**
|
158 |
+
|
159 |
+
### **للمشاركة:**
|
160 |
+
- 📤 يمكنك **مشاركة APK** مع الآخرين
|
161 |
+
- 🔒 هذا **إصدار تجريبي** (Debug)
|
162 |
+
- 🏪 للنشر في Google Play تحتاج **إصدار Release**
|
163 |
+
|
164 |
+
---
|
165 |
+
|
166 |
+
## 🎉 **تهانينا!**
|
167 |
+
|
168 |
+
عند اكتمال هذه الخطوات، ستحصل على:
|
169 |
+
- 📱 **تطبيق أندرويد** كامل وجاهز
|
170 |
+
- 💼 **محفظة موحدة** لجميع المحافظ اليمنية
|
171 |
+
- �� **واجهة احترافية** باللغة العربية
|
172 |
+
- 🔒 **نظام أمان** متقدم
|
173 |
+
|
174 |
+
**🚀 مبروك! تطبيقك جاهز للعالم!**
|
175 |
+
|
176 |
+
---
|
177 |
+
|
178 |
+
## 📞 **تحتاج مساعدة؟**
|
179 |
+
|
180 |
+
إذا واجهت أي مشكلة:
|
181 |
+
1. راجع قسم "حل المشاكل" أعلاه
|
182 |
+
2. تأكد من اتباع الخطوات بالترتيب
|
183 |
+
3. تواصل للحصول على المساعدة
|
184 |
+
|
185 |
+
**💪 لا تستسلم - النجاح قريب!**
|
Windows
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
اختر: "Command line tools only"
|
angular.json
ADDED
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
3 |
+
"version": 1,
|
4 |
+
"newProjectRoot": "projects",
|
5 |
+
"projects": {
|
6 |
+
"unified-wallet-app": {
|
7 |
+
"projectType": "application",
|
8 |
+
"schematics": {
|
9 |
+
"@ionic/angular-toolkit:component": {
|
10 |
+
"styleext": "scss"
|
11 |
+
},
|
12 |
+
"@ionic/angular-toolkit:page": {
|
13 |
+
"styleext": "scss"
|
14 |
+
}
|
15 |
+
},
|
16 |
+
"root": "",
|
17 |
+
"sourceRoot": "src",
|
18 |
+
"prefix": "app",
|
19 |
+
"architect": {
|
20 |
+
"build": {
|
21 |
+
"builder": "@angular-devkit/build-angular:browser",
|
22 |
+
"options": {
|
23 |
+
"outputPath": "dist/unified-wallet-app",
|
24 |
+
"index": "src/index.html",
|
25 |
+
"main": "src/main.ts",
|
26 |
+
"polyfills": [
|
27 |
+
"zone.js"
|
28 |
+
],
|
29 |
+
"tsConfig": "tsconfig.app.json",
|
30 |
+
"inlineStyleLanguage": "scss",
|
31 |
+
"assets": [
|
32 |
+
{
|
33 |
+
"glob": "**/*",
|
34 |
+
"input": "src/assets",
|
35 |
+
"output": "assets"
|
36 |
+
},
|
37 |
+
{
|
38 |
+
"glob": "**/*.svg",
|
39 |
+
"input": "node_modules/ionicons/dist/ionicons/svg",
|
40 |
+
"output": "./svg"
|
41 |
+
}
|
42 |
+
],
|
43 |
+
"styles": [
|
44 |
+
"src/theme/variables.scss",
|
45 |
+
"src/global.scss"
|
46 |
+
],
|
47 |
+
"scripts": []
|
48 |
+
},
|
49 |
+
"configurations": {
|
50 |
+
"production": {
|
51 |
+
"budgets": [
|
52 |
+
{
|
53 |
+
"type": "initial",
|
54 |
+
"maximumWarning": "2mb",
|
55 |
+
"maximumError": "5mb"
|
56 |
+
},
|
57 |
+
{
|
58 |
+
"type": "anyComponentStyle",
|
59 |
+
"maximumWarning": "2kb",
|
60 |
+
"maximumError": "4kb"
|
61 |
+
}
|
62 |
+
],
|
63 |
+
"outputHashing": "all"
|
64 |
+
},
|
65 |
+
"development": {
|
66 |
+
"buildOptimizer": false,
|
67 |
+
"optimization": false,
|
68 |
+
"vendorChunk": true,
|
69 |
+
"extractLicenses": false,
|
70 |
+
"sourceMap": true,
|
71 |
+
"namedChunks": true
|
72 |
+
}
|
73 |
+
},
|
74 |
+
"defaultConfiguration": "production"
|
75 |
+
},
|
76 |
+
"serve": {
|
77 |
+
"builder": "@angular-devkit/build-angular:dev-server",
|
78 |
+
"configurations": {
|
79 |
+
"production": {
|
80 |
+
"buildTarget": "unified-wallet-app:build:production"
|
81 |
+
},
|
82 |
+
"development": {
|
83 |
+
"buildTarget": "unified-wallet-app:build:development"
|
84 |
+
}
|
85 |
+
},
|
86 |
+
"defaultConfiguration": "development"
|
87 |
+
},
|
88 |
+
"extract-i18n": {
|
89 |
+
"builder": "@angular-devkit/build-angular:extract-i18n",
|
90 |
+
"options": {
|
91 |
+
"buildTarget": "unified-wallet-app:build"
|
92 |
+
}
|
93 |
+
},
|
94 |
+
"test": {
|
95 |
+
"builder": "@angular-devkit/build-angular:karma",
|
96 |
+
"options": {
|
97 |
+
"polyfills": [
|
98 |
+
"zone.js",
|
99 |
+
"zone.js/testing"
|
100 |
+
],
|
101 |
+
"tsConfig": "tsconfig.spec.json",
|
102 |
+
"inlineStyleLanguage": "scss",
|
103 |
+
"assets": [
|
104 |
+
{
|
105 |
+
"glob": "**/*",
|
106 |
+
"input": "src/assets",
|
107 |
+
"output": "assets"
|
108 |
+
},
|
109 |
+
{
|
110 |
+
"glob": "**/*.svg",
|
111 |
+
"input": "node_modules/ionicons/dist/ionicons/svg",
|
112 |
+
"output": "./svg"
|
113 |
+
}
|
114 |
+
],
|
115 |
+
"styles": [
|
116 |
+
"src/theme/variables.scss",
|
117 |
+
"src/global.scss"
|
118 |
+
],
|
119 |
+
"scripts": []
|
120 |
+
}
|
121 |
+
},
|
122 |
+
"lint": {
|
123 |
+
"builder": "@angular-eslint/builder:lint",
|
124 |
+
"options": {
|
125 |
+
"lintFilePatterns": [
|
126 |
+
"src/**/*.ts",
|
127 |
+
"src/**/*.html"
|
128 |
+
]
|
129 |
+
}
|
130 |
+
}
|
131 |
+
}
|
132 |
+
}
|
133 |
+
},
|
134 |
+
"cli": {
|
135 |
+
"schematicCollections": [
|
136 |
+
"@ionic/angular-toolkit"
|
137 |
+
]
|
138 |
+
},
|
139 |
+
"schematics": {
|
140 |
+
"@ionic/angular-toolkit:component": {
|
141 |
+
"styleext": "scss"
|
142 |
+
},
|
143 |
+
"@ionic/angular-toolkit:page": {
|
144 |
+
"styleext": "scss"
|
145 |
+
}
|
146 |
+
}
|
147 |
+
}
|
app.js
ADDED
@@ -0,0 +1,780 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// التطبيق الرئيسي - محفظتي الموحدة
|
2 |
+
class UnifiedWalletApp {
|
3 |
+
constructor() {
|
4 |
+
this.currentUser = null;
|
5 |
+
this.currentScreen = 'login';
|
6 |
+
this.wallets = [];
|
7 |
+
this.transactions = [];
|
8 |
+
this.isBalanceHidden = false;
|
9 |
+
|
10 |
+
// تهيئة المدراء
|
11 |
+
this.authManager = new AuthenticationManager();
|
12 |
+
this.walletManager = new WalletManager();
|
13 |
+
this.notificationManager = new NotificationManager();
|
14 |
+
|
15 |
+
this.init();
|
16 |
+
}
|
17 |
+
|
18 |
+
// تهيئة التطبيق
|
19 |
+
init() {
|
20 |
+
this.setupEventListeners();
|
21 |
+
this.loadUserData();
|
22 |
+
this.showLoadingScreen();
|
23 |
+
|
24 |
+
// محاكاة تحميل البيانات
|
25 |
+
setTimeout(() => {
|
26 |
+
this.hideLoadingScreen();
|
27 |
+
this.checkAuthStatus();
|
28 |
+
}, 2000);
|
29 |
+
}
|
30 |
+
|
31 |
+
// إعداد مستمعي الأحداث
|
32 |
+
setupEventListeners() {
|
33 |
+
// تسجيل الدخول
|
34 |
+
document.getElementById('login-btn').addEventListener('click', () => this.handleLogin());
|
35 |
+
document.getElementById('biometric-login').addEventListener('click', () => this.handleBiometricLogin());
|
36 |
+
document.getElementById('register-link').addEventListener('click', (e) => {
|
37 |
+
e.preventDefault();
|
38 |
+
this.showRegisterModal();
|
39 |
+
});
|
40 |
+
|
41 |
+
// الشاشة الرئيسية
|
42 |
+
document.getElementById('refresh-balance').addEventListener('click', () => this.refreshBalances());
|
43 |
+
document.getElementById('hide-balance').addEventListener('click', () => this.toggleBalanceVisibility());
|
44 |
+
|
45 |
+
// الإجراءات السريعة
|
46 |
+
document.getElementById('transfer-btn').addEventListener('click', () => this.showTransferScreen());
|
47 |
+
document.getElementById('pay-bills-btn').addEventListener('click', () => this.showBillsScreen());
|
48 |
+
document.getElementById('recharge-btn').addEventListener('click', () => this.showRechargeScreen());
|
49 |
+
document.getElementById('qr-scan-btn').addEventListener('click', () => this.startQRScan());
|
50 |
+
|
51 |
+
// شريط التنقل
|
52 |
+
document.querySelectorAll('.nav-item').forEach(item => {
|
53 |
+
item.addEventListener('click', () => {
|
54 |
+
const screen = item.dataset.screen;
|
55 |
+
this.switchScreen(screen);
|
56 |
+
});
|
57 |
+
});
|
58 |
+
|
59 |
+
// أزرار الإعدادات والإشعارات
|
60 |
+
document.getElementById('settings-btn').addEventListener('click', () => this.showSettings());
|
61 |
+
document.getElementById('notifications-btn').addEventListener('click', () => this.showNotifications());
|
62 |
+
|
63 |
+
// أزرار الإدارة
|
64 |
+
document.getElementById('manage-wallets').addEventListener('click', () => this.showWalletManagement());
|
65 |
+
document.getElementById('view-all-transactions').addEventListener('click', () => this.showAllTransactions());
|
66 |
+
|
67 |
+
// أزرار الرجوع
|
68 |
+
document.getElementById('transfer-back').addEventListener('click', () => this.switchScreen('main'));
|
69 |
+
|
70 |
+
// شاشة التحويل
|
71 |
+
this.setupTransferScreen();
|
72 |
+
|
73 |
+
// إدخال رقم الهاتف
|
74 |
+
document.getElementById('phone-number').addEventListener('input', this.formatPhoneNumber);
|
75 |
+
document.getElementById('pin-code').addEventListener('keypress', (e) => {
|
76 |
+
if (e.key === 'Enter') this.handleLogin();
|
77 |
+
});
|
78 |
+
}
|
79 |
+
|
80 |
+
// عرض شاشة التحميل
|
81 |
+
showLoadingScreen() {
|
82 |
+
document.getElementById('loading-screen').style.display = 'flex';
|
83 |
+
}
|
84 |
+
|
85 |
+
// إخفاء شاشة التحميل
|
86 |
+
hideLoadingScreen() {
|
87 |
+
document.getElementById('loading-screen').style.display = 'none';
|
88 |
+
}
|
89 |
+
|
90 |
+
// فحص حالة المصادقة
|
91 |
+
checkAuthStatus() {
|
92 |
+
const savedUser = localStorage.getItem('unifiedWallet_user');
|
93 |
+
if (savedUser) {
|
94 |
+
this.currentUser = JSON.parse(savedUser);
|
95 |
+
this.switchScreen('main');
|
96 |
+
this.loadUserWallets();
|
97 |
+
} else {
|
98 |
+
this.switchScreen('login');
|
99 |
+
}
|
100 |
+
}
|
101 |
+
|
102 |
+
// تسجيل الدخول
|
103 |
+
async handleLogin() {
|
104 |
+
const phoneNumber = document.getElementById('phone-number').value;
|
105 |
+
const pinCode = document.getElementById('pin-code').value;
|
106 |
+
|
107 |
+
if (!phoneNumber || !pinCode) {
|
108 |
+
this.showAlert('يرجى إدخال رقم الهاتف ورمز PIN', 'error');
|
109 |
+
return;
|
110 |
+
}
|
111 |
+
|
112 |
+
this.showLoading('جاري تسجيل الدخول...');
|
113 |
+
|
114 |
+
try {
|
115 |
+
const user = await this.authManager.loginWithPin(phoneNumber, pinCode);
|
116 |
+
this.currentUser = user;
|
117 |
+
|
118 |
+
this.hideLoading();
|
119 |
+
this.showAlert('تم تسجيل الدخول بنجاح', 'success');
|
120 |
+
this.switchScreen('main');
|
121 |
+
await this.loadUserWallets();
|
122 |
+
|
123 |
+
} catch (error) {
|
124 |
+
this.hideLoading();
|
125 |
+
this.showAlert(error.message, 'error');
|
126 |
+
}
|
127 |
+
}
|
128 |
+
|
129 |
+
// تسجيل الدخ��ل بالبصمة
|
130 |
+
async handleBiometricLogin() {
|
131 |
+
this.showLoading('جاري التحقق من البصمة...');
|
132 |
+
|
133 |
+
try {
|
134 |
+
const user = await this.authManager.loginWithBiometric();
|
135 |
+
this.currentUser = user;
|
136 |
+
|
137 |
+
this.hideLoading();
|
138 |
+
this.showAlert('تم تسجيل الدخول بالبصمة بنجاح', 'success');
|
139 |
+
this.switchScreen('main');
|
140 |
+
await this.loadUserWallets();
|
141 |
+
|
142 |
+
} catch (error) {
|
143 |
+
this.hideLoading();
|
144 |
+
this.showAlert(error.message, 'error');
|
145 |
+
}
|
146 |
+
}
|
147 |
+
|
148 |
+
// تحميل محافظ المستخدم
|
149 |
+
async loadUserWallets() {
|
150 |
+
this.showLoading('جاري تحميل المحافظ...');
|
151 |
+
|
152 |
+
try {
|
153 |
+
// الحصول على محافظ المستخدم من مدير المحافظ
|
154 |
+
this.wallets = this.walletManager.getUserWallets();
|
155 |
+
|
156 |
+
// إذا لم توجد محافظ، إنشاء محافظ تجريبية
|
157 |
+
if (this.wallets.length === 0) {
|
158 |
+
await this.createDemoWallets();
|
159 |
+
}
|
160 |
+
|
161 |
+
// تحديث أرصدة المحافظ
|
162 |
+
await this.walletManager.updateAllBalances();
|
163 |
+
this.wallets = this.walletManager.getUserWallets();
|
164 |
+
|
165 |
+
this.loadRecentTransactions();
|
166 |
+
this.updateUI();
|
167 |
+
this.hideLoading();
|
168 |
+
|
169 |
+
} catch (error) {
|
170 |
+
this.hideLoading();
|
171 |
+
this.showAlert('فشل في تحميل المحافظ', 'error');
|
172 |
+
}
|
173 |
+
}
|
174 |
+
|
175 |
+
// إنشاء محافظ تجريبية
|
176 |
+
async createDemoWallets() {
|
177 |
+
const demoWallets = [
|
178 |
+
{ id: 'jawali', accountNumber: '777123456', pin: '1234' },
|
179 |
+
{ id: 'onecash', accountNumber: '777234567', pin: '1234' },
|
180 |
+
{ id: 'cash', accountNumber: '777345678', pin: '1234' },
|
181 |
+
{ id: 'jaib', accountNumber: '777456789', pin: '1234' },
|
182 |
+
{ id: 'mfloos', accountNumber: '777567890', pin: '1234' },
|
183 |
+
{ id: 'mobilemoney', accountNumber: '777678901', pin: '1234' }
|
184 |
+
];
|
185 |
+
|
186 |
+
for (const wallet of demoWallets) {
|
187 |
+
try {
|
188 |
+
await this.walletManager.addWallet(wallet.id, wallet.accountNumber, wallet.pin);
|
189 |
+
} catch (error) {
|
190 |
+
console.warn(`فشل في إضافة محفظة ${wallet.id}:`, error.message);
|
191 |
+
}
|
192 |
+
}
|
193 |
+
}
|
194 |
+
|
195 |
+
// تحميل المعاملات الأخيرة
|
196 |
+
loadRecentTransactions() {
|
197 |
+
this.transactions = [
|
198 |
+
{
|
199 |
+
id: 1,
|
200 |
+
type: 'send',
|
201 |
+
title: 'تحويل إلى أحمد محمد',
|
202 |
+
wallet: 'جوالي',
|
203 |
+
amount: -500,
|
204 |
+
time: '10:30 ص',
|
205 |
+
date: new Date().toISOString()
|
206 |
+
},
|
207 |
+
{
|
208 |
+
id: 2,
|
209 |
+
type: 'receive',
|
210 |
+
title: 'استلام من سارة أحمد',
|
211 |
+
wallet: 'ONE Cash',
|
212 |
+
amount: 1200,
|
213 |
+
time: '09:15 ص',
|
214 |
+
date: new Date().toISOString()
|
215 |
+
},
|
216 |
+
{
|
217 |
+
id: 3,
|
218 |
+
type: 'bill',
|
219 |
+
title: 'فاتورة كهرباء',
|
220 |
+
wallet: 'Cash',
|
221 |
+
amount: -350,
|
222 |
+
time: 'أمس',
|
223 |
+
date: new Date(Date.now() - 86400000).toISOString()
|
224 |
+
},
|
225 |
+
{
|
226 |
+
id: 4,
|
227 |
+
type: 'send',
|
228 |
+
title: 'شحن رصيد',
|
229 |
+
wallet: 'Jaib',
|
230 |
+
amount: -100,
|
231 |
+
time: 'أمس',
|
232 |
+
date: new Date(Date.now() - 86400000).toISOString()
|
233 |
+
}
|
234 |
+
];
|
235 |
+
}
|
236 |
+
|
237 |
+
// تحديث واجهة المستخدم
|
238 |
+
updateUI() {
|
239 |
+
this.updateUserInfo();
|
240 |
+
this.updateTotalBalance();
|
241 |
+
this.renderWallets();
|
242 |
+
this.renderTransactions();
|
243 |
+
}
|
244 |
+
|
245 |
+
// تحديث معلومات المستخدم
|
246 |
+
updateUserInfo() {
|
247 |
+
if (this.currentUser) {
|
248 |
+
document.getElementById('user-name').textContent = `مرحباً ${this.currentUser.name}`;
|
249 |
+
document.getElementById('user-phone').textContent = this.currentUser.phone;
|
250 |
+
}
|
251 |
+
}
|
252 |
+
|
253 |
+
// تحديث الرصيد الإجمالي
|
254 |
+
updateTotalBalance() {
|
255 |
+
const total = this.wallets.reduce((sum, wallet) => sum + wallet.balance, 0);
|
256 |
+
const balanceElement = document.getElementById('total-balance');
|
257 |
+
|
258 |
+
if (this.isBalanceHidden) {
|
259 |
+
balanceElement.textContent = '••••• ر.ي';
|
260 |
+
} else {
|
261 |
+
balanceElement.textContent = `${total.toLocaleString()} ر.ي`;
|
262 |
+
}
|
263 |
+
}
|
264 |
+
|
265 |
+
// عرض المحافظ
|
266 |
+
renderWallets() {
|
267 |
+
const walletsList = document.getElementById('wallets-list');
|
268 |
+
walletsList.innerHTML = '';
|
269 |
+
|
270 |
+
this.wallets.forEach(wallet => {
|
271 |
+
const walletCard = this.createWalletCard(wallet);
|
272 |
+
walletsList.appendChild(walletCard);
|
273 |
+
});
|
274 |
+
}
|
275 |
+
|
276 |
+
// إنشاء بطاقة محفظة
|
277 |
+
createWalletCard(wallet) {
|
278 |
+
const card = document.createElement('div');
|
279 |
+
card.className = 'wallet-card';
|
280 |
+
card.onclick = () => this.openWalletDetails(wallet);
|
281 |
+
|
282 |
+
const balanceDisplay = this.isBalanceHidden ?
|
283 |
+
'•••••' : wallet.balance.toLocaleString();
|
284 |
+
|
285 |
+
card.innerHTML = `
|
286 |
+
<div class="wallet-info">
|
287 |
+
<div class="wallet-icon">
|
288 |
+
<img src="${wallet.icon}" alt="${wallet.name}" onerror="this.style.display='none'">
|
289 |
+
</div>
|
290 |
+
<div class="wallet-details">
|
291 |
+
<h4>${wallet.name}</h4>
|
292 |
+
<p>${wallet.provider}</p>
|
293 |
+
</div>
|
294 |
+
</div>
|
295 |
+
<div class="wallet-balance">
|
296 |
+
<div class="amount">${balanceDisplay}</div>
|
297 |
+
<div class="currency">ر.ي</div>
|
298 |
+
</div>
|
299 |
+
`;
|
300 |
+
|
301 |
+
return card;
|
302 |
+
}
|
303 |
+
|
304 |
+
// عرض المعاملات
|
305 |
+
renderTransactions() {
|
306 |
+
const transactionsList = document.getElementById('transactions-list');
|
307 |
+
transactionsList.innerHTML = '';
|
308 |
+
|
309 |
+
this.transactions.slice(0, 5).forEach(transaction => {
|
310 |
+
const transactionItem = this.createTransactionItem(transaction);
|
311 |
+
transactionsList.appendChild(transactionItem);
|
312 |
+
});
|
313 |
+
}
|
314 |
+
|
315 |
+
// إنشاء عنصر معاملة
|
316 |
+
createTransactionItem(transaction) {
|
317 |
+
const item = document.createElement('div');
|
318 |
+
item.className = 'transaction-item';
|
319 |
+
|
320 |
+
const iconClass = transaction.type === 'send' ? 'fas fa-arrow-up' :
|
321 |
+
transaction.type === 'receive' ? 'fas fa-arrow-down' :
|
322 |
+
'fas fa-file-invoice-dollar';
|
323 |
+
|
324 |
+
const amountClass = transaction.amount > 0 ? 'positive' : 'negative';
|
325 |
+
const amountSign = transaction.amount > 0 ? '+' : '';
|
326 |
+
|
327 |
+
item.innerHTML = `
|
328 |
+
<div class="transaction-info">
|
329 |
+
<div class="transaction-icon ${transaction.type}">
|
330 |
+
<i class="${iconClass}"></i>
|
331 |
+
</div>
|
332 |
+
<div class="transaction-details">
|
333 |
+
<h5>${transaction.title}</h5>
|
334 |
+
<p>${transaction.wallet}</p>
|
335 |
+
</div>
|
336 |
+
</div>
|
337 |
+
<div class="transaction-amount ${amountClass}">
|
338 |
+
<div class="amount">${amountSign}${Math.abs(transaction.amount).toLocaleString()} ر.ي</div>
|
339 |
+
<div class="time">${transaction.time}</div>
|
340 |
+
</div>
|
341 |
+
`;
|
342 |
+
|
343 |
+
return item;
|
344 |
+
}
|
345 |
+
|
346 |
+
// محاكاة استدعاء API
|
347 |
+
simulateAPICall(delay = 1000) {
|
348 |
+
return new Promise((resolve) => {
|
349 |
+
setTimeout(resolve, delay);
|
350 |
+
});
|
351 |
+
}
|
352 |
+
|
353 |
+
// عرض رسالة تنبيه
|
354 |
+
showAlert(message, type = 'info') {
|
355 |
+
this.notificationManager.showToast(message, type);
|
356 |
+
}
|
357 |
+
|
358 |
+
// عرض شاشة التحميل
|
359 |
+
showLoading(message = 'جاري التحميل...') {
|
360 |
+
this.loadingModal = this.notificationManager.showLoading(message);
|
361 |
+
}
|
362 |
+
|
363 |
+
// إخفاء شاشة التحميل
|
364 |
+
hideLoading() {
|
365 |
+
this.notificationManager.hideLoading();
|
366 |
+
}
|
367 |
+
|
368 |
+
// تبديل الشاشات
|
369 |
+
switchScreen(screenName) {
|
370 |
+
document.querySelectorAll('.screen').forEach(screen => {
|
371 |
+
screen.classList.remove('active');
|
372 |
+
});
|
373 |
+
|
374 |
+
document.getElementById(`${screenName}-screen`).classList.add('active');
|
375 |
+
|
376 |
+
// تحديث شريط التنقل
|
377 |
+
document.querySelectorAll('.nav-item').forEach(item => {
|
378 |
+
item.classList.remove('active');
|
379 |
+
});
|
380 |
+
|
381 |
+
const activeNavItem = document.querySelector(`[data-screen="${screenName}"]`);
|
382 |
+
if (activeNavItem) {
|
383 |
+
activeNavItem.classList.add('active');
|
384 |
+
}
|
385 |
+
|
386 |
+
this.currentScreen = screenName;
|
387 |
+
}
|
388 |
+
|
389 |
+
// تنسيق رقم الهاتف
|
390 |
+
formatPhoneNumber(e) {
|
391 |
+
let value = e.target.value.replace(/\D/g, '');
|
392 |
+
if (value.length > 9) {
|
393 |
+
value = value.slice(0, 9);
|
394 |
+
}
|
395 |
+
e.target.value = value;
|
396 |
+
}
|
397 |
+
|
398 |
+
// تحديث الأرصدة
|
399 |
+
async refreshBalances() {
|
400 |
+
this.showLoading('جاري تحديث الأرصدة...');
|
401 |
+
await this.simulateAPICall(1500);
|
402 |
+
this.updateTotalBalance();
|
403 |
+
this.renderWallets();
|
404 |
+
this.hideLoading();
|
405 |
+
this.showAlert('تم تحديث الأرصدة بنجاح', 'success');
|
406 |
+
}
|
407 |
+
|
408 |
+
// إخفاء/إظهار الرصيد
|
409 |
+
toggleBalanceVisibility() {
|
410 |
+
this.isBalanceHidden = !this.isBalanceHidden;
|
411 |
+
const hideBtn = document.getElementById('hide-balance');
|
412 |
+
const icon = hideBtn.querySelector('i');
|
413 |
+
|
414 |
+
if (this.isBalanceHidden) {
|
415 |
+
icon.className = 'fas fa-eye';
|
416 |
+
hideBtn.innerHTML = '<i class="fas fa-eye"></i> إظهار';
|
417 |
+
} else {
|
418 |
+
icon.className = 'fas fa-eye-slash';
|
419 |
+
hideBtn.innerHTML = '<i class="fas fa-eye-slash"></i> إخفاء';
|
420 |
+
}
|
421 |
+
|
422 |
+
this.updateTotalBalance();
|
423 |
+
this.renderWallets();
|
424 |
+
}
|
425 |
+
|
426 |
+
// تحميل بيانات المستخدم
|
427 |
+
loadUserData() {
|
428 |
+
// تحميل الإعدادات المحفوظة
|
429 |
+
const savedSettings = localStorage.getItem('unifiedWallet_settings');
|
430 |
+
if (savedSettings) {
|
431 |
+
const settings = JSON.parse(savedSettings);
|
432 |
+
this.isBalanceHidden = settings.hideBalance || false;
|
433 |
+
}
|
434 |
+
}
|
435 |
+
|
436 |
+
// حفظ بيانات المستخدم
|
437 |
+
saveUserData() {
|
438 |
+
const settings = {
|
439 |
+
hideBalance: this.isBalanceHidden
|
440 |
+
};
|
441 |
+
localStorage.setItem('unifiedWallet_settings', JSON.stringify(settings));
|
442 |
+
}
|
443 |
+
|
444 |
+
// إعداد شاشة التحويل
|
445 |
+
setupTransferScreen() {
|
446 |
+
this.currentTransferStep = 1;
|
447 |
+
this.selectedSourceWallet = null;
|
448 |
+
this.transferData = {};
|
449 |
+
|
450 |
+
// أزرار التنقل
|
451 |
+
document.getElementById('transfer-next-btn').addEventListener('click', () => this.nextTransferStep());
|
452 |
+
document.getElementById('transfer-prev-btn').addEventListener('click', () => this.prevTransferStep());
|
453 |
+
document.getElementById('transfer-confirm-btn').addEventListener('click', () => this.confirmTransfer());
|
454 |
+
|
455 |
+
// اقتراحات المبلغ
|
456 |
+
document.querySelectorAll('.amount-btn').forEach(btn => {
|
457 |
+
btn.addEventListener('click', () => {
|
458 |
+
document.getElementById('transfer-amount').value = btn.dataset.amount;
|
459 |
+
});
|
460 |
+
});
|
461 |
+
}
|
462 |
+
|
463 |
+
// عرض شاشة التحويل
|
464 |
+
showTransferScreen() {
|
465 |
+
this.switchScreen('transfer');
|
466 |
+
this.resetTransferForm();
|
467 |
+
this.renderSourceWallets();
|
468 |
+
}
|
469 |
+
|
470 |
+
// إعادة تعيين نموذج التحويل
|
471 |
+
resetTransferForm() {
|
472 |
+
this.currentTransferStep = 1;
|
473 |
+
this.selectedSourceWallet = null;
|
474 |
+
this.transferData = {};
|
475 |
+
|
476 |
+
// إعادة تعيين الخطوات
|
477 |
+
this.updateTransferSteps();
|
478 |
+
|
479 |
+
// مسح النموذج
|
480 |
+
document.getElementById('destination-wallet').value = '';
|
481 |
+
document.getElementById('recipient-number').value = '';
|
482 |
+
document.getElementById('transfer-amount').value = '';
|
483 |
+
document.getElementById('transfer-note').value = '';
|
484 |
+
document.getElementById('transfer-pin').value = '';
|
485 |
+
}
|
486 |
+
|
487 |
+
// تحديث مؤشر الخطوات
|
488 |
+
updateTransferSteps() {
|
489 |
+
for (let i = 1; i <= 3; i++) {
|
490 |
+
const step = document.getElementById(`transfer-step-${i}`);
|
491 |
+
const content = document.getElementById(`transfer-step-content-${i}`);
|
492 |
+
|
493 |
+
if (i < this.currentTransferStep) {
|
494 |
+
step.classList.add('completed');
|
495 |
+
step.classList.remove('active');
|
496 |
+
} else if (i === this.currentTransferStep) {
|
497 |
+
step.classList.add('active');
|
498 |
+
step.classList.remove('completed');
|
499 |
+
} else {
|
500 |
+
step.classList.remove('active', 'completed');
|
501 |
+
}
|
502 |
+
|
503 |
+
content.classList.toggle('active', i === this.currentTransferStep);
|
504 |
+
}
|
505 |
+
|
506 |
+
// تحديث أزرار التنقل
|
507 |
+
const prevBtn = document.getElementById('transfer-prev-btn');
|
508 |
+
const nextBtn = document.getElementById('transfer-next-btn');
|
509 |
+
const confirmBtn = document.getElementById('transfer-confirm-btn');
|
510 |
+
|
511 |
+
prevBtn.style.display = this.currentTransferStep > 1 ? 'block' : 'none';
|
512 |
+
nextBtn.style.display = this.currentTransferStep < 3 ? 'block' : 'none';
|
513 |
+
confirmBtn.style.display = this.currentTransferStep === 3 ? 'block' : 'none';
|
514 |
+
}
|
515 |
+
|
516 |
+
// عرض المحافظ المصدر
|
517 |
+
renderSourceWallets() {
|
518 |
+
const container = document.getElementById('source-wallet-selection');
|
519 |
+
container.innerHTML = '';
|
520 |
+
|
521 |
+
this.wallets.forEach(wallet => {
|
522 |
+
const option = this.createWalletOption(wallet);
|
523 |
+
container.appendChild(option);
|
524 |
+
});
|
525 |
+
}
|
526 |
+
|
527 |
+
// إنشاء خيار محفظة
|
528 |
+
createWalletOption(wallet) {
|
529 |
+
const option = document.createElement('div');
|
530 |
+
option.className = 'wallet-option';
|
531 |
+
option.onclick = () => this.selectSourceWallet(wallet);
|
532 |
+
|
533 |
+
const balanceDisplay = this.isBalanceHidden ?
|
534 |
+
'•••••' : wallet.balance.toLocaleString();
|
535 |
+
|
536 |
+
option.innerHTML = `
|
537 |
+
<div class="wallet-option-info">
|
538 |
+
<div class="wallet-option-icon">
|
539 |
+
<img src="${wallet.icon}" alt="${wallet.name}" onerror="this.style.display='none'">
|
540 |
+
</div>
|
541 |
+
<div class="wallet-option-details">
|
542 |
+
<h4>${wallet.name}</h4>
|
543 |
+
<p>${wallet.provider}</p>
|
544 |
+
</div>
|
545 |
+
</div>
|
546 |
+
<div class="wallet-option-balance">
|
547 |
+
<div class="amount">${balanceDisplay}</div>
|
548 |
+
<div class="currency">ر.ي</div>
|
549 |
+
</div>
|
550 |
+
`;
|
551 |
+
|
552 |
+
return option;
|
553 |
+
}
|
554 |
+
|
555 |
+
// اختيار المحفظة المصدر
|
556 |
+
selectSourceWallet(wallet) {
|
557 |
+
this.selectedSourceWallet = wallet;
|
558 |
+
|
559 |
+
// تحديث التحديد البصري
|
560 |
+
document.querySelectorAll('.wallet-option').forEach(option => {
|
561 |
+
option.classList.remove('selected');
|
562 |
+
});
|
563 |
+
event.currentTarget.classList.add('selected');
|
564 |
+
|
565 |
+
this.transferData.sourceWallet = wallet;
|
566 |
+
}
|
567 |
+
|
568 |
+
// الخطوة التالية
|
569 |
+
nextTransferStep() {
|
570 |
+
if (this.validateCurrentStep()) {
|
571 |
+
this.currentTransferStep++;
|
572 |
+
this.updateTransferSteps();
|
573 |
+
|
574 |
+
if (this.currentTransferStep === 3) {
|
575 |
+
this.updateTransferSummary();
|
576 |
+
}
|
577 |
+
}
|
578 |
+
}
|
579 |
+
|
580 |
+
// الخطوة السابقة
|
581 |
+
prevTransferStep() {
|
582 |
+
this.currentTransferStep--;
|
583 |
+
this.updateTransferSteps();
|
584 |
+
}
|
585 |
+
|
586 |
+
// التحقق من صحة الخطوة الحالية
|
587 |
+
validateCurrentStep() {
|
588 |
+
switch (this.currentTransferStep) {
|
589 |
+
case 1:
|
590 |
+
if (!this.selectedSourceWallet) {
|
591 |
+
this.showAlert('يرجى اختيار المحفظة المرسلة', 'error');
|
592 |
+
return false;
|
593 |
+
}
|
594 |
+
return true;
|
595 |
+
|
596 |
+
case 2:
|
597 |
+
const destinationWallet = document.getElementById('destination-wallet').value;
|
598 |
+
const recipientNumber = document.getElementById('recipient-number').value;
|
599 |
+
const amount = parseFloat(document.getElementById('transfer-amount').value);
|
600 |
+
|
601 |
+
if (!destinationWallet) {
|
602 |
+
this.showAlert('يرجى اختيار المحفظة المستقبلة', 'error');
|
603 |
+
return false;
|
604 |
+
}
|
605 |
+
|
606 |
+
if (!recipientNumber || recipientNumber.length !== 9) {
|
607 |
+
this.showAlert('يرجى إدخال رقم المستقبل صحيح (9 أرقام)', 'error');
|
608 |
+
return false;
|
609 |
+
}
|
610 |
+
|
611 |
+
if (!amount || amount <= 0) {
|
612 |
+
this.showAlert('يرجى إدخال مبلغ صحيح', 'error');
|
613 |
+
return false;
|
614 |
+
}
|
615 |
+
|
616 |
+
if (amount > this.selectedSourceWallet.balance) {
|
617 |
+
this.showAlert('المبلغ أكبر من الرصيد المتاح', 'error');
|
618 |
+
return false;
|
619 |
+
}
|
620 |
+
|
621 |
+
// حفظ بيانات التحويل
|
622 |
+
this.transferData.destinationWallet = destinationWallet;
|
623 |
+
this.transferData.recipientNumber = recipientNumber;
|
624 |
+
this.transferData.amount = amount;
|
625 |
+
this.transferData.note = document.getElementById('transfer-note').value;
|
626 |
+
|
627 |
+
return true;
|
628 |
+
|
629 |
+
default:
|
630 |
+
return true;
|
631 |
+
}
|
632 |
+
}
|
633 |
+
|
634 |
+
// تحديث ملخص التحويل
|
635 |
+
updateTransferSummary() {
|
636 |
+
const sourceWalletInfo = this.walletManager.getWalletInfo(this.transferData.sourceWallet.id);
|
637 |
+
const destinationWalletInfo = this.walletManager.getWalletInfo(this.transferData.destinationWallet);
|
638 |
+
|
639 |
+
const fee = sourceWalletInfo.fees.transfer;
|
640 |
+
const total = this.transferData.amount + fee;
|
641 |
+
|
642 |
+
document.getElementById('summary-from-wallet').textContent = this.transferData.sourceWallet.name;
|
643 |
+
document.getElementById('summary-to-wallet').textContent = destinationWalletInfo.name;
|
644 |
+
document.getElementById('summary-recipient').textContent = `+967${this.transferData.recipientNumber}`;
|
645 |
+
document.getElementById('summary-amount').textContent = `${this.transferData.amount.toLocaleString()} ر.ي`;
|
646 |
+
document.getElementById('summary-fee').textContent = `${fee.toLocaleString()} ر.ي`;
|
647 |
+
document.getElementById('summary-total').textContent = `${total.toLocaleString()} ر.ي`;
|
648 |
+
}
|
649 |
+
|
650 |
+
// تأكيد التحويل
|
651 |
+
async confirmTransfer() {
|
652 |
+
const pin = document.getElementById('transfer-pin').value;
|
653 |
+
|
654 |
+
if (!pin) {
|
655 |
+
this.showAlert('يرجى إدخال رمز PIN', 'error');
|
656 |
+
return;
|
657 |
+
}
|
658 |
+
|
659 |
+
this.showLoading('جاري تنفيذ التحويل...');
|
660 |
+
|
661 |
+
try {
|
662 |
+
const result = await this.walletManager.executeTransfer(
|
663 |
+
this.transferData.sourceWallet.id,
|
664 |
+
this.transferData.destinationWallet,
|
665 |
+
this.transferData.amount,
|
666 |
+
this.transferData.recipientNumber,
|
667 |
+
pin
|
668 |
+
);
|
669 |
+
|
670 |
+
this.hideLoading();
|
671 |
+
this.showTransferSuccess(result);
|
672 |
+
|
673 |
+
// تحديث البيانات
|
674 |
+
await this.loadUserWallets();
|
675 |
+
|
676 |
+
} catch (error) {
|
677 |
+
this.hideLoading();
|
678 |
+
this.showAlert(error.message, 'error');
|
679 |
+
}
|
680 |
+
}
|
681 |
+
|
682 |
+
// عرض نجاح التحويل
|
683 |
+
showTransferSuccess(result) {
|
684 |
+
const message = `
|
685 |
+
تم التحويل بنجاح!
|
686 |
+
|
687 |
+
رقم المعاملة: ${result.transactionId}
|
688 |
+
المبلغ المحول: ${result.amount.toLocaleString()} ر.ي
|
689 |
+
الرسوم: ${result.fee.toLocaleString()} ر.ي
|
690 |
+
الرصيد المتبقي: ${result.newBalance.toLocaleString()} ر.ي
|
691 |
+
`;
|
692 |
+
|
693 |
+
this.showAlert(message, 'success');
|
694 |
+
this.switchScreen('main');
|
695 |
+
}
|
696 |
+
|
697 |
+
// الوظائف التي سيتم تطويرها لاحقاً
|
698 |
+
showBillsScreen() { console.log('عرض شاشة الفواتير'); }
|
699 |
+
showRechargeScreen() { console.log('عرض شاشة الشحن'); }
|
700 |
+
startQRScan() { console.log('بدء مسح QR'); }
|
701 |
+
showSettings() { console.log('عرض الإعدادات'); }
|
702 |
+
showNotifications() {
|
703 |
+
this.notificationManager.showNotificationsList();
|
704 |
+
}
|
705 |
+
showWalletManagement() { console.log('إدارة المحافظ'); }
|
706 |
+
showAllTransactions() { console.log('عرض جميع المعاملات'); }
|
707 |
+
openWalletDetails(wallet) { console.log('تفاصيل المحفظة:', wallet.name); }
|
708 |
+
showRegisterModal() {
|
709 |
+
this.notificationManager.showModal(
|
710 |
+
'إنشاء حساب جديد',
|
711 |
+
`
|
712 |
+
<div class="form-group">
|
713 |
+
<label>رقم الهاتف</label>
|
714 |
+
<div class="input-group">
|
715 |
+
<span class="input-prefix">+967</span>
|
716 |
+
<input type="tel" id="register-phone" placeholder="7xxxxxxxx" maxlength="9">
|
717 |
+
</div>
|
718 |
+
</div>
|
719 |
+
<div class="form-group">
|
720 |
+
<label>رمز PIN</label>
|
721 |
+
<input type="password" id="register-pin" placeholder="أدخل رمز PIN" maxlength="6">
|
722 |
+
</div>
|
723 |
+
<div class="form-group">
|
724 |
+
<label>تأكيد رمز PIN</label>
|
725 |
+
<input type="password" id="register-pin-confirm" placeholder="أعد إدخال رمز PIN" maxlength="6">
|
726 |
+
</div>
|
727 |
+
`,
|
728 |
+
[
|
729 |
+
{
|
730 |
+
text: 'إنشاء الحساب',
|
731 |
+
class: 'btn-primary',
|
732 |
+
action: () => this.handleRegister()
|
733 |
+
},
|
734 |
+
{
|
735 |
+
text: 'إلغاء',
|
736 |
+
class: 'btn-secondary'
|
737 |
+
}
|
738 |
+
]
|
739 |
+
);
|
740 |
+
}
|
741 |
+
|
742 |
+
// التعامل مع التسجيل
|
743 |
+
handleRegister() {
|
744 |
+
const phone = document.getElementById('register-phone').value;
|
745 |
+
const pin = document.getElementById('register-pin').value;
|
746 |
+
const pinConfirm = document.getElementById('register-pin-confirm').value;
|
747 |
+
|
748 |
+
if (!phone || !pin || !pinConfirm) {
|
749 |
+
this.showAlert('يرجى ملء جميع الحقول', 'error');
|
750 |
+
return;
|
751 |
+
}
|
752 |
+
|
753 |
+
if (pin !== pinConfirm) {
|
754 |
+
this.showAlert('رمز PIN غير متطابق', 'error');
|
755 |
+
return;
|
756 |
+
}
|
757 |
+
|
758 |
+
if (phone.length !== 9) {
|
759 |
+
this.showAlert('رقم الهاتف يجب أن يكون 9 أرقام', 'error');
|
760 |
+
return;
|
761 |
+
}
|
762 |
+
|
763 |
+
if (pin.length < 4) {
|
764 |
+
this.showAlert('رمز PIN يجب أن يكون 4 أرقام على الأقل', 'error');
|
765 |
+
return;
|
766 |
+
}
|
767 |
+
|
768 |
+
// محاكاة إنشاء الحساب
|
769 |
+
this.showAlert('تم إنشاء الحساب بنجاح! يمكنك الآن تسجيل الدخول', 'success');
|
770 |
+
|
771 |
+
// ملء بيانات تسجيل الدخول
|
772 |
+
document.getElementById('phone-number').value = phone;
|
773 |
+
document.getElementById('pin-code').value = pin;
|
774 |
+
}
|
775 |
+
}
|
776 |
+
|
777 |
+
// تهيئة التطبيق عند تحميل الصفحة
|
778 |
+
document.addEventListener('DOMContentLoaded', () => {
|
779 |
+
window.app = new UnifiedWalletApp();
|
780 |
+
});
|
app/(public)/layout.tsx
CHANGED
@@ -6,7 +6,7 @@ export default async function PublicLayout({
|
|
6 |
children: React.ReactNode;
|
7 |
}>) {
|
8 |
return (
|
9 |
-
<div className="h-screen bg-
|
10 |
<div className="background__noisy" />
|
11 |
<Navigation />
|
12 |
{children}
|
|
|
6 |
children: React.ReactNode;
|
7 |
}>) {
|
8 |
return (
|
9 |
+
<div className="min-h-screen bg-black z-1 relative">
|
10 |
<div className="background__noisy" />
|
11 |
<Navigation />
|
12 |
{children}
|
app/(public)/page.tsx
CHANGED
@@ -1,5 +1,44 @@
|
|
1 |
-
import {
|
2 |
-
|
3 |
-
export default
|
4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
}
|
|
|
1 |
+
import { AskAi } from "@/components/space/ask-ai";
|
2 |
+
import { redirect } from "next/navigation";
|
3 |
+
export default function Home() {
|
4 |
+
redirect("/projects/new");
|
5 |
+
return (
|
6 |
+
<>
|
7 |
+
<header className="container mx-auto pt-20 px-6 relative flex flex-col items-center justify-center text-center">
|
8 |
+
<div className="rounded-full border border-neutral-100/10 bg-neutral-100/5 text-xs text-neutral-300 px-3 py-1 max-w-max mx-auto mb-2">
|
9 |
+
✨ DeepSite Public Beta
|
10 |
+
</div>
|
11 |
+
<h1 className="text-8xl font-semibold text-white font-mono max-w-4xl">
|
12 |
+
Code your website with AI in seconds
|
13 |
+
</h1>
|
14 |
+
<p className="text-2xl text-neutral-300/80 mt-4 text-center max-w-2xl">
|
15 |
+
Vibe Coding has never been so easy.
|
16 |
+
</p>
|
17 |
+
<div className="mt-14 max-w-2xl w-full mx-auto">
|
18 |
+
<AskAi />
|
19 |
+
</div>
|
20 |
+
<div className="absolute inset-0 pointer-events-none -z-[1]">
|
21 |
+
<div className="w-full h-full bg-gradient-to-r from-purple-500 to-pink-500 opacity-10 blur-3xl rounded-full" />
|
22 |
+
<div className="w-2/3 h-3/4 bg-gradient-to-r from-blue-500 to-teal-500 opacity-24 blur-3xl absolute -top-20 right-10 transform rotate-12" />
|
23 |
+
<div className="w-1/2 h-1/2 bg-gradient-to-r from-amber-500 to-rose-500 opacity-20 blur-3xl absolute bottom-0 left-10 rounded-3xl" />
|
24 |
+
<div className="w-48 h-48 bg-gradient-to-r from-cyan-500 to-indigo-500 opacity-20 blur-3xl absolute top-1/3 right-1/3 rounded-lg transform -rotate-15" />
|
25 |
+
</div>
|
26 |
+
</header>
|
27 |
+
<div id="community" className="h-screen flex items-center justify-center">
|
28 |
+
<h1 className="text-7xl font-extrabold text-white font-mono">
|
29 |
+
Community Driven
|
30 |
+
</h1>
|
31 |
+
</div>
|
32 |
+
<div id="deploy" className="h-screen flex items-center justify-center">
|
33 |
+
<h1 className="text-7xl font-extrabold text-white font-mono">
|
34 |
+
Deploy your website in seconds
|
35 |
+
</h1>
|
36 |
+
</div>
|
37 |
+
<div id="features" className="h-screen flex items-center justify-center">
|
38 |
+
<h1 className="text-7xl font-extrabold text-white font-mono">
|
39 |
+
Features that make you smile
|
40 |
+
</h1>
|
41 |
+
</div>
|
42 |
+
</>
|
43 |
+
);
|
44 |
}
|
app/(public)/projects/page.tsx
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { redirect } from "next/navigation";
|
2 |
+
|
3 |
+
import { MyProjects } from "@/components/my-projects";
|
4 |
+
import { getProjects } from "@/app/actions/projects";
|
5 |
+
|
6 |
+
export default async function ProjectsPage() {
|
7 |
+
const { ok, projects } = await getProjects();
|
8 |
+
if (!ok) {
|
9 |
+
redirect("/");
|
10 |
+
}
|
11 |
+
|
12 |
+
return <MyProjects projects={projects} />;
|
13 |
+
}
|
app/[namespace]/[repoId]/page.tsx
DELETED
@@ -1,28 +0,0 @@
|
|
1 |
-
import { AppEditor } from "@/components/editor";
|
2 |
-
import { generateSEO } from "@/lib/seo";
|
3 |
-
import { Metadata } from "next";
|
4 |
-
|
5 |
-
export async function generateMetadata({
|
6 |
-
params,
|
7 |
-
}: {
|
8 |
-
params: Promise<{ namespace: string; repoId: string }>;
|
9 |
-
}): Promise<Metadata> {
|
10 |
-
const { namespace, repoId } = await params;
|
11 |
-
|
12 |
-
return generateSEO({
|
13 |
-
title: `${namespace}/${repoId} - DeepSite Editor`,
|
14 |
-
description: `Edit and build ${namespace}/${repoId} with AI-powered tools on DeepSite. Create stunning websites with no code required.`,
|
15 |
-
path: `/${namespace}/${repoId}`,
|
16 |
-
// Prevent indexing of individual project editor pages if they contain sensitive content
|
17 |
-
noIndex: false, // Set to true if you want to keep project pages private
|
18 |
-
});
|
19 |
-
}
|
20 |
-
|
21 |
-
export default async function ProjectNamespacePage({
|
22 |
-
params,
|
23 |
-
}: {
|
24 |
-
params: Promise<{ namespace: string; repoId: string }>;
|
25 |
-
}) {
|
26 |
-
const { namespace, repoId } = await params;
|
27 |
-
return <AppEditor namespace={namespace} repoId={repoId} />;
|
28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/actions/projects.ts
CHANGED
@@ -2,13 +2,13 @@
|
|
2 |
|
3 |
import { isAuthenticated } from "@/lib/auth";
|
4 |
import { NextResponse } from "next/server";
|
5 |
-
import
|
6 |
-
import
|
|
|
7 |
|
8 |
export async function getProjects(): Promise<{
|
9 |
ok: boolean;
|
10 |
projects: ProjectType[];
|
11 |
-
isEmpty?: boolean;
|
12 |
}> {
|
13 |
const user = await isAuthenticated();
|
14 |
|
@@ -19,29 +19,45 @@ export async function getProjects(): Promise<{
|
|
19 |
};
|
20 |
}
|
21 |
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
(
|
35 |
-
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
|
36 |
-
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
|
37 |
-
)
|
38 |
-
) {
|
39 |
-
projects.push(space);
|
40 |
-
}
|
41 |
}
|
42 |
-
|
43 |
return {
|
44 |
ok: true,
|
45 |
-
projects,
|
46 |
};
|
47 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
import { isAuthenticated } from "@/lib/auth";
|
4 |
import { NextResponse } from "next/server";
|
5 |
+
import dbConnect from "@/lib/mongodb";
|
6 |
+
import Project from "@/models/Project";
|
7 |
+
import { Project as ProjectType } from "@/types";
|
8 |
|
9 |
export async function getProjects(): Promise<{
|
10 |
ok: boolean;
|
11 |
projects: ProjectType[];
|
|
|
12 |
}> {
|
13 |
const user = await isAuthenticated();
|
14 |
|
|
|
19 |
};
|
20 |
}
|
21 |
|
22 |
+
await dbConnect();
|
23 |
+
const projects = await Project.find({
|
24 |
+
user_id: user?.id,
|
25 |
+
})
|
26 |
+
.sort({ _createdAt: -1 })
|
27 |
+
.limit(100)
|
28 |
+
.lean();
|
29 |
+
if (!projects) {
|
30 |
+
return {
|
31 |
+
ok: false,
|
32 |
+
projects: [],
|
33 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
}
|
|
|
35 |
return {
|
36 |
ok: true,
|
37 |
+
projects: JSON.parse(JSON.stringify(projects)) as ProjectType[],
|
38 |
};
|
39 |
}
|
40 |
+
|
41 |
+
export async function getProject(
|
42 |
+
namespace: string,
|
43 |
+
repoId: string
|
44 |
+
): Promise<ProjectType | null> {
|
45 |
+
const user = await isAuthenticated();
|
46 |
+
|
47 |
+
if (user instanceof NextResponse || !user) {
|
48 |
+
return null;
|
49 |
+
}
|
50 |
+
|
51 |
+
await dbConnect();
|
52 |
+
const project = await Project.findOne({
|
53 |
+
user_id: user.id,
|
54 |
+
namespace,
|
55 |
+
repoId,
|
56 |
+
}).lean();
|
57 |
+
|
58 |
+
if (!project) {
|
59 |
+
return null;
|
60 |
+
}
|
61 |
+
|
62 |
+
return JSON.parse(JSON.stringify(project)) as ProjectType;
|
63 |
+
}
|
app/api/{ask → ask-ai}/route.ts
RENAMED
@@ -4,29 +4,16 @@ import { NextResponse } from "next/server";
|
|
4 |
import { headers } from "next/headers";
|
5 |
import { InferenceClient } from "@huggingface/inference";
|
6 |
|
7 |
-
import { MODELS } from "@/lib/providers";
|
8 |
import {
|
9 |
DIVIDER,
|
10 |
FOLLOW_UP_SYSTEM_PROMPT,
|
11 |
INITIAL_SYSTEM_PROMPT,
|
12 |
MAX_REQUESTS_PER_IP,
|
13 |
-
NEW_PAGE_END,
|
14 |
-
NEW_PAGE_START,
|
15 |
REPLACE_END,
|
16 |
SEARCH_START,
|
17 |
-
UPDATE_PAGE_START,
|
18 |
-
UPDATE_PAGE_END,
|
19 |
-
PROMPT_FOR_PROJECT_NAME,
|
20 |
} from "@/lib/prompts";
|
21 |
-
import { calculateMaxTokens, estimateInputTokens, getProviderSpecificConfig } from "@/lib/max-tokens";
|
22 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
23 |
-
import { Page } from "@/types";
|
24 |
-
import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
|
25 |
-
import { isAuthenticated } from "@/lib/auth";
|
26 |
-
import { getBestProvider } from "@/lib/best-provider";
|
27 |
-
// import { rewritePrompt } from "@/lib/rewrite-prompt";
|
28 |
-
import { COLORS } from "@/lib/utils";
|
29 |
-
import { templates } from "@/lib/templates";
|
30 |
|
31 |
const ipAddresses = new Map();
|
32 |
|
@@ -35,7 +22,7 @@ export async function POST(request: NextRequest) {
|
|
35 |
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
36 |
|
37 |
const body = await request.json();
|
38 |
-
const { prompt, provider, model, redesignMarkdown,
|
39 |
|
40 |
if (!model || (!prompt && !redesignMarkdown)) {
|
41 |
return NextResponse.json(
|
@@ -47,7 +34,6 @@ export async function POST(request: NextRequest) {
|
|
47 |
const selectedModel = MODELS.find(
|
48 |
(m) => m.value === model || m.label === model
|
49 |
);
|
50 |
-
|
51 |
if (!selectedModel) {
|
52 |
return NextResponse.json(
|
53 |
{ ok: false, error: "Invalid model selected" },
|
@@ -66,8 +52,7 @@ export async function POST(request: NextRequest) {
|
|
66 |
);
|
67 |
}
|
68 |
|
69 |
-
let token
|
70 |
-
if (userToken) token = userToken;
|
71 |
let billTo: string | null = null;
|
72 |
|
73 |
/**
|
@@ -100,19 +85,19 @@ export async function POST(request: NextRequest) {
|
|
100 |
billTo = "huggingface";
|
101 |
}
|
102 |
|
103 |
-
const
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
// rewrittenPrompt = await rewritePrompt(rewrittenPrompt, enhancedSettings, { token, billTo }, selectedModel.value, selectedProvider.provider);
|
109 |
-
}
|
110 |
|
111 |
try {
|
|
|
112 |
const encoder = new TextEncoder();
|
113 |
const stream = new TransformStream();
|
114 |
const writer = stream.writable.getWriter();
|
115 |
|
|
|
116 |
const response = new NextResponse(stream.readable, {
|
117 |
headers: {
|
118 |
"Content-Type": "text/plain; charset=utf-8",
|
@@ -122,52 +107,75 @@ export async function POST(request: NextRequest) {
|
|
122 |
});
|
123 |
|
124 |
(async () => {
|
125 |
-
|
126 |
try {
|
127 |
const client = new InferenceClient(token);
|
128 |
-
|
129 |
-
const systemPrompt = INITIAL_SYSTEM_PROMPT;
|
130 |
-
|
131 |
-
const userPrompt = rewrittenPrompt;
|
132 |
-
const estimatedInputTokens = estimateInputTokens(systemPrompt, userPrompt);
|
133 |
-
const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, true);
|
134 |
-
const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
|
135 |
-
|
136 |
const chatCompletion = client.chatCompletionStream(
|
137 |
{
|
138 |
model: selectedModel.value,
|
139 |
-
provider: selectedProvider.
|
140 |
messages: [
|
141 |
{
|
142 |
role: "system",
|
143 |
-
content:
|
144 |
},
|
145 |
{
|
146 |
role: "user",
|
147 |
-
content:
|
148 |
-
|
149 |
-
|
|
|
|
|
150 |
},
|
151 |
],
|
152 |
-
|
153 |
},
|
154 |
billTo ? { billTo } : {}
|
155 |
);
|
156 |
|
157 |
while (true) {
|
158 |
-
const { done, value } = await chatCompletion.next()
|
159 |
if (done) {
|
160 |
break;
|
161 |
}
|
162 |
|
163 |
const chunk = value.choices[0]?.delta?.content;
|
164 |
if (chunk) {
|
165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
166 |
}
|
167 |
}
|
168 |
-
|
169 |
-
// Explicitly close the writer after successful completion
|
170 |
-
await writer.close();
|
171 |
} catch (error: any) {
|
172 |
if (error.message?.includes("exceeded your monthly included credits")) {
|
173 |
await writer.write(
|
@@ -179,18 +187,7 @@ export async function POST(request: NextRequest) {
|
|
179 |
})
|
180 |
)
|
181 |
);
|
182 |
-
} else
|
183 |
-
await writer.write(
|
184 |
-
encoder.encode(
|
185 |
-
JSON.stringify({
|
186 |
-
ok: false,
|
187 |
-
openSelectProvider: true,
|
188 |
-
message: error.message,
|
189 |
-
})
|
190 |
-
)
|
191 |
-
);
|
192 |
-
}
|
193 |
-
else {
|
194 |
await writer.write(
|
195 |
encoder.encode(
|
196 |
JSON.stringify({
|
@@ -203,12 +200,7 @@ export async function POST(request: NextRequest) {
|
|
203 |
);
|
204 |
}
|
205 |
} finally {
|
206 |
-
|
207 |
-
try {
|
208 |
-
await writer?.close();
|
209 |
-
} catch {
|
210 |
-
// Ignore errors when closing the writer as it might already be closed
|
211 |
-
}
|
212 |
}
|
213 |
})();
|
214 |
|
@@ -227,20 +219,14 @@ export async function POST(request: NextRequest) {
|
|
227 |
}
|
228 |
|
229 |
export async function PUT(request: NextRequest) {
|
230 |
-
const user = await isAuthenticated();
|
231 |
-
if (user instanceof NextResponse || !user) {
|
232 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
233 |
-
}
|
234 |
-
|
235 |
const authHeaders = await headers();
|
|
|
236 |
|
237 |
const body = await request.json();
|
238 |
-
const { prompt,
|
239 |
body;
|
240 |
|
241 |
-
|
242 |
-
|
243 |
-
if (!prompt || pages.length === 0) {
|
244 |
return NextResponse.json(
|
245 |
{ ok: false, error: "Missing required fields" },
|
246 |
{ status: 400 }
|
@@ -257,7 +243,7 @@ export async function PUT(request: NextRequest) {
|
|
257 |
);
|
258 |
}
|
259 |
|
260 |
-
let token =
|
261 |
let billTo: string | null = null;
|
262 |
|
263 |
/**
|
@@ -292,121 +278,52 @@ export async function PUT(request: NextRequest) {
|
|
292 |
|
293 |
const client = new InferenceClient(token);
|
294 |
|
295 |
-
|
296 |
-
const
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
// Helper function to create flexible HTML regex that handles varying spaces
|
301 |
-
const createFlexibleHtmlRegex = (searchBlock: string) => {
|
302 |
-
let searchRegex = escapeRegExp(searchBlock)
|
303 |
-
.replace(/\s+/g, '\\s*') // Allow any amount of whitespace where there are spaces
|
304 |
-
.replace(/>\s*</g, '>\\s*<') // Allow spaces between HTML tags
|
305 |
-
.replace(/\s*>/g, '\\s*>'); // Allow spaces before closing >
|
306 |
-
|
307 |
-
return new RegExp(searchRegex, 'g');
|
308 |
-
};
|
309 |
-
|
310 |
-
const selectedProvider = await getBestProvider(selectedModel.value, provider)
|
311 |
|
312 |
try {
|
313 |
-
|
314 |
-
const systemPrompt = FOLLOW_UP_SYSTEM_PROMPT + (isNew ? PROMPT_FOR_PROJECT_NAME : "");
|
315 |
-
const userContext = "You are modifying the HTML file based on the user's request.";
|
316 |
-
|
317 |
-
// Helper function to get only the most relevant pages based on prompt
|
318 |
-
const getRelevantPages = (pages: Page[], prompt: string, maxPages: number = 2): Page[] => {
|
319 |
-
if (pages.length <= maxPages) return pages;
|
320 |
-
|
321 |
-
// Always include index/main page
|
322 |
-
const indexPage = pages.find(p => p.path === '/' || p.path === '/index' || p.path === 'index');
|
323 |
-
const otherPages = pages.filter(p => p !== indexPage);
|
324 |
-
|
325 |
-
// If we have a selected element, only include pages that might contain it
|
326 |
-
if (selectedElementHtml) {
|
327 |
-
const elementKeywords = selectedElementHtml.toLowerCase().match(/class=["']([^"']*)["']|id=["']([^"']*)["']/g) || [];
|
328 |
-
const relevantPages = otherPages.filter(page => {
|
329 |
-
const pageContent = page.html.toLowerCase();
|
330 |
-
return elementKeywords.some((keyword: string) => pageContent.includes(keyword.toLowerCase()));
|
331 |
-
});
|
332 |
-
|
333 |
-
return indexPage ? [indexPage, ...relevantPages.slice(0, maxPages - 1)] : relevantPages.slice(0, maxPages);
|
334 |
-
}
|
335 |
-
|
336 |
-
// Score pages based on keyword matches from prompt
|
337 |
-
const keywords = prompt.toLowerCase().split(/\s+/).filter(word => word.length > 3);
|
338 |
-
const scoredPages = otherPages.map(page => {
|
339 |
-
const pageContent = (page.path + ' ' + page.html).toLowerCase();
|
340 |
-
const score = keywords.reduce((acc, keyword) => {
|
341 |
-
return acc + (pageContent.includes(keyword) ? 1 : 0);
|
342 |
-
}, 0);
|
343 |
-
return { page, score };
|
344 |
-
});
|
345 |
-
|
346 |
-
// Sort by relevance and take top pages
|
347 |
-
const topPages = scoredPages
|
348 |
-
.sort((a, b) => b.score - a.score)
|
349 |
-
.slice(0, maxPages - (indexPage ? 1 : 0))
|
350 |
-
.map(item => item.page);
|
351 |
-
|
352 |
-
return indexPage ? [indexPage, ...topPages] : topPages;
|
353 |
-
};
|
354 |
-
|
355 |
-
// Get only the most relevant pages - provide FULL HTML for accurate replacements
|
356 |
-
const relevantPages = getRelevantPages(pages || [], prompt);
|
357 |
-
const pagesContext = relevantPages
|
358 |
-
.map((p: Page) => `- ${p.path}\n${p.html}`)
|
359 |
-
.join("\n\n");
|
360 |
-
|
361 |
-
const assistantContext = `${
|
362 |
-
selectedElementHtml
|
363 |
-
? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\` Could be in multiple pages, if so, update all the pages.`
|
364 |
-
: ""
|
365 |
-
}. Current pages (${relevantPages.length}/${pages?.length || 0} shown): ${pagesContext}. ${files?.length > 0 ? `Available images: ${files?.map((f: string) => f.split('/').pop()).join(', ')}.` : ""}`;
|
366 |
-
|
367 |
-
const estimatedInputTokens = estimateInputTokens(systemPrompt, prompt, userContext + assistantContext);
|
368 |
-
const dynamicMaxTokens = calculateMaxTokens(selectedProvider, estimatedInputTokens, false);
|
369 |
-
const providerConfig = getProviderSpecificConfig(selectedProvider, dynamicMaxTokens);
|
370 |
-
|
371 |
-
const chatCompletion = client.chatCompletionStream(
|
372 |
{
|
373 |
model: selectedModel.value,
|
374 |
-
provider: selectedProvider.
|
375 |
messages: [
|
376 |
{
|
377 |
role: "system",
|
378 |
-
content:
|
379 |
},
|
380 |
{
|
381 |
role: "user",
|
382 |
-
content:
|
|
|
|
|
383 |
},
|
384 |
{
|
385 |
role: "assistant",
|
386 |
-
|
|
|
|
|
|
|
|
|
|
|
387 |
},
|
388 |
{
|
389 |
role: "user",
|
390 |
content: prompt,
|
391 |
},
|
392 |
],
|
393 |
-
...
|
|
|
|
|
|
|
|
|
394 |
},
|
395 |
billTo ? { billTo } : {}
|
396 |
);
|
397 |
|
398 |
-
|
399 |
-
while (true) {
|
400 |
-
const { done, value } = await chatCompletion.next();
|
401 |
-
if (done) {
|
402 |
-
break;
|
403 |
-
}
|
404 |
-
|
405 |
-
const deltaContent = value.choices[0]?.delta?.content;
|
406 |
-
if (deltaContent) {
|
407 |
-
chunk += deltaContent;
|
408 |
-
}
|
409 |
-
}
|
410 |
if (!chunk) {
|
411 |
return NextResponse.json(
|
412 |
{ ok: false, message: "No content returned from the model" },
|
@@ -416,234 +333,61 @@ export async function PUT(request: NextRequest) {
|
|
416 |
|
417 |
if (chunk) {
|
418 |
const updatedLines: number[][] = [];
|
419 |
-
let newHtml =
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
const pageIndex = updatedPages.findIndex(p => p.path === pagePath);
|
429 |
-
if (pageIndex !== -1) {
|
430 |
-
let pageHtml = updatedPages[pageIndex].html;
|
431 |
-
|
432 |
-
let processedContent = pageContent;
|
433 |
-
const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
|
434 |
-
if (htmlMatch) {
|
435 |
-
processedContent = htmlMatch[1];
|
436 |
-
}
|
437 |
-
let position = 0;
|
438 |
-
let moreBlocks = true;
|
439 |
-
|
440 |
-
while (moreBlocks) {
|
441 |
-
const searchStartIndex = processedContent.indexOf(SEARCH_START, position);
|
442 |
-
if (searchStartIndex === -1) {
|
443 |
-
moreBlocks = false;
|
444 |
-
continue;
|
445 |
-
}
|
446 |
-
|
447 |
-
const dividerIndex = processedContent.indexOf(DIVIDER, searchStartIndex);
|
448 |
-
if (dividerIndex === -1) {
|
449 |
-
moreBlocks = false;
|
450 |
-
continue;
|
451 |
-
}
|
452 |
-
|
453 |
-
const replaceEndIndex = processedContent.indexOf(REPLACE_END, dividerIndex);
|
454 |
-
if (replaceEndIndex === -1) {
|
455 |
-
moreBlocks = false;
|
456 |
-
continue;
|
457 |
-
}
|
458 |
-
|
459 |
-
const searchBlock = processedContent.substring(
|
460 |
-
searchStartIndex + SEARCH_START.length,
|
461 |
-
dividerIndex
|
462 |
-
);
|
463 |
-
const replaceBlock = processedContent.substring(
|
464 |
-
dividerIndex + DIVIDER.length,
|
465 |
-
replaceEndIndex
|
466 |
-
);
|
467 |
-
|
468 |
-
if (searchBlock.trim() === "") {
|
469 |
-
pageHtml = `${replaceBlock}\n${pageHtml}`;
|
470 |
-
updatedLines.push([1, replaceBlock.split("\n").length]);
|
471 |
-
} else {
|
472 |
-
const regex = createFlexibleHtmlRegex(searchBlock);
|
473 |
-
const match = regex.exec(pageHtml);
|
474 |
-
|
475 |
-
if (match) {
|
476 |
-
const matchedText = match[0];
|
477 |
-
const beforeText = pageHtml.substring(0, match.index);
|
478 |
-
const startLineNumber = beforeText.split("\n").length;
|
479 |
-
const replaceLines = replaceBlock.split("\n").length;
|
480 |
-
const endLineNumber = startLineNumber + replaceLines - 1;
|
481 |
-
|
482 |
-
updatedLines.push([startLineNumber, endLineNumber]);
|
483 |
-
pageHtml = pageHtml.replace(matchedText, replaceBlock);
|
484 |
-
}
|
485 |
-
}
|
486 |
-
|
487 |
-
position = replaceEndIndex + REPLACE_END.length;
|
488 |
-
}
|
489 |
-
|
490 |
-
updatedPages[pageIndex].html = pageHtml;
|
491 |
-
|
492 |
-
if (pagePath === '/' || pagePath === '/index' || pagePath === 'index') {
|
493 |
-
newHtml = pageHtml;
|
494 |
-
}
|
495 |
}
|
496 |
-
}
|
497 |
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
const [, pagePath, pageContent] = newPageMatch;
|
503 |
-
|
504 |
-
let pageHtml = pageContent;
|
505 |
-
const htmlMatch = pageContent.match(/```html\s*([\s\S]*?)\s*```/);
|
506 |
-
if (htmlMatch) {
|
507 |
-
pageHtml = htmlMatch[1];
|
508 |
}
|
509 |
-
|
510 |
-
const existingPageIndex = updatedPages.findIndex(p => p.path === pagePath);
|
511 |
-
|
512 |
-
if (existingPageIndex !== -1) {
|
513 |
-
updatedPages[existingPageIndex] = {
|
514 |
-
path: pagePath,
|
515 |
-
html: pageHtml.trim()
|
516 |
-
};
|
517 |
-
} else {
|
518 |
-
updatedPages.push({
|
519 |
-
path: pagePath,
|
520 |
-
html: pageHtml.trim()
|
521 |
-
});
|
522 |
-
}
|
523 |
-
}
|
524 |
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
|
529 |
-
|
530 |
-
const searchStartIndex = chunk.indexOf(SEARCH_START, position);
|
531 |
-
if (searchStartIndex === -1) {
|
532 |
-
moreBlocks = false;
|
533 |
-
continue;
|
534 |
-
}
|
535 |
-
|
536 |
-
const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
|
537 |
-
if (dividerIndex === -1) {
|
538 |
-
moreBlocks = false;
|
539 |
-
continue;
|
540 |
-
}
|
541 |
-
|
542 |
-
const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
|
543 |
-
if (replaceEndIndex === -1) {
|
544 |
-
moreBlocks = false;
|
545 |
-
continue;
|
546 |
-
}
|
547 |
|
548 |
-
|
549 |
-
|
550 |
-
|
551 |
-
|
552 |
-
|
553 |
-
|
554 |
-
|
555 |
-
|
556 |
|
557 |
-
|
558 |
-
|
559 |
-
|
560 |
-
|
561 |
-
|
562 |
-
|
563 |
-
|
564 |
-
|
565 |
-
|
566 |
-
|
567 |
-
|
568 |
-
|
569 |
-
|
570 |
-
|
571 |
-
updatedLines.push([startLineNumber, endLineNumber]);
|
572 |
-
newHtml = newHtml.replace(matchedText, replaceBlock);
|
573 |
-
}
|
574 |
}
|
575 |
-
|
576 |
-
position = replaceEndIndex + REPLACE_END.length;
|
577 |
-
}
|
578 |
-
|
579 |
-
// Update the main HTML if it's the index page
|
580 |
-
const mainPageIndex = updatedPages.findIndex(p => p.path === '/' || p.path === '/index' || p.path === 'index');
|
581 |
-
if (mainPageIndex !== -1) {
|
582 |
-
updatedPages[mainPageIndex].html = newHtml;
|
583 |
}
|
584 |
-
}
|
585 |
|
586 |
-
|
587 |
-
updatedPages.forEach((page: Page) => {
|
588 |
-
const file = new File([page.html], page.path, { type: "text/html" });
|
589 |
-
files.push(file);
|
590 |
-
});
|
591 |
-
|
592 |
-
if (isNew) {
|
593 |
-
const projectName = chunk.match(/<<<<<<< PROJECT_NAME_START ([\s\S]*?) >>>>>>> PROJECT_NAME_END/)?.[1]?.trim();
|
594 |
-
const formattedTitle = projectName?.toLowerCase()
|
595 |
-
.replace(/[^a-z0-9]+/g, "-")
|
596 |
-
.split("-")
|
597 |
-
.filter(Boolean)
|
598 |
-
.join("-")
|
599 |
-
.slice(0, 96);
|
600 |
-
const repo: RepoDesignation = {
|
601 |
-
type: "space",
|
602 |
-
name: `${user.name}/${formattedTitle}`,
|
603 |
-
};
|
604 |
-
const { repoUrl} = await createRepo({
|
605 |
-
repo,
|
606 |
-
accessToken: user.token as string,
|
607 |
-
});
|
608 |
-
repoId = repoUrl.split("/").slice(-2).join("/");
|
609 |
-
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
610 |
-
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
611 |
-
const README = `---
|
612 |
-
title: ${projectName}
|
613 |
-
colorFrom: ${colorFrom}
|
614 |
-
colorTo: ${colorTo}
|
615 |
-
emoji: 🐳
|
616 |
-
sdk: static
|
617 |
-
pinned: false
|
618 |
-
tags:
|
619 |
-
- deepsite-v3
|
620 |
-
---
|
621 |
-
|
622 |
-
# Welcome to your new DeepSite project!
|
623 |
-
This project was created with [DeepSite](https://deepsite.hf.co).
|
624 |
-
`;
|
625 |
-
files.push(new File([README], "README.md", { type: "text/markdown" }));
|
626 |
}
|
627 |
|
628 |
-
const response = await uploadFiles({
|
629 |
-
repo: {
|
630 |
-
type: "space",
|
631 |
-
name: repoId,
|
632 |
-
},
|
633 |
-
files,
|
634 |
-
commitTitle: prompt,
|
635 |
-
accessToken: user.token as string,
|
636 |
-
});
|
637 |
-
|
638 |
return NextResponse.json({
|
639 |
ok: true,
|
|
|
640 |
updatedLines,
|
641 |
-
pages: updatedPages,
|
642 |
-
repoId,
|
643 |
-
commit: {
|
644 |
-
...response.commit,
|
645 |
-
title: prompt,
|
646 |
-
}
|
647 |
});
|
648 |
} else {
|
649 |
return NextResponse.json(
|
@@ -673,4 +417,3 @@ This project was created with [DeepSite](https://deepsite.hf.co).
|
|
673 |
);
|
674 |
}
|
675 |
}
|
676 |
-
|
|
|
4 |
import { headers } from "next/headers";
|
5 |
import { InferenceClient } from "@huggingface/inference";
|
6 |
|
7 |
+
import { MODELS, PROVIDERS } from "@/lib/providers";
|
8 |
import {
|
9 |
DIVIDER,
|
10 |
FOLLOW_UP_SYSTEM_PROMPT,
|
11 |
INITIAL_SYSTEM_PROMPT,
|
12 |
MAX_REQUESTS_PER_IP,
|
|
|
|
|
13 |
REPLACE_END,
|
14 |
SEARCH_START,
|
|
|
|
|
|
|
15 |
} from "@/lib/prompts";
|
|
|
16 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
const ipAddresses = new Map();
|
19 |
|
|
|
22 |
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
23 |
|
24 |
const body = await request.json();
|
25 |
+
const { prompt, provider, model, redesignMarkdown, html } = body;
|
26 |
|
27 |
if (!model || (!prompt && !redesignMarkdown)) {
|
28 |
return NextResponse.json(
|
|
|
34 |
const selectedModel = MODELS.find(
|
35 |
(m) => m.value === model || m.label === model
|
36 |
);
|
|
|
37 |
if (!selectedModel) {
|
38 |
return NextResponse.json(
|
39 |
{ ok: false, error: "Invalid model selected" },
|
|
|
52 |
);
|
53 |
}
|
54 |
|
55 |
+
let token = userToken;
|
|
|
56 |
let billTo: string | null = null;
|
57 |
|
58 |
/**
|
|
|
85 |
billTo = "huggingface";
|
86 |
}
|
87 |
|
88 |
+
const DEFAULT_PROVIDER = PROVIDERS.novita;
|
89 |
+
const selectedProvider =
|
90 |
+
provider === "auto"
|
91 |
+
? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
|
92 |
+
: PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
|
|
|
|
|
93 |
|
94 |
try {
|
95 |
+
// Create a stream response
|
96 |
const encoder = new TextEncoder();
|
97 |
const stream = new TransformStream();
|
98 |
const writer = stream.writable.getWriter();
|
99 |
|
100 |
+
// Start the response
|
101 |
const response = new NextResponse(stream.readable, {
|
102 |
headers: {
|
103 |
"Content-Type": "text/plain; charset=utf-8",
|
|
|
107 |
});
|
108 |
|
109 |
(async () => {
|
110 |
+
let completeResponse = "";
|
111 |
try {
|
112 |
const client = new InferenceClient(token);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
113 |
const chatCompletion = client.chatCompletionStream(
|
114 |
{
|
115 |
model: selectedModel.value,
|
116 |
+
provider: selectedProvider.id as any,
|
117 |
messages: [
|
118 |
{
|
119 |
role: "system",
|
120 |
+
content: INITIAL_SYSTEM_PROMPT,
|
121 |
},
|
122 |
{
|
123 |
role: "user",
|
124 |
+
content: redesignMarkdown
|
125 |
+
? `Here is my current design as a markdown:\n\n${redesignMarkdown}\n\nNow, please create a new design based on this markdown.`
|
126 |
+
: html
|
127 |
+
? `Here is my current HTML code:\n\n\`\`\`html\n${html}\n\`\`\`\n\nNow, please create a new design based on this HTML.`
|
128 |
+
: prompt,
|
129 |
},
|
130 |
],
|
131 |
+
max_tokens: selectedProvider.max_tokens,
|
132 |
},
|
133 |
billTo ? { billTo } : {}
|
134 |
);
|
135 |
|
136 |
while (true) {
|
137 |
+
const { done, value } = await chatCompletion.next();
|
138 |
if (done) {
|
139 |
break;
|
140 |
}
|
141 |
|
142 |
const chunk = value.choices[0]?.delta?.content;
|
143 |
if (chunk) {
|
144 |
+
let newChunk = chunk;
|
145 |
+
if (!selectedModel?.isThinker) {
|
146 |
+
if (provider !== "sambanova") {
|
147 |
+
await writer.write(encoder.encode(chunk));
|
148 |
+
completeResponse += chunk;
|
149 |
+
|
150 |
+
if (completeResponse.includes("</html>")) {
|
151 |
+
break;
|
152 |
+
}
|
153 |
+
} else {
|
154 |
+
if (chunk.includes("</html>")) {
|
155 |
+
newChunk = newChunk.replace(/<\/html>[\s\S]*/, "</html>");
|
156 |
+
}
|
157 |
+
completeResponse += newChunk;
|
158 |
+
await writer.write(encoder.encode(newChunk));
|
159 |
+
if (newChunk.includes("</html>")) {
|
160 |
+
break;
|
161 |
+
}
|
162 |
+
}
|
163 |
+
} else {
|
164 |
+
const lastThinkTagIndex =
|
165 |
+
completeResponse.lastIndexOf("</think>");
|
166 |
+
completeResponse += newChunk;
|
167 |
+
await writer.write(encoder.encode(newChunk));
|
168 |
+
if (lastThinkTagIndex !== -1) {
|
169 |
+
const afterLastThinkTag = completeResponse.slice(
|
170 |
+
lastThinkTagIndex + "</think>".length
|
171 |
+
);
|
172 |
+
if (afterLastThinkTag.includes("</html>")) {
|
173 |
+
break;
|
174 |
+
}
|
175 |
+
}
|
176 |
+
}
|
177 |
}
|
178 |
}
|
|
|
|
|
|
|
179 |
} catch (error: any) {
|
180 |
if (error.message?.includes("exceeded your monthly included credits")) {
|
181 |
await writer.write(
|
|
|
187 |
})
|
188 |
)
|
189 |
);
|
190 |
+
} else {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
191 |
await writer.write(
|
192 |
encoder.encode(
|
193 |
JSON.stringify({
|
|
|
200 |
);
|
201 |
}
|
202 |
} finally {
|
203 |
+
await writer?.close();
|
|
|
|
|
|
|
|
|
|
|
204 |
}
|
205 |
})();
|
206 |
|
|
|
219 |
}
|
220 |
|
221 |
export async function PUT(request: NextRequest) {
|
|
|
|
|
|
|
|
|
|
|
222 |
const authHeaders = await headers();
|
223 |
+
const userToken = request.cookies.get(MY_TOKEN_KEY())?.value;
|
224 |
|
225 |
const body = await request.json();
|
226 |
+
const { prompt, html, previousPrompt, provider, selectedElementHtml, model } =
|
227 |
body;
|
228 |
|
229 |
+
if (!prompt || !html) {
|
|
|
|
|
230 |
return NextResponse.json(
|
231 |
{ ok: false, error: "Missing required fields" },
|
232 |
{ status: 400 }
|
|
|
243 |
);
|
244 |
}
|
245 |
|
246 |
+
let token = userToken;
|
247 |
let billTo: string | null = null;
|
248 |
|
249 |
/**
|
|
|
278 |
|
279 |
const client = new InferenceClient(token);
|
280 |
|
281 |
+
const DEFAULT_PROVIDER = PROVIDERS.novita;
|
282 |
+
const selectedProvider =
|
283 |
+
provider === "auto"
|
284 |
+
? PROVIDERS[selectedModel.autoProvider as keyof typeof PROVIDERS]
|
285 |
+
: PROVIDERS[provider as keyof typeof PROVIDERS] ?? DEFAULT_PROVIDER;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
286 |
|
287 |
try {
|
288 |
+
const response = await client.chatCompletion(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
289 |
{
|
290 |
model: selectedModel.value,
|
291 |
+
provider: selectedProvider.id as any,
|
292 |
messages: [
|
293 |
{
|
294 |
role: "system",
|
295 |
+
content: FOLLOW_UP_SYSTEM_PROMPT,
|
296 |
},
|
297 |
{
|
298 |
role: "user",
|
299 |
+
content: previousPrompt
|
300 |
+
? previousPrompt
|
301 |
+
: "You are modifying the HTML file based on the user's request.",
|
302 |
},
|
303 |
{
|
304 |
role: "assistant",
|
305 |
+
|
306 |
+
content: `The current code is: \n\`\`\`html\n${html}\n\`\`\` ${
|
307 |
+
selectedElementHtml
|
308 |
+
? `\n\nYou have to update ONLY the following element, NOTHING ELSE: \n\n\`\`\`html\n${selectedElementHtml}\n\`\`\``
|
309 |
+
: ""
|
310 |
+
}`,
|
311 |
},
|
312 |
{
|
313 |
role: "user",
|
314 |
content: prompt,
|
315 |
},
|
316 |
],
|
317 |
+
...(selectedProvider.id !== "sambanova"
|
318 |
+
? {
|
319 |
+
max_tokens: selectedProvider.max_tokens,
|
320 |
+
}
|
321 |
+
: {}),
|
322 |
},
|
323 |
billTo ? { billTo } : {}
|
324 |
);
|
325 |
|
326 |
+
const chunk = response.choices[0]?.message?.content;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
327 |
if (!chunk) {
|
328 |
return NextResponse.json(
|
329 |
{ ok: false, message: "No content returned from the model" },
|
|
|
333 |
|
334 |
if (chunk) {
|
335 |
const updatedLines: number[][] = [];
|
336 |
+
let newHtml = html;
|
337 |
+
let position = 0;
|
338 |
+
let moreBlocks = true;
|
339 |
+
|
340 |
+
while (moreBlocks) {
|
341 |
+
const searchStartIndex = chunk.indexOf(SEARCH_START, position);
|
342 |
+
if (searchStartIndex === -1) {
|
343 |
+
moreBlocks = false;
|
344 |
+
continue;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
345 |
}
|
|
|
346 |
|
347 |
+
const dividerIndex = chunk.indexOf(DIVIDER, searchStartIndex);
|
348 |
+
if (dividerIndex === -1) {
|
349 |
+
moreBlocks = false;
|
350 |
+
continue;
|
|
|
|
|
|
|
|
|
|
|
|
|
351 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
352 |
|
353 |
+
const replaceEndIndex = chunk.indexOf(REPLACE_END, dividerIndex);
|
354 |
+
if (replaceEndIndex === -1) {
|
355 |
+
moreBlocks = false;
|
356 |
+
continue;
|
357 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
358 |
|
359 |
+
const searchBlock = chunk.substring(
|
360 |
+
searchStartIndex + SEARCH_START.length,
|
361 |
+
dividerIndex
|
362 |
+
);
|
363 |
+
const replaceBlock = chunk.substring(
|
364 |
+
dividerIndex + DIVIDER.length,
|
365 |
+
replaceEndIndex
|
366 |
+
);
|
367 |
|
368 |
+
if (searchBlock.trim() === "") {
|
369 |
+
newHtml = `${replaceBlock}\n${newHtml}`;
|
370 |
+
updatedLines.push([1, replaceBlock.split("\n").length]);
|
371 |
+
} else {
|
372 |
+
const blockPosition = newHtml.indexOf(searchBlock);
|
373 |
+
if (blockPosition !== -1) {
|
374 |
+
const beforeText = newHtml.substring(0, blockPosition);
|
375 |
+
const startLineNumber = beforeText.split("\n").length;
|
376 |
+
const replaceLines = replaceBlock.split("\n").length;
|
377 |
+
const endLineNumber = startLineNumber + replaceLines - 1;
|
378 |
+
|
379 |
+
updatedLines.push([startLineNumber, endLineNumber]);
|
380 |
+
newHtml = newHtml.replace(searchBlock, replaceBlock);
|
|
|
|
|
|
|
|
|
381 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
382 |
}
|
|
|
383 |
|
384 |
+
position = replaceEndIndex + REPLACE_END.length;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
385 |
}
|
386 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
387 |
return NextResponse.json({
|
388 |
ok: true,
|
389 |
+
html: newHtml,
|
390 |
updatedLines,
|
|
|
|
|
|
|
|
|
|
|
|
|
391 |
});
|
392 |
} else {
|
393 |
return NextResponse.json(
|
|
|
417 |
);
|
418 |
}
|
419 |
}
|
|
app/api/auth/login-url/route.ts
DELETED
@@ -1,23 +0,0 @@
|
|
1 |
-
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
|
3 |
-
export async function GET(req: NextRequest) {
|
4 |
-
const host = req.headers.get("host") ?? "localhost:3000";
|
5 |
-
|
6 |
-
let url: string;
|
7 |
-
if (host.includes("localhost")) {
|
8 |
-
url = host;
|
9 |
-
} else if (host.includes("hf.space") || host.includes("/spaces/enzostvs")) {
|
10 |
-
url = "enzostvs-deepsite.hf.space";
|
11 |
-
} else {
|
12 |
-
url = "deepsite.hf.co";
|
13 |
-
}
|
14 |
-
|
15 |
-
const redirect_uri =
|
16 |
-
`${host.includes("localhost") ? "http://" : "https://"}` +
|
17 |
-
url +
|
18 |
-
"/auth/callback";
|
19 |
-
|
20 |
-
const loginRedirectUrl = `https://huggingface.co/oauth/authorize?client_id=${process.env.OAUTH_CLIENT_ID}&redirect_uri=${redirect_uri}&response_type=code&scope=openid%20profile%20write-repos%20manage-repos%20inference-api&prompt=consent&state=1234567890`;
|
21 |
-
|
22 |
-
return NextResponse.json({ loginUrl: loginRedirectUrl });
|
23 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/auth/logout/route.ts
DELETED
@@ -1,25 +0,0 @@
|
|
1 |
-
import { NextResponse } from "next/server";
|
2 |
-
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
3 |
-
|
4 |
-
export async function POST() {
|
5 |
-
const cookieName = MY_TOKEN_KEY();
|
6 |
-
const isProduction = process.env.NODE_ENV === "production";
|
7 |
-
|
8 |
-
const response = NextResponse.json(
|
9 |
-
{ message: "Logged out successfully" },
|
10 |
-
{ status: 200 }
|
11 |
-
);
|
12 |
-
|
13 |
-
// Clear the HTTP-only cookie
|
14 |
-
const cookieOptions = [
|
15 |
-
`${cookieName}=`,
|
16 |
-
"Max-Age=0",
|
17 |
-
"Path=/",
|
18 |
-
"HttpOnly",
|
19 |
-
...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
|
20 |
-
].join("; ");
|
21 |
-
|
22 |
-
response.headers.set("Set-Cookie", cookieOptions);
|
23 |
-
|
24 |
-
return response;
|
25 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/auth/route.ts
CHANGED
@@ -1,5 +1,4 @@
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
3 |
|
4 |
export async function POST(req: NextRequest) {
|
5 |
const body = await req.json();
|
@@ -71,17 +70,11 @@ export async function POST(req: NextRequest) {
|
|
71 |
}
|
72 |
const user = await userResponse.json();
|
73 |
|
74 |
-
|
75 |
-
const isProduction = process.env.NODE_ENV === "production";
|
76 |
-
|
77 |
-
// Create response with user data
|
78 |
-
const nextResponse = NextResponse.json(
|
79 |
{
|
80 |
access_token: response.access_token,
|
81 |
expires_in: response.expires_in,
|
82 |
user,
|
83 |
-
// Include fallback flag for iframe contexts
|
84 |
-
useLocalStorageFallback: true,
|
85 |
},
|
86 |
{
|
87 |
status: 200,
|
@@ -90,17 +83,4 @@ export async function POST(req: NextRequest) {
|
|
90 |
},
|
91 |
}
|
92 |
);
|
93 |
-
|
94 |
-
// Set HTTP-only cookie with proper attributes for iframe support
|
95 |
-
const cookieOptions = [
|
96 |
-
`${cookieName}=${response.access_token}`,
|
97 |
-
`Max-Age=${response.expires_in || 3600}`, // Default 1 hour if not provided
|
98 |
-
"Path=/",
|
99 |
-
"HttpOnly",
|
100 |
-
...(isProduction ? ["Secure", "SameSite=None"] : ["SameSite=Lax"])
|
101 |
-
].join("; ");
|
102 |
-
|
103 |
-
nextResponse.headers.set("Set-Cookie", cookieOptions);
|
104 |
-
|
105 |
-
return nextResponse;
|
106 |
}
|
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
|
|
2 |
|
3 |
export async function POST(req: NextRequest) {
|
4 |
const body = await req.json();
|
|
|
70 |
}
|
71 |
const user = await userResponse.json();
|
72 |
|
73 |
+
return NextResponse.json(
|
|
|
|
|
|
|
|
|
74 |
{
|
75 |
access_token: response.access_token,
|
76 |
expires_in: response.expires_in,
|
77 |
user,
|
|
|
|
|
78 |
},
|
79 |
{
|
80 |
status: 200,
|
|
|
83 |
},
|
84 |
}
|
85 |
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
}
|
app/api/me/projects/[namespace]/[repoId]/commits/[commitId]/promote/route.ts
DELETED
@@ -1,190 +0,0 @@
|
|
1 |
-
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import { RepoDesignation, listFiles, spaceInfo, uploadFiles, deleteFiles } from "@huggingface/hub";
|
3 |
-
|
4 |
-
import { isAuthenticated } from "@/lib/auth";
|
5 |
-
import { Page } from "@/types";
|
6 |
-
|
7 |
-
export async function POST(
|
8 |
-
req: NextRequest,
|
9 |
-
{ params }: {
|
10 |
-
params: Promise<{
|
11 |
-
namespace: string;
|
12 |
-
repoId: string;
|
13 |
-
commitId: string;
|
14 |
-
}>
|
15 |
-
}
|
16 |
-
) {
|
17 |
-
const user = await isAuthenticated();
|
18 |
-
|
19 |
-
if (user instanceof NextResponse || !user) {
|
20 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
21 |
-
}
|
22 |
-
|
23 |
-
const param = await params;
|
24 |
-
const { namespace, repoId, commitId } = param;
|
25 |
-
|
26 |
-
try {
|
27 |
-
const repo: RepoDesignation = {
|
28 |
-
type: "space",
|
29 |
-
name: `${namespace}/${repoId}`,
|
30 |
-
};
|
31 |
-
|
32 |
-
const space = await spaceInfo({
|
33 |
-
name: `${namespace}/${repoId}`,
|
34 |
-
accessToken: user.token as string,
|
35 |
-
additionalFields: ["author"],
|
36 |
-
});
|
37 |
-
|
38 |
-
if (!space || space.sdk !== "static") {
|
39 |
-
return NextResponse.json(
|
40 |
-
{ ok: false, error: "Space is not a static space." },
|
41 |
-
{ status: 404 }
|
42 |
-
);
|
43 |
-
}
|
44 |
-
|
45 |
-
if (space.author !== user.name) {
|
46 |
-
return NextResponse.json(
|
47 |
-
{ ok: false, error: "Space does not belong to the authenticated user." },
|
48 |
-
{ status: 403 }
|
49 |
-
);
|
50 |
-
}
|
51 |
-
|
52 |
-
// Fetch files from the specific commit
|
53 |
-
const files: File[] = [];
|
54 |
-
const pages: Page[] = [];
|
55 |
-
const allowedExtensions = ["html", "md", "css", "js", "json", "txt"];
|
56 |
-
const commitFilePaths: Set<string> = new Set();
|
57 |
-
|
58 |
-
// Get all files from the specific commit
|
59 |
-
for await (const fileInfo of listFiles({
|
60 |
-
repo,
|
61 |
-
accessToken: user.token as string,
|
62 |
-
revision: commitId,
|
63 |
-
})) {
|
64 |
-
const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
|
65 |
-
|
66 |
-
if (allowedExtensions.includes(fileExtension || "")) {
|
67 |
-
commitFilePaths.add(fileInfo.path);
|
68 |
-
|
69 |
-
// Fetch the file content from the specific commit
|
70 |
-
const response = await fetch(
|
71 |
-
`https://huggingface.co/spaces/${namespace}/${repoId}/raw/${commitId}/${fileInfo.path}`
|
72 |
-
);
|
73 |
-
|
74 |
-
if (response.ok) {
|
75 |
-
const content = await response.text();
|
76 |
-
let mimeType = "text/plain";
|
77 |
-
|
78 |
-
switch (fileExtension) {
|
79 |
-
case "html":
|
80 |
-
mimeType = "text/html";
|
81 |
-
// Add HTML files to pages array for client-side setPages
|
82 |
-
pages.push({
|
83 |
-
path: fileInfo.path,
|
84 |
-
html: content,
|
85 |
-
});
|
86 |
-
break;
|
87 |
-
case "css":
|
88 |
-
mimeType = "text/css";
|
89 |
-
break;
|
90 |
-
case "js":
|
91 |
-
mimeType = "application/javascript";
|
92 |
-
break;
|
93 |
-
case "json":
|
94 |
-
mimeType = "application/json";
|
95 |
-
break;
|
96 |
-
case "md":
|
97 |
-
mimeType = "text/markdown";
|
98 |
-
break;
|
99 |
-
}
|
100 |
-
|
101 |
-
const file = new File([content], fileInfo.path, { type: mimeType });
|
102 |
-
files.push(file);
|
103 |
-
}
|
104 |
-
}
|
105 |
-
}
|
106 |
-
|
107 |
-
// Get files currently in main branch to identify files to delete
|
108 |
-
const mainBranchFilePaths: Set<string> = new Set();
|
109 |
-
for await (const fileInfo of listFiles({
|
110 |
-
repo,
|
111 |
-
accessToken: user.token as string,
|
112 |
-
revision: "main",
|
113 |
-
})) {
|
114 |
-
const fileExtension = fileInfo.path.split('.').pop()?.toLowerCase();
|
115 |
-
|
116 |
-
if (allowedExtensions.includes(fileExtension || "")) {
|
117 |
-
mainBranchFilePaths.add(fileInfo.path);
|
118 |
-
}
|
119 |
-
}
|
120 |
-
|
121 |
-
// Identify files to delete (exist in main but not in commit)
|
122 |
-
const filesToDelete: string[] = [];
|
123 |
-
for (const mainFilePath of mainBranchFilePaths) {
|
124 |
-
if (!commitFilePaths.has(mainFilePath)) {
|
125 |
-
filesToDelete.push(mainFilePath);
|
126 |
-
}
|
127 |
-
}
|
128 |
-
|
129 |
-
if (files.length === 0 && filesToDelete.length === 0) {
|
130 |
-
return NextResponse.json(
|
131 |
-
{ ok: false, error: "No files found in the specified commit and no files to delete" },
|
132 |
-
{ status: 404 }
|
133 |
-
);
|
134 |
-
}
|
135 |
-
|
136 |
-
// Delete files that exist in main but not in the commit being promoted
|
137 |
-
if (filesToDelete.length > 0) {
|
138 |
-
await deleteFiles({
|
139 |
-
repo,
|
140 |
-
paths: filesToDelete,
|
141 |
-
accessToken: user.token as string,
|
142 |
-
commitTitle: `Removed files from promoting ${commitId.slice(0, 7)}`,
|
143 |
-
commitDescription: `Removed files that don't exist in commit ${commitId}:\n${filesToDelete.map(path => `- ${path}`).join('\n')}`,
|
144 |
-
});
|
145 |
-
}
|
146 |
-
|
147 |
-
// Upload the files to the main branch with a promotion commit message
|
148 |
-
if (files.length > 0) {
|
149 |
-
await uploadFiles({
|
150 |
-
repo,
|
151 |
-
files,
|
152 |
-
accessToken: user.token as string,
|
153 |
-
commitTitle: `Promote version ${commitId.slice(0, 7)} to main`,
|
154 |
-
commitDescription: `Promoted commit ${commitId} to main branch`,
|
155 |
-
});
|
156 |
-
}
|
157 |
-
|
158 |
-
return NextResponse.json(
|
159 |
-
{
|
160 |
-
ok: true,
|
161 |
-
message: "Version promoted successfully",
|
162 |
-
promotedCommit: commitId,
|
163 |
-
pages: pages,
|
164 |
-
},
|
165 |
-
{ status: 200 }
|
166 |
-
);
|
167 |
-
|
168 |
-
} catch (error: any) {
|
169 |
-
|
170 |
-
// Handle specific HuggingFace API errors
|
171 |
-
if (error.statusCode === 404) {
|
172 |
-
return NextResponse.json(
|
173 |
-
{ ok: false, error: "Commit not found" },
|
174 |
-
{ status: 404 }
|
175 |
-
);
|
176 |
-
}
|
177 |
-
|
178 |
-
if (error.statusCode === 403) {
|
179 |
-
return NextResponse.json(
|
180 |
-
{ ok: false, error: "Access denied to repository" },
|
181 |
-
{ status: 403 }
|
182 |
-
);
|
183 |
-
}
|
184 |
-
|
185 |
-
return NextResponse.json(
|
186 |
-
{ ok: false, error: error.message || "Failed to promote version" },
|
187 |
-
{ status: 500 }
|
188 |
-
);
|
189 |
-
}
|
190 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/me/projects/[namespace]/[repoId]/images/route.ts
DELETED
@@ -1,113 +0,0 @@
|
|
1 |
-
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import { RepoDesignation, spaceInfo, uploadFiles } from "@huggingface/hub";
|
3 |
-
|
4 |
-
import { isAuthenticated } from "@/lib/auth";
|
5 |
-
import Project from "@/models/Project";
|
6 |
-
import dbConnect from "@/lib/mongodb";
|
7 |
-
|
8 |
-
export async function POST(
|
9 |
-
req: NextRequest,
|
10 |
-
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
11 |
-
) {
|
12 |
-
try {
|
13 |
-
const user = await isAuthenticated();
|
14 |
-
|
15 |
-
if (user instanceof NextResponse || !user) {
|
16 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
17 |
-
}
|
18 |
-
|
19 |
-
const param = await params;
|
20 |
-
const { namespace, repoId } = param;
|
21 |
-
|
22 |
-
const space = await spaceInfo({
|
23 |
-
name: `${namespace}/${repoId}`,
|
24 |
-
accessToken: user.token as string,
|
25 |
-
additionalFields: ["author"],
|
26 |
-
});
|
27 |
-
|
28 |
-
if (!space || space.sdk !== "static") {
|
29 |
-
return NextResponse.json(
|
30 |
-
{ ok: false, error: "Space is not a static space." },
|
31 |
-
{ status: 404 }
|
32 |
-
);
|
33 |
-
}
|
34 |
-
|
35 |
-
if (space.author !== user.name) {
|
36 |
-
return NextResponse.json(
|
37 |
-
{ ok: false, error: "Space does not belong to the authenticated user." },
|
38 |
-
{ status: 403 }
|
39 |
-
);
|
40 |
-
}
|
41 |
-
|
42 |
-
// Parse the FormData to get the images
|
43 |
-
const formData = await req.formData();
|
44 |
-
const imageFiles = formData.getAll("images") as File[];
|
45 |
-
|
46 |
-
if (!imageFiles || imageFiles.length === 0) {
|
47 |
-
return NextResponse.json(
|
48 |
-
{
|
49 |
-
ok: false,
|
50 |
-
error: "At least one image file is required under the 'images' key",
|
51 |
-
},
|
52 |
-
{ status: 400 }
|
53 |
-
);
|
54 |
-
}
|
55 |
-
|
56 |
-
const files: File[] = [];
|
57 |
-
for (const file of imageFiles) {
|
58 |
-
if (!(file instanceof File)) {
|
59 |
-
return NextResponse.json(
|
60 |
-
{
|
61 |
-
ok: false,
|
62 |
-
error: "Invalid file format - all items under 'images' key must be files",
|
63 |
-
},
|
64 |
-
{ status: 400 }
|
65 |
-
);
|
66 |
-
}
|
67 |
-
|
68 |
-
if (!file.type.startsWith('image/')) {
|
69 |
-
return NextResponse.json(
|
70 |
-
{
|
71 |
-
ok: false,
|
72 |
-
error: `File ${file.name} is not an image`,
|
73 |
-
},
|
74 |
-
{ status: 400 }
|
75 |
-
);
|
76 |
-
}
|
77 |
-
|
78 |
-
// Create File object with images/ folder prefix
|
79 |
-
const fileName = `images/${file.name}`;
|
80 |
-
const processedFile = new File([file], fileName, { type: file.type });
|
81 |
-
files.push(processedFile);
|
82 |
-
}
|
83 |
-
|
84 |
-
// Upload files to HuggingFace space
|
85 |
-
const repo: RepoDesignation = {
|
86 |
-
type: "space",
|
87 |
-
name: `${namespace}/${repoId}`,
|
88 |
-
};
|
89 |
-
|
90 |
-
await uploadFiles({
|
91 |
-
repo,
|
92 |
-
files,
|
93 |
-
accessToken: user.token as string,
|
94 |
-
commitTitle: `Upload ${files.length} image(s)`,
|
95 |
-
});
|
96 |
-
|
97 |
-
return NextResponse.json({
|
98 |
-
ok: true,
|
99 |
-
message: `Successfully uploaded ${files.length} image(s) to ${namespace}/${repoId}/images/`,
|
100 |
-
uploadedFiles: files.map((file) => `https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${file.name}`),
|
101 |
-
}, { status: 200 });
|
102 |
-
|
103 |
-
} catch (error) {
|
104 |
-
console.error('Error uploading images:', error);
|
105 |
-
return NextResponse.json(
|
106 |
-
{
|
107 |
-
ok: false,
|
108 |
-
error: "Failed to upload images",
|
109 |
-
},
|
110 |
-
{ status: 500 }
|
111 |
-
);
|
112 |
-
}
|
113 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/me/projects/[namespace]/[repoId]/route.ts
CHANGED
@@ -1,10 +1,12 @@
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import { RepoDesignation, spaceInfo,
|
3 |
|
4 |
import { isAuthenticated } from "@/lib/auth";
|
5 |
-
import
|
|
|
|
|
6 |
|
7 |
-
export async function
|
8 |
req: NextRequest,
|
9 |
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
10 |
) {
|
@@ -14,63 +16,24 @@ export async function DELETE(
|
|
14 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
15 |
}
|
16 |
|
|
|
17 |
const param = await params;
|
18 |
const { namespace, repoId } = param;
|
19 |
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
});
|
26 |
-
|
27 |
-
if (!space || space.sdk !== "static") {
|
28 |
-
return NextResponse.json(
|
29 |
-
{ ok: false, error: "Space is not a static space." },
|
30 |
-
{ status: 404 }
|
31 |
-
);
|
32 |
-
}
|
33 |
-
|
34 |
-
if (space.author !== user.name) {
|
35 |
-
return NextResponse.json(
|
36 |
-
{ ok: false, error: "Space does not belong to the authenticated user." },
|
37 |
-
{ status: 403 }
|
38 |
-
);
|
39 |
-
}
|
40 |
-
|
41 |
-
const repo: RepoDesignation = {
|
42 |
-
type: "space",
|
43 |
-
name: `${namespace}/${repoId}`,
|
44 |
-
};
|
45 |
-
|
46 |
-
await deleteRepo({
|
47 |
-
repo,
|
48 |
-
accessToken: user.token as string,
|
49 |
-
});
|
50 |
-
|
51 |
-
|
52 |
-
return NextResponse.json({ ok: true }, { status: 200 });
|
53 |
-
} catch (error: any) {
|
54 |
return NextResponse.json(
|
55 |
-
{
|
56 |
-
|
|
|
|
|
|
|
57 |
);
|
58 |
}
|
59 |
-
}
|
60 |
-
|
61 |
-
export async function GET(
|
62 |
-
req: NextRequest,
|
63 |
-
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
64 |
-
) {
|
65 |
-
const user = await isAuthenticated();
|
66 |
-
|
67 |
-
if (user instanceof NextResponse || !user) {
|
68 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
69 |
-
}
|
70 |
-
|
71 |
-
const param = await params;
|
72 |
-
const { namespace, repoId } = param;
|
73 |
-
|
74 |
try {
|
75 |
const space = await spaceInfo({
|
76 |
name: namespace + "/" + repoId,
|
@@ -97,75 +60,26 @@ export async function GET(
|
|
97 |
);
|
98 |
}
|
99 |
|
100 |
-
const
|
101 |
-
|
102 |
-
name: `${namespace}/${repoId}`,
|
103 |
-
};
|
104 |
-
|
105 |
-
const htmlFiles: Page[] = [];
|
106 |
-
const files: string[] = [];
|
107 |
-
|
108 |
-
const allowedFilesExtensions = ["jpg", "jpeg", "png", "gif", "svg", "webp", "avif", "heic", "heif", "ico", "bmp", "tiff", "tif"];
|
109 |
-
|
110 |
-
for await (const fileInfo of listFiles({repo, accessToken: user.token as string})) {
|
111 |
-
if (fileInfo.path.endsWith(".html")) {
|
112 |
-
const blob = await downloadFile({ repo, accessToken: user.token as string, path: fileInfo.path, raw: true });
|
113 |
-
const html = await blob?.text();
|
114 |
-
if (!html) {
|
115 |
-
continue;
|
116 |
-
}
|
117 |
-
if (fileInfo.path === "index.html") {
|
118 |
-
htmlFiles.unshift({
|
119 |
-
path: fileInfo.path,
|
120 |
-
html,
|
121 |
-
});
|
122 |
-
} else {
|
123 |
-
htmlFiles.push({
|
124 |
-
path: fileInfo.path,
|
125 |
-
html,
|
126 |
-
});
|
127 |
-
}
|
128 |
-
}
|
129 |
-
if (fileInfo.type === "directory" && fileInfo.path === "images") {
|
130 |
-
for await (const imageInfo of listFiles({repo, accessToken: user.token as string, path: fileInfo.path})) {
|
131 |
-
if (allowedFilesExtensions.includes(imageInfo.path.split(".").pop() || "")) {
|
132 |
-
files.push(`https://huggingface.co/spaces/${namespace}/${repoId}/resolve/main/${imageInfo.path}`);
|
133 |
-
}
|
134 |
-
}
|
135 |
-
}
|
136 |
-
}
|
137 |
-
const commits: Commit[] = [];
|
138 |
-
for await (const commit of listCommits({ repo, accessToken: user.token as string })) {
|
139 |
-
if (commit.title.includes("initial commit") || commit.title.includes("image(s)") || commit.title.includes("Removed files from promoting")) {
|
140 |
-
continue;
|
141 |
-
}
|
142 |
-
commits.push({
|
143 |
-
title: commit.title,
|
144 |
-
oid: commit.oid,
|
145 |
-
date: commit.date,
|
146 |
-
});
|
147 |
-
}
|
148 |
-
|
149 |
-
if (htmlFiles.length === 0) {
|
150 |
return NextResponse.json(
|
151 |
{
|
152 |
ok: false,
|
153 |
-
error: "
|
154 |
},
|
155 |
{ status: 404 }
|
156 |
);
|
157 |
}
|
|
|
|
|
|
|
|
|
158 |
return NextResponse.json(
|
159 |
{
|
160 |
project: {
|
161 |
-
|
162 |
-
|
163 |
-
private: space.private,
|
164 |
-
_updatedAt: space.updatedAt,
|
165 |
},
|
166 |
-
pages: htmlFiles,
|
167 |
-
files,
|
168 |
-
commits,
|
169 |
ok: true,
|
170 |
},
|
171 |
{ status: 200 }
|
@@ -174,6 +88,10 @@ export async function GET(
|
|
174 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
175 |
} catch (error: any) {
|
176 |
if (error.statusCode === 404) {
|
|
|
|
|
|
|
|
|
177 |
return NextResponse.json(
|
178 |
{ error: "Space not found", ok: false },
|
179 |
{ status: 404 }
|
@@ -185,3 +103,135 @@ export async function GET(
|
|
185 |
);
|
186 |
}
|
187 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
+
import { RepoDesignation, spaceInfo, uploadFile } from "@huggingface/hub";
|
3 |
|
4 |
import { isAuthenticated } from "@/lib/auth";
|
5 |
+
import Project from "@/models/Project";
|
6 |
+
import dbConnect from "@/lib/mongodb";
|
7 |
+
import { getPTag } from "@/lib/utils";
|
8 |
|
9 |
+
export async function GET(
|
10 |
req: NextRequest,
|
11 |
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
12 |
) {
|
|
|
16 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
17 |
}
|
18 |
|
19 |
+
await dbConnect();
|
20 |
const param = await params;
|
21 |
const { namespace, repoId } = param;
|
22 |
|
23 |
+
const project = await Project.findOne({
|
24 |
+
user_id: user.id,
|
25 |
+
space_id: `${namespace}/${repoId}`,
|
26 |
+
}).lean();
|
27 |
+
if (!project) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
return NextResponse.json(
|
29 |
+
{
|
30 |
+
ok: false,
|
31 |
+
error: "Project not found",
|
32 |
+
},
|
33 |
+
{ status: 404 }
|
34 |
);
|
35 |
}
|
36 |
+
const space_url = `https://huggingface.co/spaces/${namespace}/${repoId}/raw/main/index.html`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
try {
|
38 |
const space = await spaceInfo({
|
39 |
name: namespace + "/" + repoId,
|
|
|
60 |
);
|
61 |
}
|
62 |
|
63 |
+
const response = await fetch(space_url);
|
64 |
+
if (!response.ok) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
65 |
return NextResponse.json(
|
66 |
{
|
67 |
ok: false,
|
68 |
+
error: "Failed to fetch space HTML",
|
69 |
},
|
70 |
{ status: 404 }
|
71 |
);
|
72 |
}
|
73 |
+
let html = await response.text();
|
74 |
+
// remove the last p tag including this url https://enzostvs-deepsite.hf.space
|
75 |
+
html = html.replace(getPTag(namespace + "/" + repoId), "");
|
76 |
+
|
77 |
return NextResponse.json(
|
78 |
{
|
79 |
project: {
|
80 |
+
...project,
|
81 |
+
html,
|
|
|
|
|
82 |
},
|
|
|
|
|
|
|
83 |
ok: true,
|
84 |
},
|
85 |
{ status: 200 }
|
|
|
88 |
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
89 |
} catch (error: any) {
|
90 |
if (error.statusCode === 404) {
|
91 |
+
await Project.deleteOne({
|
92 |
+
user_id: user.id,
|
93 |
+
space_id: `${namespace}/${repoId}`,
|
94 |
+
});
|
95 |
return NextResponse.json(
|
96 |
{ error: "Space not found", ok: false },
|
97 |
{ status: 404 }
|
|
|
103 |
);
|
104 |
}
|
105 |
}
|
106 |
+
|
107 |
+
export async function PUT(
|
108 |
+
req: NextRequest,
|
109 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
110 |
+
) {
|
111 |
+
const user = await isAuthenticated();
|
112 |
+
|
113 |
+
if (user instanceof NextResponse || !user) {
|
114 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
115 |
+
}
|
116 |
+
|
117 |
+
await dbConnect();
|
118 |
+
const param = await params;
|
119 |
+
const { namespace, repoId } = param;
|
120 |
+
const { html, prompts } = await req.json();
|
121 |
+
|
122 |
+
const project = await Project.findOne({
|
123 |
+
user_id: user.id,
|
124 |
+
space_id: `${namespace}/${repoId}`,
|
125 |
+
}).lean();
|
126 |
+
if (!project) {
|
127 |
+
return NextResponse.json(
|
128 |
+
{
|
129 |
+
ok: false,
|
130 |
+
error: "Project not found",
|
131 |
+
},
|
132 |
+
{ status: 404 }
|
133 |
+
);
|
134 |
+
}
|
135 |
+
|
136 |
+
const repo: RepoDesignation = {
|
137 |
+
type: "space",
|
138 |
+
name: `${namespace}/${repoId}`,
|
139 |
+
};
|
140 |
+
|
141 |
+
const newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}</body>`);
|
142 |
+
const file = new File([newHtml], "index.html", { type: "text/html" });
|
143 |
+
await uploadFile({
|
144 |
+
repo,
|
145 |
+
file,
|
146 |
+
accessToken: user.token as string,
|
147 |
+
commitTitle: `${prompts[prompts.length - 1]} - Follow Up Deployment`,
|
148 |
+
});
|
149 |
+
|
150 |
+
await Project.updateOne(
|
151 |
+
{ user_id: user.id, space_id: `${namespace}/${repoId}` },
|
152 |
+
{
|
153 |
+
$set: {
|
154 |
+
prompts: [
|
155 |
+
...(project && "prompts" in project ? project.prompts : []),
|
156 |
+
...prompts,
|
157 |
+
],
|
158 |
+
},
|
159 |
+
}
|
160 |
+
);
|
161 |
+
return NextResponse.json({ ok: true }, { status: 200 });
|
162 |
+
}
|
163 |
+
|
164 |
+
export async function POST(
|
165 |
+
req: NextRequest,
|
166 |
+
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
167 |
+
) {
|
168 |
+
const user = await isAuthenticated();
|
169 |
+
|
170 |
+
if (user instanceof NextResponse || !user) {
|
171 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
172 |
+
}
|
173 |
+
|
174 |
+
await dbConnect();
|
175 |
+
const param = await params;
|
176 |
+
const { namespace, repoId } = param;
|
177 |
+
|
178 |
+
const space = await spaceInfo({
|
179 |
+
name: namespace + "/" + repoId,
|
180 |
+
accessToken: user.token as string,
|
181 |
+
additionalFields: ["author"],
|
182 |
+
});
|
183 |
+
|
184 |
+
if (!space || space.sdk !== "static") {
|
185 |
+
return NextResponse.json(
|
186 |
+
{
|
187 |
+
ok: false,
|
188 |
+
error: "Space is not a static space",
|
189 |
+
},
|
190 |
+
{ status: 404 }
|
191 |
+
);
|
192 |
+
}
|
193 |
+
if (space.author !== user.name) {
|
194 |
+
return NextResponse.json(
|
195 |
+
{
|
196 |
+
ok: false,
|
197 |
+
error: "Space does not belong to the authenticated user",
|
198 |
+
},
|
199 |
+
{ status: 403 }
|
200 |
+
);
|
201 |
+
}
|
202 |
+
|
203 |
+
const project = await Project.findOne({
|
204 |
+
user_id: user.id,
|
205 |
+
space_id: `${namespace}/${repoId}`,
|
206 |
+
}).lean();
|
207 |
+
if (project) {
|
208 |
+
// redirect to the project page if it already exists
|
209 |
+
return NextResponse.json(
|
210 |
+
{
|
211 |
+
ok: false,
|
212 |
+
error: "Project already exists",
|
213 |
+
redirect: `/projects/${namespace}/${repoId}`,
|
214 |
+
},
|
215 |
+
{ status: 400 }
|
216 |
+
);
|
217 |
+
}
|
218 |
+
|
219 |
+
const newProject = new Project({
|
220 |
+
user_id: user.id,
|
221 |
+
space_id: `${namespace}/${repoId}`,
|
222 |
+
prompts: [],
|
223 |
+
});
|
224 |
+
|
225 |
+
await newProject.save();
|
226 |
+
return NextResponse.json(
|
227 |
+
{
|
228 |
+
ok: true,
|
229 |
+
project: {
|
230 |
+
id: newProject._id,
|
231 |
+
space_id: newProject.space_id,
|
232 |
+
prompts: newProject.prompts,
|
233 |
+
},
|
234 |
+
},
|
235 |
+
{ status: 201 }
|
236 |
+
);
|
237 |
+
}
|
app/api/me/projects/[namespace]/[repoId]/save/route.ts
DELETED
@@ -1,64 +0,0 @@
|
|
1 |
-
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import { uploadFiles } from "@huggingface/hub";
|
3 |
-
|
4 |
-
import { isAuthenticated } from "@/lib/auth";
|
5 |
-
import { Page } from "@/types";
|
6 |
-
|
7 |
-
export async function PUT(
|
8 |
-
req: NextRequest,
|
9 |
-
{ params }: { params: Promise<{ namespace: string; repoId: string }> }
|
10 |
-
) {
|
11 |
-
const user = await isAuthenticated();
|
12 |
-
if (user instanceof NextResponse || !user) {
|
13 |
-
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
14 |
-
}
|
15 |
-
|
16 |
-
const param = await params;
|
17 |
-
const { namespace, repoId } = param;
|
18 |
-
const { pages, commitTitle = "Manual changes saved" } = await req.json();
|
19 |
-
|
20 |
-
if (!pages || !Array.isArray(pages) || pages.length === 0) {
|
21 |
-
return NextResponse.json(
|
22 |
-
{ ok: false, error: "Pages are required" },
|
23 |
-
{ status: 400 }
|
24 |
-
);
|
25 |
-
}
|
26 |
-
|
27 |
-
try {
|
28 |
-
// Prepare files for upload
|
29 |
-
const files: File[] = [];
|
30 |
-
pages.forEach((page: Page) => {
|
31 |
-
const file = new File([page.html], page.path, { type: "text/html" });
|
32 |
-
files.push(file);
|
33 |
-
});
|
34 |
-
|
35 |
-
// Upload files to HuggingFace Hub
|
36 |
-
const response = await uploadFiles({
|
37 |
-
repo: {
|
38 |
-
type: "space",
|
39 |
-
name: `${namespace}/${repoId}`,
|
40 |
-
},
|
41 |
-
files,
|
42 |
-
commitTitle,
|
43 |
-
accessToken: user.token as string,
|
44 |
-
});
|
45 |
-
|
46 |
-
return NextResponse.json({
|
47 |
-
ok: true,
|
48 |
-
pages,
|
49 |
-
commit: {
|
50 |
-
...response.commit,
|
51 |
-
title: commitTitle,
|
52 |
-
}
|
53 |
-
});
|
54 |
-
} catch (error: any) {
|
55 |
-
console.error("Error saving manual changes:", error);
|
56 |
-
return NextResponse.json(
|
57 |
-
{
|
58 |
-
ok: false,
|
59 |
-
error: error.message || "Failed to save changes",
|
60 |
-
},
|
61 |
-
{ status: 500 }
|
62 |
-
);
|
63 |
-
}
|
64 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/me/projects/route.ts
CHANGED
@@ -1,107 +1,126 @@
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
-
import {
|
3 |
|
4 |
import { isAuthenticated } from "@/lib/auth";
|
5 |
-
import
|
6 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
|
8 |
-
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
const user = await isAuthenticated();
|
|
|
12 |
if (user instanceof NextResponse || !user) {
|
13 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
14 |
}
|
15 |
|
16 |
-
const { title
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
|
18 |
-
|
|
|
|
|
19 |
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
colorFrom: ${colorFrom}
|
37 |
colorTo: ${colorTo}
|
38 |
-
emoji: 🐳
|
39 |
sdk: static
|
40 |
pinned: false
|
41 |
tags:
|
42 |
-
- deepsite
|
43 |
---
|
44 |
|
45 |
-
|
46 |
-
This project was created with [DeepSite](https://deepsite.hf.co).
|
47 |
-
`;
|
48 |
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
const file = new File([page.html], page.path, { type: "text/html" });
|
54 |
-
files.push(file);
|
55 |
-
});
|
56 |
-
|
57 |
-
try {
|
58 |
-
const { repoUrl} = await createRepo({
|
59 |
-
repo,
|
60 |
-
accessToken: user.token as string,
|
61 |
});
|
62 |
-
const
|
63 |
await uploadFiles({
|
64 |
repo,
|
65 |
files,
|
66 |
accessToken: user.token as string,
|
67 |
-
commitTitle
|
68 |
});
|
69 |
-
|
70 |
const path = repoUrl.split("/").slice(-2).join("/");
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
continue;
|
76 |
-
}
|
77 |
-
commits.push({
|
78 |
-
title: commit.title,
|
79 |
-
oid: commit.oid,
|
80 |
-
date: commit.date,
|
81 |
-
});
|
82 |
-
}
|
83 |
-
|
84 |
-
const space = await spaceInfo({
|
85 |
-
name: repo.name,
|
86 |
-
accessToken: user.token as string,
|
87 |
});
|
88 |
-
|
89 |
-
|
90 |
-
files,
|
91 |
-
pages,
|
92 |
-
commits,
|
93 |
-
project: {
|
94 |
-
id: space.id,
|
95 |
-
space_id: space.name,
|
96 |
-
_updatedAt: space.updatedAt,
|
97 |
-
}
|
98 |
-
}
|
99 |
-
|
100 |
-
return NextResponse.json({ space: newProject, path, ok: true }, { status: 201 });
|
101 |
} catch (err: any) {
|
102 |
return NextResponse.json(
|
103 |
{ error: err.message, ok: false },
|
104 |
{ status: 500 }
|
105 |
);
|
106 |
}
|
107 |
-
}
|
|
|
1 |
import { NextRequest, NextResponse } from "next/server";
|
2 |
+
import { createRepo, RepoDesignation, uploadFiles } from "@huggingface/hub";
|
3 |
|
4 |
import { isAuthenticated } from "@/lib/auth";
|
5 |
+
import Project from "@/models/Project";
|
6 |
+
import dbConnect from "@/lib/mongodb";
|
7 |
+
import { COLORS, getPTag } from "@/lib/utils";
|
8 |
+
// import type user
|
9 |
+
export async function GET() {
|
10 |
+
const user = await isAuthenticated();
|
11 |
+
|
12 |
+
if (user instanceof NextResponse || !user) {
|
13 |
+
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
14 |
+
}
|
15 |
|
16 |
+
await dbConnect();
|
17 |
+
|
18 |
+
const projects = await Project.find({
|
19 |
+
user_id: user?.id,
|
20 |
+
})
|
21 |
+
.sort({ _createdAt: -1 })
|
22 |
+
.limit(100)
|
23 |
+
.lean();
|
24 |
+
if (!projects) {
|
25 |
+
return NextResponse.json(
|
26 |
+
{
|
27 |
+
ok: false,
|
28 |
+
projects: [],
|
29 |
+
},
|
30 |
+
{ status: 404 }
|
31 |
+
);
|
32 |
+
}
|
33 |
+
return NextResponse.json(
|
34 |
+
{
|
35 |
+
ok: true,
|
36 |
+
projects,
|
37 |
+
},
|
38 |
+
{ status: 200 }
|
39 |
+
);
|
40 |
+
}
|
41 |
+
|
42 |
+
/**
|
43 |
+
* This API route creates a new project in Hugging Face Spaces.
|
44 |
+
* It requires an Authorization header with a valid token and a JSON body with the project details.
|
45 |
+
*/
|
46 |
+
export async function POST(request: NextRequest) {
|
47 |
const user = await isAuthenticated();
|
48 |
+
|
49 |
if (user instanceof NextResponse || !user) {
|
50 |
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
|
51 |
}
|
52 |
|
53 |
+
const { title, html, prompts } = await request.json();
|
54 |
+
|
55 |
+
if (!title || !html) {
|
56 |
+
return NextResponse.json(
|
57 |
+
{ message: "Title and HTML content are required.", ok: false },
|
58 |
+
{ status: 400 }
|
59 |
+
);
|
60 |
+
}
|
61 |
+
|
62 |
+
await dbConnect();
|
63 |
|
64 |
+
try {
|
65 |
+
let readme = "";
|
66 |
+
let newHtml = html;
|
67 |
|
68 |
+
const newTitle = title
|
69 |
+
.toLowerCase()
|
70 |
+
.replace(/[^a-z0-9]+/g, "-")
|
71 |
+
.split("-")
|
72 |
+
.filter(Boolean)
|
73 |
+
.join("-")
|
74 |
+
.slice(0, 96);
|
75 |
|
76 |
+
const repo: RepoDesignation = {
|
77 |
+
type: "space",
|
78 |
+
name: `${user.name}/${newTitle}`,
|
79 |
+
};
|
80 |
+
|
81 |
+
const { repoUrl } = await createRepo({
|
82 |
+
repo,
|
83 |
+
accessToken: user.token as string,
|
84 |
+
});
|
85 |
+
const colorFrom = COLORS[Math.floor(Math.random() * COLORS.length)];
|
86 |
+
const colorTo = COLORS[Math.floor(Math.random() * COLORS.length)];
|
87 |
+
readme = `---
|
88 |
+
title: ${newTitle}
|
89 |
+
emoji: 🐳
|
90 |
colorFrom: ${colorFrom}
|
91 |
colorTo: ${colorTo}
|
|
|
92 |
sdk: static
|
93 |
pinned: false
|
94 |
tags:
|
95 |
+
- deepsite
|
96 |
---
|
97 |
|
98 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference`;
|
|
|
|
|
99 |
|
100 |
+
newHtml = html.replace(/<\/body>/, `${getPTag(repo.name)}</body>`);
|
101 |
+
const file = new File([newHtml], "index.html", { type: "text/html" });
|
102 |
+
const readmeFile = new File([readme], "README.md", {
|
103 |
+
type: "text/markdown",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
});
|
105 |
+
const files = [file, readmeFile];
|
106 |
await uploadFiles({
|
107 |
repo,
|
108 |
files,
|
109 |
accessToken: user.token as string,
|
110 |
+
commitTitle: `${prompts[prompts.length - 1]} - Initial Deployment`,
|
111 |
});
|
|
|
112 |
const path = repoUrl.split("/").slice(-2).join("/");
|
113 |
+
const project = await Project.create({
|
114 |
+
user_id: user.id,
|
115 |
+
space_id: path,
|
116 |
+
prompts,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
117 |
});
|
118 |
+
return NextResponse.json({ project, path, ok: true }, { status: 201 });
|
119 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
} catch (err: any) {
|
121 |
return NextResponse.json(
|
122 |
{ error: err.message, ok: false },
|
123 |
{ status: 500 }
|
124 |
);
|
125 |
}
|
126 |
+
}
|
app/api/me/route.ts
CHANGED
@@ -1,4 +1,3 @@
|
|
1 |
-
import { listSpaces } from "@huggingface/hub";
|
2 |
import { headers } from "next/headers";
|
3 |
import { NextResponse } from "next/server";
|
4 |
|
@@ -22,25 +21,5 @@ export async function GET() {
|
|
22 |
);
|
23 |
}
|
24 |
const user = await userResponse.json();
|
25 |
-
|
26 |
-
for await (const space of listSpaces({
|
27 |
-
accessToken: token.replace("Bearer ", "") as string,
|
28 |
-
additionalFields: ["author", "cardData"],
|
29 |
-
search: {
|
30 |
-
owner: user.name,
|
31 |
-
}
|
32 |
-
})) {
|
33 |
-
if (
|
34 |
-
space.sdk === "static" &&
|
35 |
-
Array.isArray((space.cardData as { tags?: string[] })?.tags) &&
|
36 |
-
(
|
37 |
-
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite-v3")) ||
|
38 |
-
((space.cardData as { tags?: string[] })?.tags?.includes("deepsite"))
|
39 |
-
)
|
40 |
-
) {
|
41 |
-
projects.push(space);
|
42 |
-
}
|
43 |
-
}
|
44 |
-
|
45 |
-
return NextResponse.json({ user, projects, errCode: null }, { status: 200 });
|
46 |
}
|
|
|
|
|
1 |
import { headers } from "next/headers";
|
2 |
import { NextResponse } from "next/server";
|
3 |
|
|
|
21 |
);
|
22 |
}
|
23 |
const user = await userResponse.json();
|
24 |
+
return NextResponse.json({ user, errCode: null }, { status: 200 });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
}
|
app/auth/callback/page.tsx
CHANGED
@@ -5,92 +5,67 @@ import { use, useState } from "react";
|
|
5 |
import { useMount, useTimeoutFn } from "react-use";
|
6 |
|
7 |
import { Button } from "@/components/ui/button";
|
8 |
-
import { AnimatedBlobs } from "@/components/animated-blobs";
|
9 |
-
import { useBroadcastChannel } from "@/lib/useBroadcastChannel";
|
10 |
export default function AuthCallback({
|
11 |
searchParams,
|
12 |
}: {
|
13 |
searchParams: Promise<{ code: string }>;
|
14 |
}) {
|
15 |
const [showButton, setShowButton] = useState(false);
|
16 |
-
const [isPopupAuth, setIsPopupAuth] = useState(false);
|
17 |
const { code } = use(searchParams);
|
18 |
const { loginFromCode } = useUser();
|
19 |
-
const { postMessage } = useBroadcastChannel("auth", () => {});
|
20 |
|
21 |
useMount(async () => {
|
22 |
if (code) {
|
23 |
-
|
24 |
-
setIsPopupAuth(isPopup);
|
25 |
-
|
26 |
-
if (isPopup) {
|
27 |
-
postMessage({
|
28 |
-
type: "user-oauth",
|
29 |
-
code: code,
|
30 |
-
});
|
31 |
-
|
32 |
-
setTimeout(() => {
|
33 |
-
if (window.opener) {
|
34 |
-
window.close();
|
35 |
-
}
|
36 |
-
}, 1000);
|
37 |
-
} else {
|
38 |
-
await loginFromCode(code);
|
39 |
-
}
|
40 |
}
|
41 |
});
|
42 |
|
43 |
-
useTimeoutFn(
|
|
|
|
|
|
|
44 |
|
45 |
return (
|
46 |
-
<div className="h-screen flex flex-col justify-center items-center
|
47 |
-
<div className="
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
<div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
53 |
-
🚀
|
54 |
-
</div>
|
55 |
-
<div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
|
56 |
-
👋
|
57 |
-
</div>
|
58 |
-
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
59 |
-
🙌
|
60 |
-
</div>
|
61 |
</div>
|
62 |
-
<
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
</p>
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
|
|
|
|
|
|
78 |
</p>
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
Go to Home
|
83 |
-
</Button>
|
84 |
-
</Link>
|
85 |
-
) : (
|
86 |
-
<p className="text-xs text-neutral-500">
|
87 |
-
Please wait, we are logging you in...
|
88 |
-
</p>
|
89 |
-
)}
|
90 |
-
</div>
|
91 |
-
</main>
|
92 |
-
</div>
|
93 |
-
<AnimatedBlobs />
|
94 |
</div>
|
95 |
</div>
|
96 |
);
|
|
|
5 |
import { useMount, useTimeoutFn } from "react-use";
|
6 |
|
7 |
import { Button } from "@/components/ui/button";
|
|
|
|
|
8 |
export default function AuthCallback({
|
9 |
searchParams,
|
10 |
}: {
|
11 |
searchParams: Promise<{ code: string }>;
|
12 |
}) {
|
13 |
const [showButton, setShowButton] = useState(false);
|
|
|
14 |
const { code } = use(searchParams);
|
15 |
const { loginFromCode } = useUser();
|
|
|
16 |
|
17 |
useMount(async () => {
|
18 |
if (code) {
|
19 |
+
await loginFromCode(code);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
}
|
21 |
});
|
22 |
|
23 |
+
useTimeoutFn(
|
24 |
+
() => setShowButton(true),
|
25 |
+
7000 // Show button after 5 seconds
|
26 |
+
);
|
27 |
|
28 |
return (
|
29 |
+
<div className="h-screen flex flex-col justify-center items-center">
|
30 |
+
<div className="!rounded-2xl !p-0 !bg-white !border-neutral-100 min-w-xs text-center overflow-hidden ring-[8px] ring-white/20">
|
31 |
+
<header className="bg-neutral-50 p-6 border-b border-neutral-200/60">
|
32 |
+
<div className="flex items-center justify-center -space-x-4 mb-3">
|
33 |
+
<div className="size-9 rounded-full bg-pink-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
34 |
+
🚀
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
</div>
|
36 |
+
<div className="size-11 rounded-full bg-amber-200 shadow-2xl flex items-center justify-center text-2xl z-2">
|
37 |
+
👋
|
38 |
+
</div>
|
39 |
+
<div className="size-9 rounded-full bg-sky-200 shadow-2xs flex items-center justify-center text-xl opacity-50">
|
40 |
+
🙌
|
41 |
+
</div>
|
42 |
+
</div>
|
43 |
+
<p className="text-xl font-semibold text-neutral-950">
|
44 |
+
Login In Progress...
|
45 |
+
</p>
|
46 |
+
<p className="text-sm text-neutral-500 mt-1.5">
|
47 |
+
Wait a moment while we log you in with your code.
|
48 |
+
</p>
|
49 |
+
</header>
|
50 |
+
<main className="space-y-4 p-6">
|
51 |
+
<div>
|
52 |
+
<p className="text-sm text-neutral-700 mb-4 max-w-xs">
|
53 |
+
If you are not redirected automatically in the next 5 seconds,
|
54 |
+
please click the button below
|
55 |
</p>
|
56 |
+
{showButton ? (
|
57 |
+
<Link href="/">
|
58 |
+
<Button variant="black" className="relative">
|
59 |
+
Go to Home
|
60 |
+
</Button>
|
61 |
+
</Link>
|
62 |
+
) : (
|
63 |
+
<p className="text-xs text-neutral-500">
|
64 |
+
Please wait, we are logging you in...
|
65 |
</p>
|
66 |
+
)}
|
67 |
+
</div>
|
68 |
+
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
</div>
|
70 |
</div>
|
71 |
);
|
app/layout.tsx
CHANGED
@@ -2,18 +2,14 @@
|
|
2 |
import type { Metadata, Viewport } from "next";
|
3 |
import { Inter, PT_Sans } from "next/font/google";
|
4 |
import { cookies } from "next/headers";
|
5 |
-
import Script from "next/script";
|
6 |
|
|
|
7 |
import "@/assets/globals.css";
|
8 |
import { Toaster } from "@/components/ui/sonner";
|
9 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
10 |
import { apiServer } from "@/lib/api";
|
11 |
-
import IframeDetector from "@/components/iframe-detector";
|
12 |
import AppContext from "@/components/contexts/app-context";
|
13 |
-
import
|
14 |
-
import { LoginProvider } from "@/components/contexts/login-context";
|
15 |
-
import { ProProvider } from "@/components/contexts/pro-context";
|
16 |
-
import { generateSEO, generateStructuredData } from "@/lib/seo";
|
17 |
|
18 |
const inter = Inter({
|
19 |
variable: "--font-inter-sans",
|
@@ -27,12 +23,31 @@ const ptSans = PT_Sans({
|
|
27 |
});
|
28 |
|
29 |
export const metadata: Metadata = {
|
30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
title: "DeepSite | Build with AI ✨",
|
32 |
description:
|
33 |
"DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
|
34 |
-
|
35 |
-
}
|
36 |
appleWebApp: {
|
37 |
capable: true,
|
38 |
title: "DeepSite",
|
@@ -43,9 +58,6 @@ export const metadata: Metadata = {
|
|
43 |
shortcut: "/logo.svg",
|
44 |
apple: "/logo.svg",
|
45 |
},
|
46 |
-
verification: {
|
47 |
-
google: process.env.GOOGLE_SITE_VERIFICATION,
|
48 |
-
},
|
49 |
};
|
50 |
|
51 |
export const viewport: Viewport = {
|
@@ -57,16 +69,16 @@ export const viewport: Viewport = {
|
|
57 |
async function getMe() {
|
58 |
const cookieStore = await cookies();
|
59 |
const token = cookieStore.get(MY_TOKEN_KEY())?.value;
|
60 |
-
if (!token) return { user: null,
|
61 |
try {
|
62 |
const res = await apiServer.get("/me", {
|
63 |
headers: {
|
64 |
Authorization: `Bearer ${token}`,
|
65 |
},
|
66 |
});
|
67 |
-
return { user: res.data.user,
|
68 |
} catch (err: any) {
|
69 |
-
return { user: null,
|
70 |
}
|
71 |
}
|
72 |
|
@@ -76,35 +88,8 @@ export default async function RootLayout({
|
|
76 |
children: React.ReactNode;
|
77 |
}>) {
|
78 |
const data = await getMe();
|
79 |
-
|
80 |
-
// Generate structured data
|
81 |
-
const structuredData = generateStructuredData("WebApplication", {
|
82 |
-
name: "DeepSite",
|
83 |
-
description: "Build websites with AI, no code required",
|
84 |
-
url: "https://deepsite.hf.co",
|
85 |
-
});
|
86 |
-
|
87 |
-
const organizationData = generateStructuredData("Organization", {
|
88 |
-
name: "DeepSite",
|
89 |
-
url: "https://deepsite.hf.co",
|
90 |
-
});
|
91 |
-
|
92 |
return (
|
93 |
<html lang="en">
|
94 |
-
<head>
|
95 |
-
<script
|
96 |
-
type="application/ld+json"
|
97 |
-
dangerouslySetInnerHTML={{
|
98 |
-
__html: JSON.stringify(structuredData),
|
99 |
-
}}
|
100 |
-
/>
|
101 |
-
<script
|
102 |
-
type="application/ld+json"
|
103 |
-
dangerouslySetInnerHTML={{
|
104 |
-
__html: JSON.stringify(organizationData),
|
105 |
-
}}
|
106 |
-
/>
|
107 |
-
</head>
|
108 |
<Script
|
109 |
defer
|
110 |
data-domain="deepsite.hf.co"
|
@@ -113,15 +98,10 @@ export default async function RootLayout({
|
|
113 |
<body
|
114 |
className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
|
115 |
>
|
116 |
-
<IframeDetector />
|
117 |
<Toaster richColors position="bottom-center" />
|
118 |
-
<
|
119 |
-
<AppContext me={data}>
|
120 |
-
|
121 |
-
<ProProvider>{children}</ProProvider>
|
122 |
-
</LoginProvider>
|
123 |
-
</AppContext>
|
124 |
-
</TanstackContext>
|
125 |
</body>
|
126 |
</html>
|
127 |
);
|
|
|
2 |
import type { Metadata, Viewport } from "next";
|
3 |
import { Inter, PT_Sans } from "next/font/google";
|
4 |
import { cookies } from "next/headers";
|
|
|
5 |
|
6 |
+
import TanstackProvider from "@/components/providers/tanstack-query-provider";
|
7 |
import "@/assets/globals.css";
|
8 |
import { Toaster } from "@/components/ui/sonner";
|
9 |
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
10 |
import { apiServer } from "@/lib/api";
|
|
|
11 |
import AppContext from "@/components/contexts/app-context";
|
12 |
+
import Script from "next/script";
|
|
|
|
|
|
|
13 |
|
14 |
const inter = Inter({
|
15 |
variable: "--font-inter-sans",
|
|
|
23 |
});
|
24 |
|
25 |
export const metadata: Metadata = {
|
26 |
+
title: "DeepSite | Build with AI ✨",
|
27 |
+
description:
|
28 |
+
"DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
|
29 |
+
openGraph: {
|
30 |
+
title: "DeepSite | Build with AI ✨",
|
31 |
+
description:
|
32 |
+
"DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
|
33 |
+
url: "https://deepsite.hf.co",
|
34 |
+
siteName: "DeepSite",
|
35 |
+
images: [
|
36 |
+
{
|
37 |
+
url: "https://deepsite.hf.co/banner.png",
|
38 |
+
width: 1200,
|
39 |
+
height: 630,
|
40 |
+
alt: "DeepSite Open Graph Image",
|
41 |
+
},
|
42 |
+
],
|
43 |
+
},
|
44 |
+
twitter: {
|
45 |
+
card: "summary_large_image",
|
46 |
title: "DeepSite | Build with AI ✨",
|
47 |
description:
|
48 |
"DeepSite is a web development tool that helps you build websites with AI, no code required. Let's deploy your website with DeepSite and enjoy the magic of AI.",
|
49 |
+
images: ["https://deepsite.hf.co/banner.png"],
|
50 |
+
},
|
51 |
appleWebApp: {
|
52 |
capable: true,
|
53 |
title: "DeepSite",
|
|
|
58 |
shortcut: "/logo.svg",
|
59 |
apple: "/logo.svg",
|
60 |
},
|
|
|
|
|
|
|
61 |
};
|
62 |
|
63 |
export const viewport: Viewport = {
|
|
|
69 |
async function getMe() {
|
70 |
const cookieStore = await cookies();
|
71 |
const token = cookieStore.get(MY_TOKEN_KEY())?.value;
|
72 |
+
if (!token) return { user: null, errCode: null };
|
73 |
try {
|
74 |
const res = await apiServer.get("/me", {
|
75 |
headers: {
|
76 |
Authorization: `Bearer ${token}`,
|
77 |
},
|
78 |
});
|
79 |
+
return { user: res.data.user, errCode: null };
|
80 |
} catch (err: any) {
|
81 |
+
return { user: null, errCode: err.status };
|
82 |
}
|
83 |
}
|
84 |
|
|
|
88 |
children: React.ReactNode;
|
89 |
}>) {
|
90 |
const data = await getMe();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
return (
|
92 |
<html lang="en">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
93 |
<Script
|
94 |
defer
|
95 |
data-domain="deepsite.hf.co"
|
|
|
98 |
<body
|
99 |
className={`${inter.variable} ${ptSans.variable} antialiased bg-black dark h-[100dvh] overflow-hidden`}
|
100 |
>
|
|
|
101 |
<Toaster richColors position="bottom-center" />
|
102 |
+
<TanstackProvider>
|
103 |
+
<AppContext me={data}>{children}</AppContext>
|
104 |
+
</TanstackProvider>
|
|
|
|
|
|
|
|
|
105 |
</body>
|
106 |
</html>
|
107 |
);
|
app/new/page.tsx
DELETED
@@ -1,14 +0,0 @@
|
|
1 |
-
import { AppEditor } from "@/components/editor";
|
2 |
-
import { Metadata } from "next";
|
3 |
-
import { generateSEO } from "@/lib/seo";
|
4 |
-
|
5 |
-
export const metadata: Metadata = generateSEO({
|
6 |
-
title: "Create New Project - DeepSite",
|
7 |
-
description:
|
8 |
-
"Start building your next website with AI. Create a new project on DeepSite and experience the power of AI-driven web development.",
|
9 |
-
path: "/new",
|
10 |
-
});
|
11 |
-
|
12 |
-
export default function NewProjectPage() {
|
13 |
-
return <AppEditor isNew />;
|
14 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/projects/[namespace]/[repoId]/page.tsx
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { cookies } from "next/headers";
|
2 |
+
import { redirect } from "next/navigation";
|
3 |
+
|
4 |
+
import { apiServer } from "@/lib/api";
|
5 |
+
import MY_TOKEN_KEY from "@/lib/get-cookie-name";
|
6 |
+
import { AppEditor } from "@/components/editor";
|
7 |
+
|
8 |
+
async function getProject(namespace: string, repoId: string) {
|
9 |
+
// TODO replace with a server action
|
10 |
+
const cookieStore = await cookies();
|
11 |
+
const token = cookieStore.get(MY_TOKEN_KEY())?.value;
|
12 |
+
if (!token) return {};
|
13 |
+
try {
|
14 |
+
const { data } = await apiServer.get(
|
15 |
+
`/me/projects/${namespace}/${repoId}`,
|
16 |
+
{
|
17 |
+
headers: {
|
18 |
+
Authorization: `Bearer ${token}`,
|
19 |
+
},
|
20 |
+
}
|
21 |
+
);
|
22 |
+
|
23 |
+
return data.project;
|
24 |
+
} catch {
|
25 |
+
return {};
|
26 |
+
}
|
27 |
+
}
|
28 |
+
|
29 |
+
export default async function ProjectNamespacePage({
|
30 |
+
params,
|
31 |
+
}: {
|
32 |
+
params: Promise<{ namespace: string; repoId: string }>;
|
33 |
+
}) {
|
34 |
+
const { namespace, repoId } = await params;
|
35 |
+
const project = await getProject(namespace, repoId);
|
36 |
+
if (!project?.html) {
|
37 |
+
redirect("/projects");
|
38 |
+
}
|
39 |
+
return <AppEditor project={project} />;
|
40 |
+
}
|
app/projects/new/page.tsx
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { AppEditor } from "@/components/editor";
|
2 |
+
|
3 |
+
export default function ProjectsNewPage() {
|
4 |
+
return <AppEditor />;
|
5 |
+
}
|
app/sitemap.ts
DELETED
@@ -1,28 +0,0 @@
|
|
1 |
-
import { MetadataRoute } from 'next';
|
2 |
-
|
3 |
-
export default function sitemap(): MetadataRoute.Sitemap {
|
4 |
-
const baseUrl = 'https://deepsite.hf.co';
|
5 |
-
|
6 |
-
return [
|
7 |
-
{
|
8 |
-
url: baseUrl,
|
9 |
-
lastModified: new Date(),
|
10 |
-
changeFrequency: 'daily',
|
11 |
-
priority: 1,
|
12 |
-
},
|
13 |
-
{
|
14 |
-
url: `${baseUrl}/new`,
|
15 |
-
lastModified: new Date(),
|
16 |
-
changeFrequency: 'weekly',
|
17 |
-
priority: 0.8,
|
18 |
-
},
|
19 |
-
{
|
20 |
-
url: `${baseUrl}/auth`,
|
21 |
-
lastModified: new Date(),
|
22 |
-
changeFrequency: 'monthly',
|
23 |
-
priority: 0.5,
|
24 |
-
},
|
25 |
-
// Note: Dynamic project routes will be handled by Next.js automatically
|
26 |
-
// but you can add specific high-priority project pages here if needed
|
27 |
-
];
|
28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assets/deepseek.svg
DELETED
assets/globals.css
CHANGED
@@ -112,10 +112,6 @@
|
|
112 |
--sidebar-ring: oklch(0.556 0 0);
|
113 |
}
|
114 |
|
115 |
-
body {
|
116 |
-
@apply scroll-smooth
|
117 |
-
}
|
118 |
-
|
119 |
@layer base {
|
120 |
* {
|
121 |
@apply border-border outline-ring/50;
|
@@ -148,224 +144,3 @@ body {
|
|
148 |
.matched-line {
|
149 |
@apply bg-sky-500/30;
|
150 |
}
|
151 |
-
|
152 |
-
/* Fast liquid deformation animations */
|
153 |
-
@keyframes liquidBlob1 {
|
154 |
-
0%, 100% {
|
155 |
-
border-radius: 40% 60% 50% 50%;
|
156 |
-
transform: scaleX(1) scaleY(1) rotate(0deg);
|
157 |
-
}
|
158 |
-
12.5% {
|
159 |
-
border-radius: 20% 80% 70% 30%;
|
160 |
-
transform: scaleX(1.6) scaleY(0.4) rotate(25deg);
|
161 |
-
}
|
162 |
-
25% {
|
163 |
-
border-radius: 80% 20% 30% 70%;
|
164 |
-
transform: scaleX(0.5) scaleY(2.1) rotate(-15deg);
|
165 |
-
}
|
166 |
-
37.5% {
|
167 |
-
border-radius: 30% 70% 80% 20%;
|
168 |
-
transform: scaleX(1.8) scaleY(0.6) rotate(40deg);
|
169 |
-
}
|
170 |
-
50% {
|
171 |
-
border-radius: 70% 30% 20% 80%;
|
172 |
-
transform: scaleX(0.4) scaleY(1.9) rotate(-30deg);
|
173 |
-
}
|
174 |
-
62.5% {
|
175 |
-
border-radius: 25% 75% 60% 40%;
|
176 |
-
transform: scaleX(1.5) scaleY(0.7) rotate(55deg);
|
177 |
-
}
|
178 |
-
75% {
|
179 |
-
border-radius: 75% 25% 40% 60%;
|
180 |
-
transform: scaleX(0.6) scaleY(1.7) rotate(-10deg);
|
181 |
-
}
|
182 |
-
87.5% {
|
183 |
-
border-radius: 50% 50% 75% 25%;
|
184 |
-
transform: scaleX(1.3) scaleY(0.8) rotate(35deg);
|
185 |
-
}
|
186 |
-
}
|
187 |
-
|
188 |
-
@keyframes liquidBlob2 {
|
189 |
-
0%, 100% {
|
190 |
-
border-radius: 60% 40% 50% 50%;
|
191 |
-
transform: scaleX(1) scaleY(1) rotate(12deg);
|
192 |
-
}
|
193 |
-
16% {
|
194 |
-
border-radius: 15% 85% 60% 40%;
|
195 |
-
transform: scaleX(0.3) scaleY(2.3) rotate(50deg);
|
196 |
-
}
|
197 |
-
32% {
|
198 |
-
border-radius: 85% 15% 25% 75%;
|
199 |
-
transform: scaleX(2.0) scaleY(0.5) rotate(-20deg);
|
200 |
-
}
|
201 |
-
48% {
|
202 |
-
border-radius: 30% 70% 85% 15%;
|
203 |
-
transform: scaleX(0.4) scaleY(1.8) rotate(70deg);
|
204 |
-
}
|
205 |
-
64% {
|
206 |
-
border-radius: 70% 30% 15% 85%;
|
207 |
-
transform: scaleX(1.9) scaleY(0.6) rotate(-35deg);
|
208 |
-
}
|
209 |
-
80% {
|
210 |
-
border-radius: 40% 60% 70% 30%;
|
211 |
-
transform: scaleX(0.7) scaleY(1.6) rotate(45deg);
|
212 |
-
}
|
213 |
-
}
|
214 |
-
|
215 |
-
@keyframes liquidBlob3 {
|
216 |
-
0%, 100% {
|
217 |
-
border-radius: 50% 50% 40% 60%;
|
218 |
-
transform: scaleX(1) scaleY(1) rotate(0deg);
|
219 |
-
}
|
220 |
-
20% {
|
221 |
-
border-radius: 10% 90% 75% 25%;
|
222 |
-
transform: scaleX(2.2) scaleY(0.3) rotate(-45deg);
|
223 |
-
}
|
224 |
-
40% {
|
225 |
-
border-radius: 90% 10% 20% 80%;
|
226 |
-
transform: scaleX(0.4) scaleY(2.5) rotate(60deg);
|
227 |
-
}
|
228 |
-
60% {
|
229 |
-
border-radius: 25% 75% 90% 10%;
|
230 |
-
transform: scaleX(1.7) scaleY(0.5) rotate(-25deg);
|
231 |
-
}
|
232 |
-
80% {
|
233 |
-
border-radius: 75% 25% 10% 90%;
|
234 |
-
transform: scaleX(0.6) scaleY(2.0) rotate(80deg);
|
235 |
-
}
|
236 |
-
}
|
237 |
-
|
238 |
-
@keyframes liquidBlob4 {
|
239 |
-
0%, 100% {
|
240 |
-
border-radius: 45% 55% 50% 50%;
|
241 |
-
transform: scaleX(1) scaleY(1) rotate(-15deg);
|
242 |
-
}
|
243 |
-
14% {
|
244 |
-
border-radius: 90% 10% 65% 35%;
|
245 |
-
transform: scaleX(0.2) scaleY(2.8) rotate(35deg);
|
246 |
-
}
|
247 |
-
28% {
|
248 |
-
border-radius: 10% 90% 20% 80%;
|
249 |
-
transform: scaleX(2.4) scaleY(0.4) rotate(-50deg);
|
250 |
-
}
|
251 |
-
42% {
|
252 |
-
border-radius: 35% 65% 90% 10%;
|
253 |
-
transform: scaleX(0.3) scaleY(2.1) rotate(70deg);
|
254 |
-
}
|
255 |
-
56% {
|
256 |
-
border-radius: 80% 20% 10% 90%;
|
257 |
-
transform: scaleX(2.0) scaleY(0.5) rotate(-40deg);
|
258 |
-
}
|
259 |
-
70% {
|
260 |
-
border-radius: 20% 80% 55% 45%;
|
261 |
-
transform: scaleX(0.5) scaleY(1.9) rotate(55deg);
|
262 |
-
}
|
263 |
-
84% {
|
264 |
-
border-radius: 65% 35% 80% 20%;
|
265 |
-
transform: scaleX(1.6) scaleY(0.6) rotate(-25deg);
|
266 |
-
}
|
267 |
-
}
|
268 |
-
|
269 |
-
/* Fast flowing movement animations */
|
270 |
-
@keyframes liquidFlow1 {
|
271 |
-
0%, 100% { transform: translate(0, 0); }
|
272 |
-
16% { transform: translate(60px, -40px); }
|
273 |
-
32% { transform: translate(-45px, -70px); }
|
274 |
-
48% { transform: translate(80px, 25px); }
|
275 |
-
64% { transform: translate(-30px, 60px); }
|
276 |
-
80% { transform: translate(50px, -20px); }
|
277 |
-
}
|
278 |
-
|
279 |
-
@keyframes liquidFlow2 {
|
280 |
-
0%, 100% { transform: translate(0, 0); }
|
281 |
-
20% { transform: translate(-70px, 50px); }
|
282 |
-
40% { transform: translate(90px, -30px); }
|
283 |
-
60% { transform: translate(-40px, -55px); }
|
284 |
-
80% { transform: translate(65px, 35px); }
|
285 |
-
}
|
286 |
-
|
287 |
-
@keyframes liquidFlow3 {
|
288 |
-
0%, 100% { transform: translate(0, 0); }
|
289 |
-
12% { transform: translate(-50px, -60px); }
|
290 |
-
24% { transform: translate(40px, -20px); }
|
291 |
-
36% { transform: translate(-30px, 70px); }
|
292 |
-
48% { transform: translate(70px, 20px); }
|
293 |
-
60% { transform: translate(-60px, -35px); }
|
294 |
-
72% { transform: translate(35px, 55px); }
|
295 |
-
84% { transform: translate(-25px, -45px); }
|
296 |
-
}
|
297 |
-
|
298 |
-
@keyframes liquidFlow4 {
|
299 |
-
0%, 100% { transform: translate(0, 0); }
|
300 |
-
14% { transform: translate(50px, 60px); }
|
301 |
-
28% { transform: translate(-80px, -40px); }
|
302 |
-
42% { transform: translate(30px, -90px); }
|
303 |
-
56% { transform: translate(-55px, 45px); }
|
304 |
-
70% { transform: translate(75px, -25px); }
|
305 |
-
84% { transform: translate(-35px, 65px); }
|
306 |
-
}
|
307 |
-
|
308 |
-
/* Light sweep animation for buttons */
|
309 |
-
@keyframes lightSweep {
|
310 |
-
0% {
|
311 |
-
transform: translateX(-150%);
|
312 |
-
opacity: 0;
|
313 |
-
}
|
314 |
-
8% {
|
315 |
-
opacity: 0.3;
|
316 |
-
}
|
317 |
-
25% {
|
318 |
-
opacity: 0.8;
|
319 |
-
}
|
320 |
-
42% {
|
321 |
-
opacity: 0.3;
|
322 |
-
}
|
323 |
-
50% {
|
324 |
-
transform: translateX(150%);
|
325 |
-
opacity: 0;
|
326 |
-
}
|
327 |
-
58% {
|
328 |
-
opacity: 0.3;
|
329 |
-
}
|
330 |
-
75% {
|
331 |
-
opacity: 0.8;
|
332 |
-
}
|
333 |
-
92% {
|
334 |
-
opacity: 0.3;
|
335 |
-
}
|
336 |
-
100% {
|
337 |
-
transform: translateX(-150%);
|
338 |
-
opacity: 0;
|
339 |
-
}
|
340 |
-
}
|
341 |
-
|
342 |
-
.light-sweep {
|
343 |
-
position: relative;
|
344 |
-
overflow: hidden;
|
345 |
-
}
|
346 |
-
|
347 |
-
.light-sweep::before {
|
348 |
-
content: '';
|
349 |
-
position: absolute;
|
350 |
-
top: 0;
|
351 |
-
left: 0;
|
352 |
-
right: 0;
|
353 |
-
bottom: 0;
|
354 |
-
width: 300%;
|
355 |
-
background: linear-gradient(
|
356 |
-
90deg,
|
357 |
-
transparent 0%,
|
358 |
-
transparent 20%,
|
359 |
-
rgba(56, 189, 248, 0.1) 35%,
|
360 |
-
rgba(56, 189, 248, 0.2) 45%,
|
361 |
-
rgba(255, 255, 255, 0.2) 50%,
|
362 |
-
rgba(168, 85, 247, 0.2) 55%,
|
363 |
-
rgba(168, 85, 247, 0.1) 65%,
|
364 |
-
transparent 80%,
|
365 |
-
transparent 100%
|
366 |
-
);
|
367 |
-
animation: lightSweep 7s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
368 |
-
pointer-events: none;
|
369 |
-
z-index: 1;
|
370 |
-
filter: blur(1px);
|
371 |
-
}
|
|
|
112 |
--sidebar-ring: oklch(0.556 0 0);
|
113 |
}
|
114 |
|
|
|
|
|
|
|
|
|
115 |
@layer base {
|
116 |
* {
|
117 |
@apply border-border outline-ring/50;
|
|
|
144 |
.matched-line {
|
145 |
@apply bg-sky-500/30;
|
146 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assets/kimi.svg
DELETED
assets/qwen.svg
DELETED
auth.js
ADDED
@@ -0,0 +1,426 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// نظام المصادقة والأمان
|
2 |
+
class AuthenticationManager {
|
3 |
+
constructor() {
|
4 |
+
this.currentUser = null;
|
5 |
+
this.sessionTimeout = 15 * 60 * 1000; // 15 دقيقة
|
6 |
+
this.maxLoginAttempts = 3;
|
7 |
+
this.lockoutDuration = 30 * 60 * 1000; // 30 دقيقة
|
8 |
+
this.biometricCredential = null;
|
9 |
+
|
10 |
+
this.init();
|
11 |
+
}
|
12 |
+
|
13 |
+
// تهيئة نظام المصادقة
|
14 |
+
init() {
|
15 |
+
this.setupSessionManagement();
|
16 |
+
this.loadStoredCredentials();
|
17 |
+
this.checkBiometricSupport();
|
18 |
+
}
|
19 |
+
|
20 |
+
// إعداد إدارة الجلسة
|
21 |
+
setupSessionManagement() {
|
22 |
+
// تحديث وقت النشاط عند التفاعل
|
23 |
+
document.addEventListener('click', () => this.updateLastActivity());
|
24 |
+
document.addEventListener('keypress', () => this.updateLastActivity());
|
25 |
+
document.addEventListener('touchstart', () => this.updateLastActivity());
|
26 |
+
|
27 |
+
// فحص انتهاء الجلسة كل دقيقة
|
28 |
+
setInterval(() => this.checkSessionExpiry(), 60000);
|
29 |
+
}
|
30 |
+
|
31 |
+
// تسجيل دخول بالرقم ورمز PIN
|
32 |
+
async loginWithPin(phoneNumber, pin) {
|
33 |
+
try {
|
34 |
+
// التحقق من محاولات تسجيل الدخول
|
35 |
+
if (this.isAccountLocked(phoneNumber)) {
|
36 |
+
const lockoutTime = this.getLockoutRemainingTime(phoneNumber);
|
37 |
+
throw new Error(`الحساب مقفل. المحاولة مرة أخرى خلال ${Math.ceil(lockoutTime / 60000)} دقيقة`);
|
38 |
+
}
|
39 |
+
|
40 |
+
// التحقق من صحة البيانات
|
41 |
+
if (!this.validatePhoneNumber(phoneNumber)) {
|
42 |
+
throw new Error('رقم الهاتف غير صحيح');
|
43 |
+
}
|
44 |
+
|
45 |
+
if (!this.validatePin(pin)) {
|
46 |
+
throw new Error('رمز PIN يجب أن يكون 4-6 أرقام');
|
47 |
+
}
|
48 |
+
|
49 |
+
// محاكاة التحقق من البيانات
|
50 |
+
const isValid = await this.verifyCredentials(phoneNumber, pin);
|
51 |
+
|
52 |
+
if (!isValid) {
|
53 |
+
this.recordFailedAttempt(phoneNumber);
|
54 |
+
const remainingAttempts = this.getRemainingAttempts(phoneNumber);
|
55 |
+
|
56 |
+
if (remainingAttempts <= 0) {
|
57 |
+
this.lockAccount(phoneNumber);
|
58 |
+
throw new Error('تم قفل الحساب بسبب المحاولات الخاطئة المتكررة');
|
59 |
+
}
|
60 |
+
|
61 |
+
throw new Error(`بيانات تسجيل الدخول غير صحيحة. المحاولات المتبقية: ${remainingAttempts}`);
|
62 |
+
}
|
63 |
+
|
64 |
+
// تسجيل دخول ناجح
|
65 |
+
this.clearFailedAttempts(phoneNumber);
|
66 |
+
const user = await this.createUserSession(phoneNumber, pin);
|
67 |
+
|
68 |
+
return user;
|
69 |
+
|
70 |
+
} catch (error) {
|
71 |
+
console.error('خطأ في تسجيل الدخول:', error);
|
72 |
+
throw error;
|
73 |
+
}
|
74 |
+
}
|
75 |
+
|
76 |
+
// تسجيل دخول بالبصمة
|
77 |
+
async loginWithBiometric() {
|
78 |
+
try {
|
79 |
+
if (!this.isBiometricSupported()) {
|
80 |
+
throw new Error('المصادقة البيومترية غير مدعومة في هذا المتصفح');
|
81 |
+
}
|
82 |
+
|
83 |
+
if (!this.biometricCredential) {
|
84 |
+
throw new Error('لم يتم تسجيل بصمة مسبقاً. يرجى تسجيل الدخول برمز PIN أولاً');
|
85 |
+
}
|
86 |
+
|
87 |
+
// التحقق من البصمة
|
88 |
+
const credential = await navigator.credentials.get({
|
89 |
+
publicKey: {
|
90 |
+
challenge: this.generateChallenge(),
|
91 |
+
allowCredentials: [{
|
92 |
+
type: 'public-key',
|
93 |
+
id: this.biometricCredential.id
|
94 |
+
}],
|
95 |
+
timeout: 60000,
|
96 |
+
userVerification: 'required'
|
97 |
+
}
|
98 |
+
});
|
99 |
+
|
100 |
+
if (!credential) {
|
101 |
+
throw new Error('فشل في التحقق من البصمة');
|
102 |
+
}
|
103 |
+
|
104 |
+
// إنشاء جلسة المستخدم
|
105 |
+
const savedUser = this.getStoredUser();
|
106 |
+
if (!savedUser) {
|
107 |
+
throw new Error('لم يتم العثور على بيانات المستخدم');
|
108 |
+
}
|
109 |
+
|
110 |
+
const user = await this.createUserSession(savedUser.phone, null, 'biometric');
|
111 |
+
return user;
|
112 |
+
|
113 |
+
} catch (error) {
|
114 |
+
console.error('خطأ في تسجيل الدخول بالبصمة:', error);
|
115 |
+
throw error;
|
116 |
+
}
|
117 |
+
}
|
118 |
+
|
119 |
+
// تسجيل البصمة
|
120 |
+
async registerBiometric(phoneNumber) {
|
121 |
+
try {
|
122 |
+
if (!this.isBiometricSupported()) {
|
123 |
+
throw new Error('المصادقة البيومترية غير مدعومة');
|
124 |
+
}
|
125 |
+
|
126 |
+
const credential = await navigator.credentials.create({
|
127 |
+
publicKey: {
|
128 |
+
challenge: this.generateChallenge(),
|
129 |
+
rp: {
|
130 |
+
name: 'محفظتي الموحدة',
|
131 |
+
id: window.location.hostname
|
132 |
+
},
|
133 |
+
user: {
|
134 |
+
id: new TextEncoder().encode(phoneNumber),
|
135 |
+
name: phoneNumber,
|
136 |
+
displayName: `مستخدم ${phoneNumber}`
|
137 |
+
},
|
138 |
+
pubKeyCredParams: [{
|
139 |
+
type: 'public-key',
|
140 |
+
alg: -7 // ES256
|
141 |
+
}],
|
142 |
+
timeout: 60000,
|
143 |
+
attestation: 'none',
|
144 |
+
authenticatorSelection: {
|
145 |
+
authenticatorAttachment: 'platform',
|
146 |
+
userVerification: 'required'
|
147 |
+
}
|
148 |
+
}
|
149 |
+
});
|
150 |
+
|
151 |
+
if (!credential) {
|
152 |
+
throw new Error('فشل في تسجيل البصمة');
|
153 |
+
}
|
154 |
+
|
155 |
+
// حفظ بيانات البصمة
|
156 |
+
this.biometricCredential = {
|
157 |
+
id: credential.rawId,
|
158 |
+
publicKey: credential.response.publicKey,
|
159 |
+
phoneNumber: phoneNumber,
|
160 |
+
registeredAt: new Date().toISOString()
|
161 |
+
};
|
162 |
+
|
163 |
+
this.storeBiometricCredential();
|
164 |
+
return true;
|
165 |
+
|
166 |
+
} catch (error) {
|
167 |
+
console.error('خطأ في تسجيل البصمة:', error);
|
168 |
+
throw error;
|
169 |
+
}
|
170 |
+
}
|
171 |
+
|
172 |
+
// إنشاء جلسة مستخدم
|
173 |
+
async createUserSession(phoneNumber, pin, authMethod = 'pin') {
|
174 |
+
const user = {
|
175 |
+
id: this.generateUserId(),
|
176 |
+
phone: phoneNumber,
|
177 |
+
name: this.extractNameFromPhone(phoneNumber),
|
178 |
+
authMethod: authMethod,
|
179 |
+
loginTime: new Date().toISOString(),
|
180 |
+
lastActivity: new Date().toISOString(),
|
181 |
+
sessionId: this.generateSessionId()
|
182 |
+
};
|
183 |
+
|
184 |
+
this.currentUser = user;
|
185 |
+
this.storeUserSession(user);
|
186 |
+
this.updateLastActivity();
|
187 |
+
|
188 |
+
return user;
|
189 |
+
}
|
190 |
+
|
191 |
+
// تسجيل الخروج
|
192 |
+
logout() {
|
193 |
+
this.currentUser = null;
|
194 |
+
this.clearUserSession();
|
195 |
+
this.clearStoredCredentials();
|
196 |
+
|
197 |
+
// إعادة توجيه لشاشة تسجيل الدخول
|
198 |
+
if (window.app) {
|
199 |
+
window.app.switchScreen('login');
|
200 |
+
}
|
201 |
+
}
|
202 |
+
|
203 |
+
// التحقق من صحة الجلسة
|
204 |
+
isSessionValid() {
|
205 |
+
if (!this.currentUser) {
|
206 |
+
return false;
|
207 |
+
}
|
208 |
+
|
209 |
+
const lastActivity = new Date(this.currentUser.lastActivity);
|
210 |
+
const now = new Date();
|
211 |
+
const timeDiff = now.getTime() - lastActivity.getTime();
|
212 |
+
|
213 |
+
return timeDiff < this.sessionTimeout;
|
214 |
+
}
|
215 |
+
|
216 |
+
// فحص انتهاء الجلسة
|
217 |
+
checkSessionExpiry() {
|
218 |
+
if (this.currentUser && !this.isSessionValid()) {
|
219 |
+
this.showSessionExpiredDialog();
|
220 |
+
}
|
221 |
+
}
|
222 |
+
|
223 |
+
// عرض حوار انتهاء الجلسة
|
224 |
+
showSessionExpiredDialog() {
|
225 |
+
if (confirm('انتهت صلاحية الجلسة. هل تريد تسجيل الدخول مرة أخرى؟')) {
|
226 |
+
this.logout();
|
227 |
+
} else {
|
228 |
+
this.logout();
|
229 |
+
}
|
230 |
+
}
|
231 |
+
|
232 |
+
// تحديث وقت النشاط الأخير
|
233 |
+
updateLastActivity() {
|
234 |
+
if (this.currentUser) {
|
235 |
+
this.currentUser.lastActivity = new Date().toISOString();
|
236 |
+
this.storeUserSession(this.currentUser);
|
237 |
+
}
|
238 |
+
}
|
239 |
+
|
240 |
+
// التحقق من قفل الحساب
|
241 |
+
isAccountLocked(phoneNumber) {
|
242 |
+
const lockData = this.getLockData(phoneNumber);
|
243 |
+
if (!lockData) return false;
|
244 |
+
|
245 |
+
const now = new Date().getTime();
|
246 |
+
return now < lockData.lockedUntil;
|
247 |
+
}
|
248 |
+
|
249 |
+
// الحصول على الوقت المتبقي للقفل
|
250 |
+
getLockoutRemainingTime(phoneNumber) {
|
251 |
+
const lockData = this.getLockData(phoneNumber);
|
252 |
+
if (!lockData) return 0;
|
253 |
+
|
254 |
+
const now = new Date().getTime();
|
255 |
+
return Math.max(0, lockData.lockedUntil - now);
|
256 |
+
}
|
257 |
+
|
258 |
+
// تسجيل محاولة فاشلة
|
259 |
+
recordFailedAttempt(phoneNumber) {
|
260 |
+
const attempts = this.getFailedAttempts(phoneNumber);
|
261 |
+
attempts.push(new Date().toISOString());
|
262 |
+
|
263 |
+
localStorage.setItem(`failed_attempts_${phoneNumber}`, JSON.stringify(attempts));
|
264 |
+
}
|
265 |
+
|
266 |
+
// الحصول على المحاولات الفاشلة
|
267 |
+
getFailedAttempts(phoneNumber) {
|
268 |
+
const stored = localStorage.getItem(`failed_attempts_${phoneNumber}`);
|
269 |
+
return stored ? JSON.parse(stored) : [];
|
270 |
+
}
|
271 |
+
|
272 |
+
// الحصول على المحاولات المتبقية
|
273 |
+
getRemainingAttempts(phoneNumber) {
|
274 |
+
const attempts = this.getFailedAttempts(phoneNumber);
|
275 |
+
return Math.max(0, this.maxLoginAttempts - attempts.length);
|
276 |
+
}
|
277 |
+
|
278 |
+
// قفل الحساب
|
279 |
+
lockAccount(phoneNumber) {
|
280 |
+
const lockData = {
|
281 |
+
lockedAt: new Date().toISOString(),
|
282 |
+
lockedUntil: new Date().getTime() + this.lockoutDuration
|
283 |
+
};
|
284 |
+
|
285 |
+
localStorage.setItem(`account_lock_${phoneNumber}`, JSON.stringify(lockData));
|
286 |
+
}
|
287 |
+
|
288 |
+
// الحصول على بيانات القفل
|
289 |
+
getLockData(phoneNumber) {
|
290 |
+
const stored = localStorage.getItem(`account_lock_${phoneNumber}`);
|
291 |
+
return stored ? JSON.parse(stored) : null;
|
292 |
+
}
|
293 |
+
|
294 |
+
// مسح المحاولات الفاشلة
|
295 |
+
clearFailedAttempts(phoneNumber) {
|
296 |
+
localStorage.removeItem(`failed_attempts_${phoneNumber}`);
|
297 |
+
localStorage.removeItem(`account_lock_${phoneNumber}`);
|
298 |
+
}
|
299 |
+
|
300 |
+
// التحقق من صحة رقم الهاتف
|
301 |
+
validatePhoneNumber(phoneNumber) {
|
302 |
+
return /^7[0-9]{8}$/.test(phoneNumber);
|
303 |
+
}
|
304 |
+
|
305 |
+
// التحقق من صحة رمز PIN
|
306 |
+
validatePin(pin) {
|
307 |
+
return /^[0-9]{4,6}$/.test(pin);
|
308 |
+
}
|
309 |
+
|
310 |
+
// محاكاة التحقق من البيانات
|
311 |
+
async verifyCredentials(phoneNumber, pin) {
|
312 |
+
// محاكاة استدعاء API
|
313 |
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
314 |
+
|
315 |
+
// في التطبيق الحقيقي، سيتم التحقق من البيانات مع الخادم
|
316 |
+
// للتجربة، نقبل أي رقم هاتف صحيح ورمز PIN من 4-6 أرقام
|
317 |
+
return this.validatePhoneNumber(phoneNumber) && this.validatePin(pin);
|
318 |
+
}
|
319 |
+
|
320 |
+
// فحص دعم المصادقة البيومترية
|
321 |
+
isBiometricSupported() {
|
322 |
+
return window.PublicKeyCredential &&
|
323 |
+
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable;
|
324 |
+
}
|
325 |
+
|
326 |
+
// فحص دعم المصادقة البيومترية
|
327 |
+
async checkBiometricSupport() {
|
328 |
+
if (this.isBiometricSupported()) {
|
329 |
+
try {
|
330 |
+
const available = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
331 |
+
return available;
|
332 |
+
} catch (error) {
|
333 |
+
console.warn('خطأ في فحص دعم المصادقة البيومترية:', error);
|
334 |
+
return false;
|
335 |
+
}
|
336 |
+
}
|
337 |
+
return false;
|
338 |
+
}
|
339 |
+
|
340 |
+
// توليد تحدي للمصادقة البيومترية
|
341 |
+
generateChallenge() {
|
342 |
+
const array = new Uint8Array(32);
|
343 |
+
crypto.getRandomValues(array);
|
344 |
+
return array;
|
345 |
+
}
|
346 |
+
|
347 |
+
// توليد معرف مستخدم
|
348 |
+
generateUserId() {
|
349 |
+
return 'user_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
350 |
+
}
|
351 |
+
|
352 |
+
// توليد معرف جلسة
|
353 |
+
generateSessionId() {
|
354 |
+
return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
355 |
+
}
|
356 |
+
|
357 |
+
// استخراج اسم من رقم الهاتف
|
358 |
+
extractNameFromPhone(phoneNumber) {
|
359 |
+
return `مستخدم ${phoneNumber.substr(-4)}`;
|
360 |
+
}
|
361 |
+
|
362 |
+
// حفظ جلسة المستخدم
|
363 |
+
storeUserSession(user) {
|
364 |
+
localStorage.setItem('unifiedWallet_session', JSON.stringify(user));
|
365 |
+
}
|
366 |
+
|
367 |
+
// تحميل جلسة المستخدم
|
368 |
+
loadStoredSession() {
|
369 |
+
const stored = localStorage.getItem('unifiedWallet_session');
|
370 |
+
if (stored) {
|
371 |
+
const user = JSON.parse(stored);
|
372 |
+
if (this.isSessionValid()) {
|
373 |
+
this.currentUser = user;
|
374 |
+
return user;
|
375 |
+
} else {
|
376 |
+
this.clearUserSession();
|
377 |
+
}
|
378 |
+
}
|
379 |
+
return null;
|
380 |
+
}
|
381 |
+
|
382 |
+
// مسح جلسة المستخدم
|
383 |
+
clearUserSession() {
|
384 |
+
localStorage.removeItem('unifiedWallet_session');
|
385 |
+
}
|
386 |
+
|
387 |
+
// حفظ بيانات البصمة
|
388 |
+
storeBiometricCredential() {
|
389 |
+
if (this.biometricCredential) {
|
390 |
+
localStorage.setItem('unifiedWallet_biometric', JSON.stringify(this.biometricCredential));
|
391 |
+
}
|
392 |
+
}
|
393 |
+
|
394 |
+
// تحميل بيانات البصمة
|
395 |
+
loadStoredCredentials() {
|
396 |
+
const stored = localStorage.getItem('unifiedWallet_biometric');
|
397 |
+
if (stored) {
|
398 |
+
this.biometricCredential = JSON.parse(stored);
|
399 |
+
}
|
400 |
+
}
|
401 |
+
|
402 |
+
// مسح بيانات البصمة
|
403 |
+
clearStoredCredentials() {
|
404 |
+
localStorage.removeItem('unifiedWallet_biometric');
|
405 |
+
this.biometricCredential = null;
|
406 |
+
}
|
407 |
+
|
408 |
+
// الحصول على المستخدم المحفوظ
|
409 |
+
getStoredUser() {
|
410 |
+
const stored = localStorage.getItem('unifiedWallet_user');
|
411 |
+
return stored ? JSON.parse(stored) : null;
|
412 |
+
}
|
413 |
+
|
414 |
+
// الحصول على المستخدم الحالي
|
415 |
+
getCurrentUser() {
|
416 |
+
return this.currentUser;
|
417 |
+
}
|
418 |
+
|
419 |
+
// التحقق من تسجيل الدخول
|
420 |
+
isLoggedIn() {
|
421 |
+
return this.currentUser !== null && this.isSessionValid();
|
422 |
+
}
|
423 |
+
}
|
424 |
+
|
425 |
+
// تصدير الكلاس للاستخدام في التطبيق الرئيسي
|
426 |
+
window.AuthenticationManager = AuthenticationManager;
|
capacitor-simple.config.ts
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { CapacitorConfig } from '@capacitor/cli';
|
2 |
+
|
3 |
+
const config: CapacitorConfig = {
|
4 |
+
appId: 'com.almada.unifiedwallet',
|
5 |
+
appName: 'محفظتي الموحدة',
|
6 |
+
webDir: '.',
|
7 |
+
bundledWebRuntime: false,
|
8 |
+
server: {
|
9 |
+
androidScheme: 'https'
|
10 |
+
},
|
11 |
+
plugins: {
|
12 |
+
SplashScreen: {
|
13 |
+
launchShowDuration: 3000,
|
14 |
+
launchAutoHide: true,
|
15 |
+
backgroundColor: "#4361ee",
|
16 |
+
androidSplashResourceName: "splash",
|
17 |
+
androidScaleType: "CENTER_CROP",
|
18 |
+
showSpinner: false,
|
19 |
+
splashFullScreen: true,
|
20 |
+
splashImmersive: true
|
21 |
+
},
|
22 |
+
StatusBar: {
|
23 |
+
style: "LIGHT",
|
24 |
+
backgroundColor: "#4361ee"
|
25 |
+
},
|
26 |
+
Keyboard: {
|
27 |
+
resize: "body",
|
28 |
+
style: "dark",
|
29 |
+
resizeOnFullScreen: true
|
30 |
+
},
|
31 |
+
LocalNotifications: {
|
32 |
+
smallIcon: "ic_stat_icon_config_sample",
|
33 |
+
iconColor: "#4361ee",
|
34 |
+
sound: "beep.wav"
|
35 |
+
},
|
36 |
+
Device: {
|
37 |
+
permissions: {
|
38 |
+
device: "تحتاج هذه الميزة لتحديد هوية الجهاز للأمان"
|
39 |
+
}
|
40 |
+
}
|
41 |
+
},
|
42 |
+
android: {
|
43 |
+
allowMixedContent: true,
|
44 |
+
captureInput: true,
|
45 |
+
webContentsDebuggingEnabled: true
|
46 |
+
}
|
47 |
+
};
|
48 |
+
|
49 |
+
export default config;
|
capacitor.config.js
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const config = {
|
2 |
+
appId: 'com.almada.unifiedwallet',
|
3 |
+
appName: 'محفظتي الموحدة',
|
4 |
+
webDir: 'www',
|
5 |
+
bundledWebRuntime: false,
|
6 |
+
server: {
|
7 |
+
androidScheme: 'https'
|
8 |
+
},
|
9 |
+
plugins: {
|
10 |
+
SplashScreen: {
|
11 |
+
launchShowDuration: 3000,
|
12 |
+
launchAutoHide: true,
|
13 |
+
backgroundColor: "#4361ee",
|
14 |
+
androidSplashResourceName: "splash",
|
15 |
+
androidScaleType: "CENTER_CROP",
|
16 |
+
showSpinner: false,
|
17 |
+
splashFullScreen: true,
|
18 |
+
splashImmersive: true
|
19 |
+
},
|
20 |
+
StatusBar: {
|
21 |
+
style: "LIGHT",
|
22 |
+
backgroundColor: "#4361ee"
|
23 |
+
},
|
24 |
+
Keyboard: {
|
25 |
+
resize: "body",
|
26 |
+
style: "dark",
|
27 |
+
resizeOnFullScreen: true
|
28 |
+
},
|
29 |
+
LocalNotifications: {
|
30 |
+
smallIcon: "ic_stat_icon_config_sample",
|
31 |
+
iconColor: "#4361ee",
|
32 |
+
sound: "beep.wav"
|
33 |
+
},
|
34 |
+
Device: {
|
35 |
+
permissions: {
|
36 |
+
device: "تحتاج هذه الميزة لتحديد هوية الجهاز للأمان"
|
37 |
+
}
|
38 |
+
}
|
39 |
+
},
|
40 |
+
android: {
|
41 |
+
allowMixedContent: true,
|
42 |
+
captureInput: true,
|
43 |
+
webContentsDebuggingEnabled: true
|
44 |
+
}
|
45 |
+
};
|
46 |
+
|
47 |
+
module.exports = config;
|
capacitor.config.ts
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { CapacitorConfig } from '@capacitor/cli';
|
2 |
+
|
3 |
+
const config: CapacitorConfig = {
|
4 |
+
appId: 'com.almada.unifiedwallet',
|
5 |
+
appName: 'محفظتي الموحدة',
|
6 |
+
webDir: 'www',
|
7 |
+
server: {
|
8 |
+
androidScheme: 'https'
|
9 |
+
},
|
10 |
+
plugins: {
|
11 |
+
SplashScreen: {
|
12 |
+
launchShowDuration: 3000,
|
13 |
+
launchAutoHide: true,
|
14 |
+
backgroundColor: "#4361ee",
|
15 |
+
androidSplashResourceName: "splash",
|
16 |
+
androidScaleType: "CENTER_CROP",
|
17 |
+
showSpinner: false,
|
18 |
+
androidSpinnerStyle: "large",
|
19 |
+
iosSpinnerStyle: "small",
|
20 |
+
spinnerColor: "#ffffff",
|
21 |
+
splashFullScreen: true,
|
22 |
+
splashImmersive: true,
|
23 |
+
layoutName: "launch_screen",
|
24 |
+
useDialog: true,
|
25 |
+
},
|
26 |
+
StatusBar: {
|
27 |
+
style: "LIGHT",
|
28 |
+
backgroundColor: "#4361ee"
|
29 |
+
},
|
30 |
+
Keyboard: {
|
31 |
+
resize: "body",
|
32 |
+
style: "dark",
|
33 |
+
resizeOnFullScreen: true
|
34 |
+
},
|
35 |
+
LocalNotifications: {
|
36 |
+
smallIcon: "ic_stat_icon_config_sample",
|
37 |
+
iconColor: "#4361ee",
|
38 |
+
sound: "beep.wav"
|
39 |
+
},
|
40 |
+
PushNotifications: {
|
41 |
+
presentationOptions: ["badge", "sound", "alert"]
|
42 |
+
},
|
43 |
+
Camera: {
|
44 |
+
permissions: {
|
45 |
+
camera: "تحتاج هذه الميزة للوصول إلى الكاميرا لمسح رموز QR",
|
46 |
+
photos: "تحتاج هذه الميزة للوصول إلى الصور لحفظ الإيصالات"
|
47 |
+
}
|
48 |
+
},
|
49 |
+
Device: {
|
50 |
+
permissions: {
|
51 |
+
device: "تحتاج هذه الميزة لتحديد هوية الجهاز للأمان"
|
52 |
+
}
|
53 |
+
}
|
54 |
+
},
|
55 |
+
android: {
|
56 |
+
allowMixedContent: true,
|
57 |
+
captureInput: true,
|
58 |
+
webContentsDebuggingEnabled: false
|
59 |
+
},
|
60 |
+
ios: {
|
61 |
+
contentInset: "automatic",
|
62 |
+
scrollEnabled: true,
|
63 |
+
allowsLinkPreview: false
|
64 |
+
}
|
65 |
+
};
|
66 |
+
|
67 |
+
export default config;
|
components.json
CHANGED
@@ -5,7 +5,7 @@
|
|
5 |
"tsx": true,
|
6 |
"tailwind": {
|
7 |
"config": "",
|
8 |
-
"css": "
|
9 |
"baseColor": "neutral",
|
10 |
"cssVariables": true,
|
11 |
"prefix": ""
|
|
|
5 |
"tsx": true,
|
6 |
"tailwind": {
|
7 |
"config": "",
|
8 |
+
"css": "app/globals.css",
|
9 |
"baseColor": "neutral",
|
10 |
"cssVariables": true,
|
11 |
"prefix": ""
|
components/animated-blobs/index.tsx
DELETED
@@ -1,34 +0,0 @@
|
|
1 |
-
export function AnimatedBlobs() {
|
2 |
-
return (
|
3 |
-
<div className="absolute inset-0 pointer-events-none -z-[1]">
|
4 |
-
<div
|
5 |
-
className="w-full h-full bg-gradient-to-r from-purple-500 to-pink-500 opacity-10 blur-3xl"
|
6 |
-
style={{
|
7 |
-
animation:
|
8 |
-
"liquidBlob1 4s ease-in-out infinite, liquidFlow1 6s ease-in-out infinite",
|
9 |
-
}}
|
10 |
-
/>
|
11 |
-
<div
|
12 |
-
className="w-2/3 h-3/4 bg-gradient-to-r from-blue-500 to-teal-500 opacity-24 blur-3xl absolute -top-20 right-10"
|
13 |
-
style={{
|
14 |
-
animation:
|
15 |
-
"liquidBlob2 5s ease-in-out infinite, liquidFlow2 7s ease-in-out infinite",
|
16 |
-
}}
|
17 |
-
/>
|
18 |
-
<div
|
19 |
-
className="w-1/2 h-1/2 bg-gradient-to-r from-amber-500 to-rose-500 opacity-20 blur-3xl absolute bottom-0 left-10"
|
20 |
-
style={{
|
21 |
-
animation:
|
22 |
-
"liquidBlob3 3.5s ease-in-out infinite, liquidFlow3 8s ease-in-out infinite",
|
23 |
-
}}
|
24 |
-
/>
|
25 |
-
<div
|
26 |
-
className="w-48 h-48 bg-gradient-to-r from-cyan-500 to-indigo-500 opacity-20 blur-3xl absolute top-1/3 right-1/3"
|
27 |
-
style={{
|
28 |
-
animation:
|
29 |
-
"liquidBlob4 4.5s ease-in-out infinite, liquidFlow4 6.5s ease-in-out infinite",
|
30 |
-
}}
|
31 |
-
/>
|
32 |
-
</div>
|
33 |
-
);
|
34 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|