easc-cli 1.1.31 → 1.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,252 @@
1
+ # CharityHub - A Modern Charity Donation Platform
2
+
3
+ A comprehensive charity website built with vanilla HTML, CSS, JavaScript, and Supabase for the backend. This platform enables users to discover charities, make donations, and provides administrators with tools to manage the platform.
4
+
5
+ ## Features
6
+
7
+ ### For Users
8
+
9
+ - šŸ  **Responsive Home Page** with hero section and live statistics
10
+ - šŸ” **Charity Discovery** with search, filtering, and categorization
11
+ - šŸ’ **Secure Donation System** with multiple payment options
12
+ - šŸ“Š **Progress Tracking** for each charity's fundraising goals
13
+ - šŸ“± **Mobile-First Design** that works on all devices
14
+ - ⭐ **Featured Charities** highlighting important causes
15
+
16
+ ### For Administrators
17
+
18
+ - šŸ“ˆ **Dashboard Analytics** with real-time statistics and charts
19
+ - šŸ¢ **Charity Management** (add, edit, delete charities)
20
+ - šŸ’ø **Donation Tracking** and management
21
+ - šŸ·ļø **Category Management** for organizing causes
22
+ - āš™ļø **Settings Configuration** for platform customization
23
+ - šŸ“Š **Data Export** capabilities
24
+
25
+ ## Technology Stack
26
+
27
+ - **Frontend**: HTML5, CSS3, Vanilla JavaScript
28
+ - **Backend**: Supabase (PostgreSQL + Real-time APIs)
29
+ - **Database**: PostgreSQL with Row Level Security (RLS)
30
+ - **Charts**: Chart.js for data visualization
31
+ - **Icons**: Font Awesome
32
+ - **Styling**: Custom CSS with CSS Grid and Flexbox
33
+
34
+ ## Project Structure
35
+
36
+ ```
37
+ charity-website/
38
+ ā”œā”€ā”€ public/
39
+ │ └── index.html # Main website
40
+ ā”œā”€ā”€ admin/
41
+ │ └── index.html # Admin dashboard
42
+ ā”œā”€ā”€ src/
43
+ │ ā”œā”€ā”€ css/
44
+ │ │ ā”œā”€ā”€ style.css # Main website styles
45
+ │ │ └── admin.css # Admin dashboard styles
46
+ │ └── js/
47
+ │ ā”œā”€ā”€ app.js # Main website JavaScript
48
+ │ └── admin.js # Admin dashboard JavaScript
49
+ ā”œā”€ā”€ data/ # Static data (if needed)
50
+ └── README.md # This file
51
+ ```
52
+
53
+ ## Database Schema
54
+
55
+ ### Tables
56
+
57
+ - **charities**: Information about charitable organizations
58
+ - **donations**: Donation records and transactions
59
+ - **categories**: Charity categories for organization
60
+
61
+ ### Key Features
62
+
63
+ - Row Level Security (RLS) for data protection
64
+ - Real-time updates using Supabase subscriptions
65
+ - Automatic timestamp tracking
66
+ - Foreign key relationships for data integrity
67
+
68
+ ## Getting Started
69
+
70
+ ### Prerequisites
71
+
72
+ - A Supabase account and project
73
+ - Modern web browser
74
+ - Local web server (optional, for development)
75
+
76
+ ### Setup Instructions
77
+
78
+ 1. **Clone or download the project files**
79
+
80
+ 2. **Set up Supabase Database**
81
+
82
+ ```sql
83
+ -- Run the provided migration scripts in your Supabase project
84
+ -- The schema includes charities, donations, and categories tables
85
+ ```
86
+
87
+ 3. **Configure Supabase Connection**
88
+ - Update the SUPABASE_URL and SUPABASE_ANON_KEY in both JavaScript files
89
+ - These can be found in your Supabase project settings
90
+
91
+ 4. **Run the Website**
92
+ - Option 1: Open `public/index.html` directly in your browser
93
+ - Option 2: Use a local web server like `live-server` or Python's HTTP server
94
+
95
+ 5. **Access Admin Dashboard**
96
+ - Navigate to `admin/index.html`
97
+ - Use the admin interface to manage charities and donations
98
+
99
+ ## Configuration
100
+
101
+ ### Supabase Setup
102
+
103
+ 1. Create a new Supabase project
104
+ 2. Run the provided SQL migrations
105
+ 3. Enable Row Level Security (RLS)
106
+ 4. Configure authentication settings if needed
107
+ 5. Get your project URL and anon key
108
+ 6. Update the JavaScript files with your credentials
109
+
110
+ ### Environment Variables
111
+
112
+ For production deployment, consider using environment variables:
113
+
114
+ ```javascript
115
+ const SUPABASE_URL = process.env.SUPABASE_URL || "your-url"
116
+ const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || "your-key"
117
+ ```
118
+
119
+ ## Features in Detail
120
+
121
+ ### Charity Management
122
+
123
+ - Add/edit/delete charities
124
+ - Upload images and set goals
125
+ - Track fundraising progress
126
+ - Categorize by cause type
127
+ - Activate/deactivate charities
128
+
129
+ ### Donation System
130
+
131
+ - Secure donation processing
132
+ - Multiple amount options
133
+ - Anonymous donation support
134
+ - Donor messages and recognition
135
+ - Real-time progress updates
136
+
137
+ ### Search & Discovery
138
+
139
+ - Full-text search across charities
140
+ - Filter by category and progress
141
+ - Sort by various criteria
142
+ - Responsive grid layout
143
+ - Load more functionality
144
+
145
+ ### Admin Dashboard
146
+
147
+ - Real-time statistics
148
+ - Interactive charts
149
+ - Recent activity feed
150
+ - Data management tools
151
+ - Export functionality
152
+
153
+ ## Security Features
154
+
155
+ - Row Level Security (RLS) on all tables
156
+ - Input validation and sanitization
157
+ - XSS protection
158
+ - Secure API endpoints
159
+ - Anonymous donation options
160
+ - Data encryption in transit
161
+
162
+ ## Performance Optimizations
163
+
164
+ - Lazy loading for images
165
+ - Debounced search functionality
166
+ - Efficient database queries
167
+ - Minimal external dependencies
168
+ - Optimized CSS and JavaScript
169
+ - Responsive image handling
170
+
171
+ ## Browser Support
172
+
173
+ - Chrome 60+
174
+ - Firefox 55+
175
+ - Safari 12+
176
+ - Edge 79+
177
+ - Mobile browsers (iOS Safari, Chrome Mobile)
178
+
179
+ ## Customization
180
+
181
+ ### Adding New Features
182
+
183
+ 1. Update the database schema if needed
184
+ 2. Modify the frontend components
185
+ 3. Update the admin dashboard
186
+ 4. Test thoroughly
187
+
188
+ ### Styling
189
+
190
+ - All styles are in `src/css/`
191
+ - Uses CSS custom properties for theming
192
+ - Responsive design with mobile-first approach
193
+ - Easy to customize colors and layouts
194
+
195
+ ### Database Extensions
196
+
197
+ - Add new tables as needed
198
+ - Update RLS policies
199
+ - Modify the JavaScript API calls
200
+ - Update admin interface
201
+
202
+ ## Deployment Options
203
+
204
+ ### Static Hosting
205
+
206
+ - Netlify
207
+ - Vercel
208
+ - GitHub Pages
209
+ - AWS S3 + CloudFront
210
+
211
+ ### Server Options
212
+
213
+ - Any static web server
214
+ - CDN for better performance
215
+ - Domain and SSL configuration
216
+
217
+ ## Contributing
218
+
219
+ 1. Fork the project
220
+ 2. Create a feature branch
221
+ 3. Make your changes
222
+ 4. Test thoroughly
223
+ 5. Submit a pull request
224
+
225
+ ## Support
226
+
227
+ For questions or support:
228
+
229
+ - Check the documentation
230
+ - Review the code comments
231
+ - Test with the provided sample data
232
+ - Ensure Supabase is properly configured
233
+
234
+ ## License
235
+
236
+ This project is open source and available under the MIT License.
237
+
238
+ ## Future Enhancements
239
+
240
+ - [ ] Payment gateway integration (Stripe, PayPal)
241
+ - [ ] Email notifications for donors
242
+ - [ ] Social sharing features
243
+ - [ ] Advanced analytics and reporting
244
+ - [ ] Multi-language support
245
+ - [ ] Recurring donations
246
+ - [ ] Charity verification system
247
+ - [ ] Mobile app development
248
+ - [ ] API for third-party integrations
249
+
250
+ ---
251
+
252
+ **Built with ā¤ļø for making the world a better place**
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "charity-hub",
3
+ "version": "1.0.0",
4
+ "description": "A modern charity donation platform built with HTML, CSS, JavaScript, and Supabase",
5
+ "main": "public/index.html",
6
+ "scripts": {
7
+ "start": "live-server public --port=3000",
8
+ "dev": "live-server public --port=3000 --watch=.",
9
+ "admin": "live-server admin --port=3001",
10
+ "build": "echo 'Build complete - static files ready for deployment'",
11
+ "deploy": "echo 'Deploy to your preferred static hosting service'"
12
+ },
13
+ "keywords": [
14
+ "charity",
15
+ "donations",
16
+ "nonprofit",
17
+ "fundraising",
18
+ "supabase",
19
+ "javascript",
20
+ "html",
21
+ "css"
22
+ ],
23
+ "author": "CharityHub Team",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/your-username/charity-hub.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/your-username/charity-hub/issues"
31
+ },
32
+ "homepage": "https://github.com/your-username/charity-hub#readme",
33
+ "devDependencies": {
34
+ "live-server": "^1.2.2"
35
+ },
36
+ "dependencies": {},
37
+ "engines": {
38
+ "node": ">=14.0.0"
39
+ }
40
+ }
@@ -0,0 +1,743 @@
1
+ // Admin Dashboard JavaScript
2
+ const SUPABASE_URL = "https://affhpjwojmovvrwxlnmn.supabase.co"
3
+ const SUPABASE_ANON_KEY =
4
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFmZmhwandvam1vdnZyd3hsbm1uIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjkwMTgyMjMsImV4cCI6MjA4NDU5NDIyM30.fN44OO4Plw-45fL7l-G1sSfliPvveN3t-gP1Pwm0LME"
5
+
6
+ // Initialize Supabase
7
+ const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
8
+
9
+ // Global state
10
+ let charities = []
11
+ let donations = []
12
+ let categories = []
13
+ let currentEditCharity = null
14
+ let currentEditCategory = null
15
+ let donationsChart = null
16
+
17
+ // DOM elements
18
+ const navLinks = document.querySelectorAll(".nav-link")
19
+ const contentSections = document.querySelectorAll(".content-section")
20
+ const pageTitle = document.getElementById("pageTitle")
21
+
22
+ // Modal elements
23
+ const charityModal = document.getElementById("charityModal")
24
+ const charityForm = document.getElementById("charityForm")
25
+ const categoryModal = document.getElementById("categoryModal")
26
+ const categoryForm = document.getElementById("categoryForm")
27
+
28
+ // Initialize admin dashboard
29
+ document.addEventListener("DOMContentLoaded", async () => {
30
+ await initializeAdmin()
31
+ })
32
+
33
+ async function initializeAdmin() {
34
+ try {
35
+ setupEventListeners()
36
+ await loadDashboardData()
37
+ showSection("dashboard")
38
+ } catch (error) {
39
+ console.error("Error initializing admin:", error)
40
+ showToast("Error loading admin dashboard", "error")
41
+ }
42
+ }
43
+
44
+ // Event Listeners
45
+ function setupEventListeners() {
46
+ // Navigation
47
+ navLinks.forEach((link) => {
48
+ link.addEventListener("click", (e) => {
49
+ e.preventDefault()
50
+ const section = link.dataset.section
51
+ showSection(section)
52
+ })
53
+ })
54
+
55
+ // Forms
56
+ charityForm.addEventListener("submit", handleCharitySubmit)
57
+ categoryForm.addEventListener("submit", handleCategorySubmit)
58
+
59
+ // Filters
60
+ document.getElementById("donationStatusFilter")?.addEventListener("change", loadDonations)
61
+ document.getElementById("donationDateFilter")?.addEventListener("change", loadDonations)
62
+ }
63
+
64
+ // Navigation
65
+ function showSection(sectionName) {
66
+ // Update nav links
67
+ navLinks.forEach((link) => {
68
+ link.classList.remove("active")
69
+ if (link.dataset.section === sectionName) {
70
+ link.classList.add("active")
71
+ }
72
+ })
73
+
74
+ // Update content sections
75
+ contentSections.forEach((section) => {
76
+ section.classList.remove("active")
77
+ })
78
+ document.getElementById(sectionName).classList.add("active")
79
+
80
+ // Update page title
81
+ const titles = {
82
+ dashboard: "Dashboard",
83
+ charities: "Charities",
84
+ donations: "Donations",
85
+ categories: "Categories",
86
+ settings: "Settings",
87
+ }
88
+ pageTitle.textContent = titles[sectionName] || "Admin"
89
+
90
+ // Load section data
91
+ loadSectionData(sectionName)
92
+ }
93
+
94
+ // Load section data
95
+ async function loadSectionData(section) {
96
+ try {
97
+ switch (section) {
98
+ case "dashboard":
99
+ await loadDashboardData()
100
+ break
101
+ case "charities":
102
+ await loadCharities()
103
+ break
104
+ case "donations":
105
+ await loadDonations()
106
+ break
107
+ case "categories":
108
+ await loadCategories()
109
+ break
110
+ }
111
+ } catch (error) {
112
+ console.error(`Error loading ${section} data:`, error)
113
+ showToast(`Error loading ${section} data`, "error")
114
+ }
115
+ }
116
+
117
+ // Load dashboard data
118
+ async function loadDashboardData() {
119
+ try {
120
+ // Load all data
121
+ const [charitiesData, donationsData, categoriesData] = await Promise.all([
122
+ loadCharitiesData(),
123
+ loadDonationsData(),
124
+ loadCategoriesData(),
125
+ ])
126
+
127
+ // Update stats
128
+ updateDashboardStats(charitiesData, donationsData)
129
+
130
+ // Update charts
131
+ updateDonationsChart(donationsData)
132
+ updateTopCharities(charitiesData)
133
+ updateRecentActivity(donationsData, charitiesData)
134
+ } catch (error) {
135
+ console.error("Error loading dashboard data:", error)
136
+ throw error
137
+ }
138
+ }
139
+
140
+ // Update dashboard stats
141
+ function updateDashboardStats(charitiesData, donationsData) {
142
+ const totalCharities = charitiesData.length
143
+ const totalDonations = donationsData
144
+ .filter((d) => d.payment_status === "completed")
145
+ .reduce((sum, d) => sum + parseFloat(d.amount), 0)
146
+ const totalDonors = new Set(donationsData.filter((d) => d.payment_status === "completed").map((d) => d.donor_email))
147
+ .size
148
+ const avgDonation = totalDonors > 0 ? totalDonations / totalDonors : 0
149
+
150
+ document.getElementById("totalCharitiesStat").textContent = totalCharities
151
+ document.getElementById("totalDonationsStat").textContent = `$${totalDonations.toLocaleString()}`
152
+ document.getElementById("totalDonorsStat").textContent = totalDonors
153
+ document.getElementById("avgDonationStat").textContent = `$${avgDonation.toFixed(2)}`
154
+ }
155
+
156
+ // Update donations chart
157
+ function updateDonationsChart(donationsData) {
158
+ const ctx = document.getElementById("donationsChart")
159
+ if (!ctx) return
160
+
161
+ // Group donations by date (last 7 days)
162
+ const last7Days = []
163
+ const donationAmounts = []
164
+
165
+ for (let i = 6; i >= 0; i--) {
166
+ const date = new Date()
167
+ date.setDate(date.getDate() - i)
168
+ const dateStr = date.toISOString().split("T")[0]
169
+ last7Days.push(date.toLocaleDateString("en-US", { weekday: "short" }))
170
+
171
+ const dayTotal = donationsData
172
+ .filter((d) => d.payment_status === "completed" && d.created_at.startsWith(dateStr))
173
+ .reduce((sum, d) => sum + parseFloat(d.amount), 0)
174
+ donationAmounts.push(dayTotal)
175
+ }
176
+
177
+ // Destroy existing chart
178
+ if (donationsChart) {
179
+ donationsChart.destroy()
180
+ }
181
+
182
+ // Create new chart
183
+ donationsChart = new Chart(ctx, {
184
+ type: "line",
185
+ data: {
186
+ labels: last7Days,
187
+ datasets: [
188
+ {
189
+ label: "Daily Donations",
190
+ data: donationAmounts,
191
+ borderColor: "#e74c3c",
192
+ backgroundColor: "rgba(231, 76, 60, 0.1)",
193
+ tension: 0.4,
194
+ },
195
+ ],
196
+ },
197
+ options: {
198
+ responsive: true,
199
+ maintainAspectRatio: false,
200
+ plugins: {
201
+ legend: {
202
+ display: false,
203
+ },
204
+ },
205
+ scales: {
206
+ y: {
207
+ beginAtZero: true,
208
+ ticks: {
209
+ callback: function (value) {
210
+ return "$" + value.toLocaleString()
211
+ },
212
+ },
213
+ },
214
+ },
215
+ },
216
+ })
217
+ }
218
+
219
+ // Update top charities
220
+ function updateTopCharities(charitiesData) {
221
+ const topCharities = charitiesData
222
+ .sort((a, b) => parseFloat(b.current_amount) - parseFloat(a.current_amount))
223
+ .slice(0, 5)
224
+
225
+ const container = document.getElementById("topCharities")
226
+ container.innerHTML = topCharities
227
+ .map(
228
+ (charity) => `
229
+ <div class="top-charity">
230
+ <span class="top-charity-name">${charity.name}</span>
231
+ <span class="top-charity-amount">$${parseFloat(charity.current_amount).toLocaleString()}</span>
232
+ </div>
233
+ `,
234
+ )
235
+ .join("")
236
+ }
237
+
238
+ // Update recent activity
239
+ function updateRecentActivity(donationsData, charitiesData) {
240
+ const recentDonations = donationsData.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)).slice(0, 10)
241
+
242
+ const container = document.getElementById("recentActivity")
243
+ container.innerHTML = recentDonations
244
+ .map((donation) => {
245
+ const charity = charitiesData.find((c) => c.id === donation.charity_id)
246
+ const timeAgo = getTimeAgo(new Date(donation.created_at))
247
+
248
+ return `
249
+ <div class="activity-item">
250
+ <div class="activity-icon activity-donation">
251
+ <i class="fas fa-hand-holding-usd"></i>
252
+ </div>
253
+ <div class="activity-details">
254
+ <div class="activity-title">
255
+ ${donation.is_anonymous ? "Anonymous" : donation.donor_name} donated $${donation.amount} to ${charity?.name || "Unknown"}
256
+ </div>
257
+ <div class="activity-time">${timeAgo}</div>
258
+ </div>
259
+ </div>
260
+ `
261
+ })
262
+ .join("")
263
+ }
264
+
265
+ // Load charities
266
+ async function loadCharities() {
267
+ try {
268
+ const data = await loadCharitiesData()
269
+ renderCharitiesTable(data)
270
+ } catch (error) {
271
+ console.error("Error loading charities:", error)
272
+ throw error
273
+ }
274
+ }
275
+
276
+ // Load charities data
277
+ async function loadCharitiesData() {
278
+ const { data, error } = await supabase.from("charities").select("*").order("created_at", { ascending: false })
279
+
280
+ if (error) throw error
281
+ charities = data
282
+ return data
283
+ }
284
+
285
+ // Render charities table
286
+ function renderCharitiesTable(charitiesData) {
287
+ const tbody = document.querySelector("#charitiesTable tbody")
288
+ tbody.innerHTML = charitiesData
289
+ .map((charity) => {
290
+ const progress = (parseFloat(charity.current_amount) / parseFloat(charity.goal_amount)) * 100
291
+
292
+ return `
293
+ <tr>
294
+ <td>${charity.name}</td>
295
+ <td>${charity.category}</td>
296
+ <td>$${parseFloat(charity.goal_amount).toLocaleString()}</td>
297
+ <td>$${parseFloat(charity.current_amount).toLocaleString()}</td>
298
+ <td>
299
+ <div class="progress-bar">
300
+ <div class="progress-fill" style="width: ${Math.min(progress, 100)}%"></div>
301
+ </div>
302
+ ${progress.toFixed(1)}%
303
+ </td>
304
+ <td>
305
+ <span class="status-badge ${charity.is_active ? "status-active" : "status-inactive"}">
306
+ ${charity.is_active ? "Active" : "Inactive"}
307
+ </span>
308
+ </td>
309
+ <td>
310
+ <button class="btn btn-sm btn-secondary" onclick="editCharity('${charity.id}')">
311
+ <i class="fas fa-edit"></i>
312
+ </button>
313
+ <button class="btn btn-sm btn-danger" onclick="deleteCharity('${charity.id}')">
314
+ <i class="fas fa-trash"></i>
315
+ </button>
316
+ </td>
317
+ </tr>
318
+ `
319
+ })
320
+ .join("")
321
+ }
322
+
323
+ // Load donations
324
+ async function loadDonations() {
325
+ try {
326
+ const data = await loadDonationsData()
327
+ renderDonationsTable(data)
328
+ } catch (error) {
329
+ console.error("Error loading donations:", error)
330
+ throw error
331
+ }
332
+ }
333
+
334
+ // Load donations data
335
+ async function loadDonationsData() {
336
+ let query = supabase.from("donations").select("*, charities(name)").order("created_at", { ascending: false })
337
+
338
+ // Apply filters
339
+ const statusFilter = document.getElementById("donationStatusFilter")?.value
340
+ if (statusFilter) {
341
+ query = query.eq("payment_status", statusFilter)
342
+ }
343
+
344
+ const dateFilter = document.getElementById("donationDateFilter")?.value
345
+ if (dateFilter) {
346
+ query = query.gte("created_at", dateFilter)
347
+ query = query.lt("created_at", new Date(dateFilter).getTime() + 24 * 60 * 60 * 1000)
348
+ }
349
+
350
+ const { data, error } = await query
351
+ if (error) throw error
352
+ donations = data
353
+ return data
354
+ }
355
+
356
+ // Render donations table
357
+ function renderDonationsTable(donationsData) {
358
+ const tbody = document.querySelector("#donationsTable tbody")
359
+ tbody.innerHTML = donationsData
360
+ .map(
361
+ (donation) => `
362
+ <tr>
363
+ <td>${donation.id.substring(0, 8)}...</td>
364
+ <td>${donation.charities?.name || "Unknown"}</td>
365
+ <td>${donation.is_anonymous ? "Anonymous" : donation.donor_name}</td>
366
+ <td>$${parseFloat(donation.amount).toLocaleString()}</td>
367
+ <td>
368
+ <span class="status-badge status-${donation.payment_status}">
369
+ ${donation.payment_status}
370
+ </span>
371
+ </td>
372
+ <td>${new Date(donation.created_at).toLocaleDateString()}</td>
373
+ <td>
374
+ <button class="btn btn-sm btn-secondary" onclick="viewDonation('${donation.id}')">
375
+ <i class="fas fa-eye"></i>
376
+ </button>
377
+ <button class="btn btn-sm btn-danger" onclick="deleteDonation('${donation.id}')">
378
+ <i class="fas fa-trash"></i>
379
+ </button>
380
+ </td>
381
+ </tr>
382
+ `,
383
+ )
384
+ .join("")
385
+ }
386
+
387
+ // Load categories
388
+ async function loadCategories() {
389
+ try {
390
+ const data = await loadCategoriesData()
391
+ renderCategoriesGrid(data)
392
+ populateCategorySelect(data)
393
+ } catch (error) {
394
+ console.error("Error loading categories:", error)
395
+ throw error
396
+ }
397
+ }
398
+
399
+ // Load categories data
400
+ async function loadCategoriesData() {
401
+ const { data, error } = await supabase.from("categories").select("*").order("name")
402
+
403
+ if (error) throw error
404
+ categories = data
405
+ return data
406
+ }
407
+
408
+ // Render categories grid
409
+ function renderCategoriesGrid(categoriesData) {
410
+ const container = document.getElementById("categoriesGrid")
411
+ container.innerHTML = categoriesData
412
+ .map(
413
+ (category) => `
414
+ <div class="category-card">
415
+ <div class="category-header">
416
+ <h3 class="category-name">${category.name}</h3>
417
+ <div>
418
+ <button class="btn btn-sm btn-secondary" onclick="editCategory('${category.id}')">
419
+ <i class="fas fa-edit"></i>
420
+ </button>
421
+ <button class="btn btn-sm btn-danger" onclick="deleteCategory('${category.id}')">
422
+ <i class="fas fa-trash"></i>
423
+ </button>
424
+ </div>
425
+ </div>
426
+ <p class="category-description">${category.description || "No description"}</p>
427
+ <div class="category-stats">
428
+ <span>Created: ${new Date(category.created_at).toLocaleDateString()}</span>
429
+ </div>
430
+ </div>
431
+ `,
432
+ )
433
+ .join("")
434
+ }
435
+
436
+ // Populate category select
437
+ function populateCategorySelect(categoriesData) {
438
+ const select = document.getElementById("charityCategory")
439
+ if (!select) return
440
+
441
+ select.innerHTML =
442
+ '<option value="">Select Category</option>' +
443
+ categoriesData
444
+ .map(
445
+ (category) => `
446
+ <option value="${category.name}">${category.name}</option>
447
+ `,
448
+ )
449
+ .join("")
450
+ }
451
+
452
+ // Charity Modal Functions
453
+ function openCharityModal(charity = null) {
454
+ currentEditCharity = charity
455
+ const modalTitle = document.getElementById("charityModalTitle")
456
+ const form = charityForm
457
+
458
+ if (charity) {
459
+ modalTitle.textContent = "Edit Charity"
460
+ form.charityName.value = charity.name
461
+ form.charityDescription.value = charity.description
462
+ form.charityCategory.value = charity.category
463
+ form.charityGoal.value = charity.goal_amount
464
+ form.charityImage.value = charity.image_url || ""
465
+ form.charityWebsite.value = charity.website_url || ""
466
+ form.charityActive.checked = charity.is_active
467
+ } else {
468
+ modalTitle.textContent = "Add Charity"
469
+ form.reset()
470
+ form.charityActive.checked = true
471
+ }
472
+
473
+ charityModal.classList.add("active")
474
+ }
475
+
476
+ function closeCharityModal() {
477
+ charityModal.classList.remove("active")
478
+ charityForm.reset()
479
+ currentEditCharity = null
480
+ }
481
+
482
+ async function handleCharitySubmit(e) {
483
+ e.preventDefault()
484
+
485
+ const formData = new FormData(charityForm)
486
+ const charityData = {
487
+ name: formData.get("charityName") || document.getElementById("charityName").value,
488
+ description: formData.get("charityDescription") || document.getElementById("charityDescription").value,
489
+ category: formData.get("charityCategory") || document.getElementById("charityCategory").value,
490
+ goal_amount: parseFloat(formData.get("charityGoal") || document.getElementById("charityGoal").value),
491
+ image_url: formData.get("charityImage") || document.getElementById("charityImage").value,
492
+ website_url: formData.get("charityWebsite") || document.getElementById("charityWebsite").value,
493
+ is_active: (formData.get("charityActive") || document.getElementById("charityActive")).checked,
494
+ }
495
+
496
+ try {
497
+ if (currentEditCharity) {
498
+ // Update existing charity
499
+ const { error } = await supabase.from("charities").update(charityData).eq("id", currentEditCharity.id)
500
+
501
+ if (error) throw error
502
+ showToast("Charity updated successfully", "success")
503
+ } else {
504
+ // Insert new charity
505
+ const { error } = await supabase.from("charities").insert([charityData])
506
+
507
+ if (error) throw error
508
+ showToast("Charity added successfully", "success")
509
+ }
510
+
511
+ closeCharityModal()
512
+ await loadCharities()
513
+ await loadDashboardData()
514
+ } catch (error) {
515
+ console.error("Error saving charity:", error)
516
+ showToast("Error saving charity", "error")
517
+ }
518
+ }
519
+
520
+ // Category Modal Functions
521
+ function openCategoryModal(category = null) {
522
+ currentEditCategory = category
523
+ const modalTitle = document.getElementById("categoryModalTitle")
524
+ const form = categoryForm
525
+
526
+ if (category) {
527
+ modalTitle.textContent = "Edit Category"
528
+ form.categoryName.value = category.name
529
+ form.categoryDescription.value = category.description || ""
530
+ } else {
531
+ modalTitle.textContent = "Add Category"
532
+ form.reset()
533
+ }
534
+
535
+ categoryModal.classList.add("active")
536
+ }
537
+
538
+ function closeCategoryModal() {
539
+ categoryModal.classList.remove("active")
540
+ categoryForm.reset()
541
+ currentEditCategory = null
542
+ }
543
+
544
+ async function handleCategorySubmit(e) {
545
+ e.preventDefault()
546
+
547
+ const formData = new FormData(categoryForm)
548
+ const categoryData = {
549
+ name: formData.get("categoryName") || document.getElementById("categoryName").value,
550
+ description: formData.get("categoryDescription") || document.getElementById("categoryDescription").value,
551
+ }
552
+
553
+ try {
554
+ if (currentEditCategory) {
555
+ // Update existing category
556
+ const { error } = await supabase.from("categories").update(categoryData).eq("id", currentEditCategory.id)
557
+
558
+ if (error) throw error
559
+ showToast("Category updated successfully", "success")
560
+ } else {
561
+ // Insert new category
562
+ const { error } = await supabase.from("categories").insert([categoryData])
563
+
564
+ if (error) throw error
565
+ showToast("Category added successfully", "success")
566
+ }
567
+
568
+ closeCategoryModal()
569
+ await loadCategories()
570
+ await loadDashboardData()
571
+ } catch (error) {
572
+ console.error("Error saving category:", error)
573
+ showToast("Error saving category", "error")
574
+ }
575
+ }
576
+
577
+ // Edit functions
578
+ async function editCharity(charityId) {
579
+ try {
580
+ const { data, error } = await supabase.from("charities").select("*").eq("id", charityId).single()
581
+
582
+ if (error) throw error
583
+ openCharityModal(data)
584
+ } catch (error) {
585
+ console.error("Error loading charity:", error)
586
+ showToast("Error loading charity", "error")
587
+ }
588
+ }
589
+
590
+ async function editCategory(categoryId) {
591
+ try {
592
+ const { data, error } = await supabase.from("categories").select("*").eq("id", categoryId).single()
593
+
594
+ if (error) throw error
595
+ openCategoryModal(data)
596
+ } catch (error) {
597
+ console.error("Error loading category:", error)
598
+ showToast("Error loading category", "error")
599
+ }
600
+ }
601
+
602
+ // Delete functions
603
+ async function deleteCharity(charityId) {
604
+ if (!confirm("Are you sure you want to delete this charity?")) return
605
+
606
+ try {
607
+ const { error } = await supabase.from("charities").delete().eq("id", charityId)
608
+
609
+ if (error) throw error
610
+ showToast("Charity deleted successfully", "success")
611
+ await loadCharities()
612
+ await loadDashboardData()
613
+ } catch (error) {
614
+ console.error("Error deleting charity:", error)
615
+ showToast("Error deleting charity", "error")
616
+ }
617
+ }
618
+
619
+ async function deleteCategory(categoryId) {
620
+ if (!confirm("Are you sure you want to delete this category?")) return
621
+
622
+ try {
623
+ const { error } = await supabase.from("categories").delete().eq("id", categoryId)
624
+
625
+ if (error) throw error
626
+ showToast("Category deleted successfully", "success")
627
+ await loadCategories()
628
+ await loadDashboardData()
629
+ } catch (error) {
630
+ console.error("Error deleting category:", error)
631
+ showToast("Error deleting category", "error")
632
+ }
633
+ }
634
+
635
+ async function deleteDonation(donationId) {
636
+ if (!confirm("Are you sure you want to delete this donation?")) return
637
+
638
+ try {
639
+ const { error } = await supabase.from("donations").delete().eq("id", donationId)
640
+
641
+ if (error) throw error
642
+ showToast("Donation deleted successfully", "success")
643
+ await loadDonations()
644
+ await loadDashboardData()
645
+ } catch (error) {
646
+ console.error("Error deleting donation:", error)
647
+ showToast("Error deleting donation", "error")
648
+ }
649
+ }
650
+
651
+ // View donation
652
+ async function viewDonation(donationId) {
653
+ try {
654
+ const { data, error } = await supabase.from("donations").select("*, charities(*)").eq("id", donationId).single()
655
+
656
+ if (error) throw error
657
+
658
+ alert(
659
+ `Donation Details:\n\n` +
660
+ `ID: ${data.id}\n` +
661
+ `Charity: ${data.charities?.name}\n` +
662
+ `Donor: ${data.is_anonymous ? "Anonymous" : data.donor_name}\n` +
663
+ `Email: ${data.donor_email}\n` +
664
+ `Amount: $${data.amount}\n` +
665
+ `Message: ${data.message || "None"}\n` +
666
+ `Status: ${data.payment_status}\n` +
667
+ `Date: ${new Date(data.created_at).toLocaleString()}`,
668
+ )
669
+ } catch (error) {
670
+ console.error("Error loading donation:", error)
671
+ showToast("Error loading donation", "error")
672
+ }
673
+ }
674
+
675
+ // Utility functions
676
+ function getTimeAgo(date) {
677
+ const seconds = Math.floor((new Date() - date) / 1000)
678
+
679
+ let interval = seconds / 31536000
680
+ if (interval > 1) return Math.floor(interval) + " years ago"
681
+
682
+ interval = seconds / 2592000
683
+ if (interval > 1) return Math.floor(interval) + " months ago"
684
+
685
+ interval = seconds / 86400
686
+ if (interval > 1) return Math.floor(interval) + " days ago"
687
+
688
+ interval = seconds / 3600
689
+ if (interval > 1) return Math.floor(interval) + " hours ago"
690
+
691
+ interval = seconds / 60
692
+ if (interval > 1) return Math.floor(interval) + " minutes ago"
693
+
694
+ return "Just now"
695
+ }
696
+
697
+ function showToast(message, type = "success") {
698
+ const toast = document.createElement("div")
699
+ toast.className = `toast ${type}`
700
+ toast.textContent = message
701
+
702
+ document.body.appendChild(toast)
703
+
704
+ setTimeout(() => {
705
+ toast.classList.add("show")
706
+ }, 100)
707
+
708
+ setTimeout(() => {
709
+ toast.classList.remove("show")
710
+ setTimeout(() => {
711
+ document.body.removeChild(toast)
712
+ }, 300)
713
+ }, 3000)
714
+ }
715
+
716
+ // Export functions
717
+ function exportData() {
718
+ // In a real implementation, this would export data to CSV/Excel
719
+ showToast("Export functionality coming soon", "warning")
720
+ }
721
+
722
+ async function refreshData() {
723
+ try {
724
+ await loadDashboardData()
725
+ showToast("Data refreshed successfully", "success")
726
+ } catch (error) {
727
+ showToast("Error refreshing data", "error")
728
+ }
729
+ }
730
+
731
+ // Global functions
732
+ window.openCharityModal = openCharityModal
733
+ window.closeCharityModal = closeCharityModal
734
+ window.openCategoryModal = openCategoryModal
735
+ window.closeCategoryModal = closeCategoryModal
736
+ window.editCharity = editCharity
737
+ window.editCategory = editCategory
738
+ window.deleteCharity = deleteCharity
739
+ window.deleteCategory = deleteCategory
740
+ window.deleteDonation = deleteDonation
741
+ window.viewDonation = viewDonation
742
+ window.exportData = exportData
743
+ window.refreshData = refreshData
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "1.1.31",
3
+ "version": "1.1.34",
4
4
  "name": "easc-cli",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,107 +23,13 @@
23
23
  "exports": {
24
24
  "./*": "./src/*.ts"
25
25
  },
26
- "devDependencies": {
27
- "@babel/core": "7.28.4",
28
- "@octokit/webhooks-types": "7.6.1",
29
- "@eliseart.ai/script": "workspace:*",
30
- "@parcel/watcher-darwin-arm64": "2.5.1",
31
- "@parcel/watcher-darwin-x64": "2.5.1",
32
- "@parcel/watcher-linux-arm64-glibc": "2.5.1",
33
- "@parcel/watcher-linux-arm64-musl": "2.5.1",
34
- "@parcel/watcher-linux-x64-glibc": "2.5.1",
35
- "@parcel/watcher-linux-x64-musl": "2.5.1",
36
- "@parcel/watcher-linux-x64-glibc": "2.5.1",
37
- "@parcel/watcher-linux-x64-musl": "2.5.1",
38
- "@parcel/watcher-win32-x64": "2.5.1",
39
- "@standard-schema/spec": "1.0.0",
40
- "@tsconfig/bun": "catalog:",
41
- "@types/babel__core": "7.20.5",
42
- "@types/bun": "catalog:",
43
- "@types/turndown": "5.0.5",
44
- "@types/yargs": "17.0.33",
45
- "@typescript/native-preview": "catalog:",
46
- "typescript": "catalog:",
47
- "vscode-languageserver-types": "3.17.5",
48
- "why-is-node-running": "3.2.2",
49
- "zod-to-json-schema": "3.24.5",
50
- "@actions/core": "1.11.1",
51
- "@actions/github": "6.0.1",
52
- "@agentclientprotocol/sdk": "0.5.1",
53
- "@ai-sdk/amazon-bedrock": "3.0.73",
54
- "@ai-sdk/anthropic": "2.0.57",
55
- "@ai-sdk/azure": "2.0.91",
56
- "@ai-sdk/cerebras": "1.0.34",
57
- "@ai-sdk/cohere": "2.0.22",
58
- "@ai-sdk/deepinfra": "1.0.31",
59
- "@ai-sdk/gateway": "2.0.25",
60
- "@ai-sdk/google": "2.0.52",
61
- "@ai-sdk/google-vertex": "3.0.97",
62
- "@ai-sdk/groq": "2.0.34",
63
- "@ai-sdk/mistral": "2.0.27",
64
- "@ai-sdk/openai": "2.0.89",
65
- "@ai-sdk/openai-compatible": "1.0.30",
66
- "@ai-sdk/perplexity": "2.0.23",
67
- "@ai-sdk/provider": "2.0.1",
68
- "@ai-sdk/provider-utils": "3.0.20",
69
- "@ai-sdk/togetherai": "1.0.31",
70
- "@ai-sdk/vercel": "1.0.31",
71
- "@ai-sdk/xai": "2.0.51",
72
- "@clack/prompts": "1.0.0-alpha.1",
73
- "@eliseart.ai/plugin": "workspace:*",
74
- "@eliseart.ai/sdk": "workspace:*",
75
- "@eliseart.ai/util": "workspace:*",
76
- "@gitlab/gitlab-ai-provider": "3.1.2",
77
- "@hono/standard-validator": "0.1.5",
78
- "@hono/zod-validator": "catalog:",
79
- "@modelcontextprotocol/sdk": "1.25.2",
80
- "@octokit/graphql": "9.0.2",
81
- "@octokit/rest": "catalog:",
82
- "@openauthjs/openauth": "catalog:",
83
- "@openrouter/ai-sdk-provider": "1.5.2",
84
- "@opentui/core": "0.1.74",
85
- "@opentui/solid": "0.1.74",
86
- "@parcel/watcher": "2.5.1",
87
- "@pierre/diffs": "catalog:",
88
- "@solid-primitives/event-bus": "1.1.2",
89
- "@solid-primitives/scheduled": "1.5.2",
90
- "@zip.js/zip.js": "2.7.62",
91
- "ai": "catalog:",
92
- "bonjour-service": "1.3.0",
93
- "bun-pty": "0.4.4",
94
- "chokidar": "4.0.3",
95
- "clipboardy": "4.0.0",
96
- "decimal.js": "10.5.0",
97
- "diff": "catalog:",
98
- "fuzzysort": "3.1.0",
99
- "gray-matter": "4.0.3",
100
- "hono": "catalog:",
101
- "hono-openapi": "catalog:",
102
- "ignore": "7.0.5",
103
- "jsonc-parser": "3.3.1",
104
- "minimatch": "10.0.3",
105
- "open": "10.1.2",
106
- "opentui-spinner": "0.0.6",
107
- "partial-json": "0.1.7",
108
- "remeda": "catalog:",
109
- "solid-js": "catalog:",
110
- "strip-ansi": "7.1.2",
111
- "tree-sitter-bash": "0.25.0",
112
- "turndown": "7.2.0",
113
- "ulid": "catalog:",
114
- "vscode-jsonrpc": "8.2.1",
115
- "web-tree-sitter": "0.25.10",
116
- "xdg-basedir": "5.1.0",
117
- "yargs": "18.0.0",
118
- "zod": "catalog:",
119
- "zod-to-json-schema": "3.24.5"
120
- },
26
+ "devDependencies": {},
121
27
  "dependencies": {},
122
28
  "optionalDependencies": {
123
- "easc-linux-x64": "0.0.0-main-202601221753",
124
- "easc-linux-arm64": "0.0.0-main-202601221753",
125
- "easc-darwin-x64": "0.0.0-main-202601221753",
126
- "easc-darwin-arm64": "0.0.0-main-202601221753",
127
- "easc-windows-x64": "0.0.0-main-202601221753"
29
+ "easc-linux-x64": "0.0.0-main-202601241225",
30
+ "easc-linux-arm64": "0.0.0-main-202601241225",
31
+ "easc-darwin-x64": "0.0.0-main-202601241225",
32
+ "easc-darwin-arm64": "0.0.0-main-202601241225",
33
+ "easc-windows-x64": "0.0.0-main-202601241225"
128
34
  }
129
35
  }
package/script/build.ts CHANGED
@@ -108,7 +108,7 @@ if (!skipInstall) {
108
108
  }
109
109
  for (const item of targets) {
110
110
  const name = [
111
- pkg.name,
111
+ "easc",
112
112
  // changing to win32 flags npm for some reason
113
113
  item.os === "win32" ? "windows" : item.os,
114
114
  item.arch,
@@ -138,7 +138,7 @@ for (const item of targets) {
138
138
  //@ts-ignore (bun types aren't up to date)
139
139
  autoloadTsconfig: true,
140
140
  autoloadPackageJson: true,
141
- target: name.replace(pkg.name, "bun") as any,
141
+ target: name.replace("easc", "bun") as any,
142
142
  outfile: `dist/${name}/bin/easc`,
143
143
  execArgv: [`--user-agent=easc/${Script.version}`, "--use-system-ca", "--"],
144
144
  windows: {},
package/script/deploy.ts CHANGED
@@ -49,8 +49,36 @@ for (const pkgName of packages) {
49
49
  }
50
50
  }
51
51
 
52
- // 3. Publish the main wrapper package
53
- console.log(`\nšŸ“¤ Publishing main wrapper package (easc@${pkg.version})...`)
52
+ // 3. Update main package.json optionalDependencies
53
+ console.log(`\nšŸ”„ Updating optionalDependencies in package.json...`)
54
+ const linuxPkg = JSON.parse(fs.readFileSync(path.join(distDir, "easc-linux-x64", "package.json"), "utf-8"))
55
+ const binaryVersion = linuxPkg.version
56
+
57
+ const mainPkgPath = path.join(root, "package.json")
58
+ const mainPkg = JSON.parse(fs.readFileSync(mainPkgPath, "utf-8"))
59
+
60
+ const platforms = [
61
+ "easc-linux-x64", "easc-linux-arm64",
62
+ "easc-darwin-x64", "easc-darwin-arm64",
63
+ "easc-windows-x64"
64
+ ]
65
+
66
+ for (const p of platforms) {
67
+ mainPkg.optionalDependencies[p] = binaryVersion
68
+ }
69
+
70
+ fs.writeFileSync(mainPkgPath, JSON.stringify(mainPkg, null, 2))
71
+ console.log(` Updated optionalDependencies to ${binaryVersion}`)
72
+
73
+ // 4. Publish the main wrapper package
74
+ console.log(`\nšŸ“¤ Publishing main wrapper package (easc-cli@${pkg.version})...`)
75
+
76
+ // Strip dependencies and devDependencies before publishing
77
+ // The wrapper package only needs optionalDependencies for the platform binaries
78
+ mainPkg.dependencies = {}
79
+ mainPkg.devDependencies = {}
80
+ fs.writeFileSync(mainPkgPath, JSON.stringify(mainPkg, null, 2))
81
+
54
82
  try {
55
83
  // Ensure we are in the root of the package
56
84
  await $`npm publish --access public`