Levohug commited on
Commit
961544c
·
verified ·
1 Parent(s): fdd232f

can you use data input from google sheets and make it into a user friendly app to access the information in the sheets?

Browse files
Files changed (2) hide show
  1. README.md +8 -5
  2. index.html +465 -18
README.md CHANGED
@@ -1,10 +1,13 @@
1
  ---
2
- title: Sheetsync Explorer
3
- emoji: 🌖
4
- colorFrom: yellow
5
- colorTo: green
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
  ---
2
+ title: SheetSync Explorer 📊
3
+ colorFrom: blue
4
+ colorTo: yellow
5
+ emoji: 🐳
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite-v3
10
  ---
11
 
12
+ # Welcome to your new DeepSite project!
13
+ This project was created with [DeepSite](https://deepsite.hf.co).
index.html CHANGED
@@ -1,19 +1,466 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SheetSync Explorer</title>
7
+ <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://unpkg.com/feather-icons"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
11
+ <script src="https://cdn.jsdelivr.net/npm/vue@3.2.31/dist/vue.global.js"></script>
12
+ <style>
13
+ .fade-enter-active, .fade-leave-active {
14
+ transition: opacity 0.3s ease;
15
+ }
16
+ .fade-enter-from, .fade-leave-to {
17
+ opacity: 0;
18
+ }
19
+ .table-container {
20
+ max-height: 70vh;
21
+ overflow-y: auto;
22
+ }
23
+ ::-webkit-scrollbar {
24
+ width: 8px;
25
+ height: 8px;
26
+ }
27
+ ::-webkit-scrollbar-track {
28
+ background: #f1f1f1;
29
+ border-radius: 10px;
30
+ }
31
+ ::-webkit-scrollbar-thumb {
32
+ background: #888;
33
+ border-radius: 10px;
34
+ }
35
+ ::-webkit-scrollbar-thumb:hover {
36
+ background: #555;
37
+ }
38
+ </style>
39
+ </head>
40
+ <body class="bg-gray-50">
41
+ <div id="app">
42
+ <header class="bg-gradient-to-r from-indigo-600 to-purple-600 shadow-lg">
43
+ <div class="container mx-auto px-4 py-6">
44
+ <div class="flex justify-between items-center">
45
+ <div class="flex items-center space-x-2">
46
+ <i data-feather="grid" class="text-white w-8 h-8"></i>
47
+ <h1 class="text-2xl font-bold text-white">SheetSync Explorer</h1>
48
+ </div>
49
+ <div class="flex space-x-4">
50
+ <button @click="showConnectDialog = true" class="flex items-center space-x-2 bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-all duration-300">
51
+ <i data-feather="link" class="w-4 h-4"></i>
52
+ <span>Connect Sheet</span>
53
+ </button>
54
+ <button @click="showHelp = true" class="flex items-center space-x-2 bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-all duration-300">
55
+ <i data-feather="help-circle" class="w-4 h-4"></i>
56
+ <span>Help</span>
57
+ </button>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </header>
62
+
63
+ <main class="container mx-auto px-4 py-8">
64
+ <transition name="fade">
65
+ <div v-if="loading" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
66
+ <div class="bg-white p-6 rounded-lg shadow-xl flex flex-col items-center">
67
+ <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-600 mb-4"></div>
68
+ <p class="text-gray-700">Loading your sheet data...</p>
69
+ </div>
70
+ </div>
71
+ </transition>
72
+
73
+ <transition name="fade">
74
+ <div v-if="showConnectDialog" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
75
+ <div class="bg-white p-6 rounded-lg shadow-xl w-full max-w-lg">
76
+ <div class="flex justify-between items-center mb-4">
77
+ <h3 class="text-xl font-semibold text-gray-800">Connect Google Sheet</h3>
78
+ <button @click="showConnectDialog = false" class="text-gray-500 hover:text-gray-700">
79
+ <i data-feather="x" class="w-5 h-5"></i>
80
+ </button>
81
+ </div>
82
+ <div class="space-y-4">
83
+ <div>
84
+ <label class="block text-sm font-medium text-gray-700 mb-1">Sheet ID</label>
85
+ <input v-model="sheetId" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="e.g. 1XqFG...">
86
+ </div>
87
+ <div>
88
+ <label class="block text-sm font-medium text-gray-700 mb-1">Sheet Name</label>
89
+ <input v-model="sheetName" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="e.g. Sheet1">
90
+ </div>
91
+ <div>
92
+ <label class="block text-sm font-medium text-gray-700 mb-1">API Key (Optional)</label>
93
+ <input v-model="apiKey" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Your Google API key">
94
+ </div>
95
+ <button @click="fetchData" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white py-2 px-4 rounded-md transition-colors duration-300 flex items-center justify-center space-x-2">
96
+ <i data-feather="download" class="w-4 h-4"></i>
97
+ <span>Load Data</span>
98
+ </button>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </transition>
103
+
104
+ <div v-if="data.length > 0" class="bg-white rounded-xl shadow-md overflow-hidden">
105
+ <div class="p-4 border-b border-gray-200 flex justify-between items-center">
106
+ <h2 class="text-xl font-semibold text-gray-800 flex items-center space-x-2">
107
+ <i data-feather="file-text" class="text-indigo-600"></i>
108
+ <span>{{ sheetName || 'Your Sheet' }}</span>
109
+ </h2>
110
+ <div class="flex space-x-2">
111
+ <div class="relative">
112
+ <input v-model="searchQuery" type="text" placeholder="Search..." class="pl-8 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500">
113
+ <i data-feather="search" class="absolute left-2 top-2.5 text-gray-400 w-4 h-4"></i>
114
+ </div>
115
+ <button @click="exportToCSV" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors duration-300">
116
+ <i data-feather="download" class="w-4 h-4"></i>
117
+ <span>Export</span>
118
+ </button>
119
+ </div>
120
+ </div>
121
+
122
+ <div class="table-container">
123
+ <table class="min-w-full divide-y divide-gray-200">
124
+ <thead class="bg-gray-50 sticky top-0">
125
+ <tr>
126
+ <th v-for="(header, index) in headers" :key="index" scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer" @click="sortBy(header)">
127
+ <div class="flex items-center">
128
+ {{ header }}
129
+ <i data-feather="chevrons-up" v-if="sortColumn === header && sortDirection === 'asc'" class="w-3 h-3 ml-1 text-gray-400"></i>
130
+ <i data-feather="chevrons-down" v-if="sortColumn === header && sortDirection === 'desc'" class="w-3 h-3 ml-1 text-gray-400"></i>
131
+ </div>
132
+ </th>
133
+ </tr>
134
+ </thead>
135
+ <tbody class="bg-white divide-y divide-gray-200">
136
+ <tr v-for="(row, rowIndex) in filteredData" :key="rowIndex" :class="{'bg-gray-50': rowIndex % 2 === 0}">
137
+ <td v-for="(value, colIndex) in row" :key="colIndex" class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
138
+ {{ value }}
139
+ </td>
140
+ </tr>
141
+ </tbody>
142
+ </table>
143
+ </div>
144
+
145
+ <div class="px-4 py-3 bg-gray-50 border-t border-gray-200 flex items-center justify-between">
146
+ <div class="flex-1 flex justify-between sm:hidden">
147
+ <button @click="currentPage = Math.max(1, currentPage - 1)" :disabled="currentPage === 1" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
148
+ Previous
149
+ </button>
150
+ <button @click="currentPage = Math.min(totalPages, currentPage + 1)" :disabled="currentPage === totalPages" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
151
+ Next
152
+ </button>
153
+ </div>
154
+ <div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
155
+ <div>
156
+ <p class="text-sm text-gray-700">
157
+ Showing <span class="font-medium">{{ (currentPage - 1) * pageSize + 1 }}</span> to <span class="font-medium">{{ Math.min(currentPage * pageSize, filteredData.length) }}</span> of <span class="font-medium">{{ filteredData.length }}</span> results
158
+ </p>
159
+ </div>
160
+ <div>
161
+ <nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
162
+ <button @click="currentPage = Math.max(1, currentPage - 1)" :disabled="currentPage === 1" class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
163
+ <span class="sr-only">Previous</span>
164
+ <i data-feather="chevron-left" class="h-5 w-5"></i>
165
+ </button>
166
+ <button v-for="page in visiblePages" :key="page" @click="currentPage = page" :class="{'z-10 bg-indigo-50 border-indigo-500 text-indigo-600': currentPage === page, 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50': currentPage !== page}" class="relative inline-flex items-center px-4 py-2 border text-sm font-medium">
167
+ {{ page }}
168
+ </button>
169
+ <button @click="currentPage = Math.min(totalPages, currentPage + 1)" :disabled="currentPage === totalPages" class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
170
+ <span class="sr-only">Next</span>
171
+ <i data-feather="chevron-right" class="h-5 w-5"></i>
172
+ </button>
173
+ </nav>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+
179
+ <div v-else class="text-center py-12">
180
+ <div class="mx-auto max-w-md p-6 bg-white rounded-xl shadow-md">
181
+ <div class="flex flex-col items-center">
182
+ <i data-feather="file" class="w-12 h-12 text-gray-400 mb-4"></i>
183
+ <h3 class="text-lg font-medium text-gray-900 mb-2">No Sheet Data Loaded</h3>
184
+ <p class="text-gray-500 mb-4">Connect to a Google Sheet to view and explore your data</p>
185
+ <button @click="showConnectDialog = true" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors duration-300">
186
+ <i data-feather="link" class="w-4 h-4"></i>
187
+ <span>Connect Sheet</span>
188
+ </button>
189
+ </div>
190
+ </div>
191
+ </div>
192
+ </main>
193
+
194
+ <transition name="fade">
195
+ <div v-if="showHelp" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
196
+ <div class="bg-white p-6 rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] overflow-y-auto">
197
+ <div class="flex justify-between items-center mb-4">
198
+ <h3 class="text-xl font-semibold text-gray-800">Help & Instructions</h3>
199
+ <button @click="showHelp = false" class="text-gray-500 hover:text-gray-700">
200
+ <i data-feather="x" class="w-5 h-5"></i>
201
+ </button>
202
+ </div>
203
+ <div class="space-y-4 text-gray-700">
204
+ <div>
205
+ <h4 class="font-medium text-gray-900 mb-2">How to Connect Your Google Sheet</h4>
206
+ <ol class="list-decimal list-inside space-y-2">
207
+ <li>Open your Google Sheet and look at the URL. The ID is the long string between "/d/" and "/edit". Copy this ID.</li>
208
+ <li>Enter the Sheet ID in the connection dialog.</li>
209
+ <li>Specify the sheet name (usually "Sheet1" if you haven't renamed it).</li>
210
+ <li>For public sheets, you don't need an API key. For private sheets, you'll need to generate a Google API key.</li>
211
+ </ol>
212
+ </div>
213
+ <div>
214
+ <h4 class="font-medium text-gray-900 mb-2">Features</h4>
215
+ <ul class="list-disc list-inside space-y-2">
216
+ <li><strong>Sorting:</strong> Click on any column header to sort by that column.</li>
217
+ <li><strong>Search:</strong> Use the search box to filter rows across all columns.</li>
218
+ <li><strong>Pagination:</strong> Navigate through large datasets with the pagination controls.</li>
219
+ <li><strong>Export:</strong> Download your filtered data as a CSV file.</li>
220
+ </ul>
221
+ </div>
222
+ <div>
223
+ <h4 class="font-medium text-gray-900 mb-2">Example Public Sheet</h4>
224
+ <p class="text-sm bg-gray-100 p-2 rounded">Try this sample public sheet:</p>
225
+ <div class="bg-gray-50 p-3 rounded mt-2">
226
+ <p class="text-sm"><strong>Sheet ID:</strong> 1XqFG1RzO4Tuo6v3i2e_Ca6Q5u1B2tX7dL0b3J2m3m3k</p>
227
+ <p class="text-sm"><strong>Sheet Name:</strong> Sheet1</p>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ </div>
233
+ </transition>
234
+
235
+ <footer class="bg-gray-100 border-t border-gray-200 mt-12">
236
+ <div class="container mx-auto px-4 py-6">
237
+ <div class="flex flex-col md:flex-row justify-between items-center">
238
+ <div class="flex items-center space-x-2 mb-4 md:mb-0">
239
+ <i data-feather="grid" class="text-indigo-600 w-6 h-6"></i>
240
+ <span class="text-gray-700">SheetSync Explorer</span>
241
+ </div>
242
+ <div class="flex space-x-6">
243
+ <a href="#" class="text-gray-500 hover:text-indigo-600 transition-colors duration-300">
244
+ <i data-feather="github" class="w-5 h-5"></i>
245
+ </a>
246
+ <a href="#" class="text-gray-500 hover:text-indigo-600 transition-colors duration-300">
247
+ <i data-feather="twitter" class="w-5 h-5"></i>
248
+ </a>
249
+ <a href="#" class="text-gray-500 hover:text-indigo-600 transition-colors duration-300">
250
+ <i data-feather="mail" class="w-5 h-5"></i>
251
+ </a>
252
+ </div>
253
+ </div>
254
+ <div class="mt-4 text-center md:text-left text-sm text-gray-500">
255
+ <p>© 2023 SheetSync Explorer. All data remains in your browser.</p>
256
+ </div>
257
+ </div>
258
+ </footer>
259
+ </div>
260
+
261
+ <script>
262
+ const { createApp, ref, computed } = Vue;
263
+
264
+ createApp({
265
+ setup() {
266
+ // State
267
+ const sheetId = ref('');
268
+ const sheetName = ref('Sheet1');
269
+ const apiKey = ref('');
270
+ const showConnectDialog = ref(false);
271
+ const showHelp = ref(false);
272
+ const loading = ref(false);
273
+ const data = ref([]);
274
+ const headers = ref([]);
275
+ const searchQuery = ref('');
276
+ const sortColumn = ref('');
277
+ const sortDirection = ref('asc');
278
+ const currentPage = ref(1);
279
+ const pageSize = ref(10);
280
+
281
+ // Computed properties
282
+ const filteredData = computed(() => {
283
+ let result = [...data.value];
284
+
285
+ // Apply search filter
286
+ if (searchQuery.value) {
287
+ const query = searchQuery.value.toLowerCase();
288
+ result = result.filter(row =>
289
+ row.some(cell =>
290
+ String(cell).toLowerCase().includes(query)
291
+ )
292
+ );
293
+ }
294
+
295
+ // Apply sorting
296
+ if (sortColumn.value) {
297
+ const columnIndex = headers.value.indexOf(sortColumn.value);
298
+ if (columnIndex >= 0) {
299
+ result.sort((a, b) => {
300
+ const valA = a[columnIndex];
301
+ const valB = b[columnIndex];
302
+
303
+ // Try to compare as numbers if possible
304
+ const numA = Number(valA);
305
+ const numB = Number(valB);
306
+
307
+ if (!isNaN(numA) && !isNaN(numB)) {
308
+ return sortDirection.value === 'asc' ? numA - numB : numB - numA;
309
+ }
310
+
311
+ // Fall back to string comparison
312
+ const strA = String(valA).toLowerCase();
313
+ const strB = String(valB).toLowerCase();
314
+
315
+ if (strA < strB) return sortDirection.value === 'asc' ? -1 : 1;
316
+ if (strA > strB) return sortDirection.value === 'asc' ? 1 : -1;
317
+ return 0;
318
+ });
319
+ }
320
+ }
321
+
322
+ return result;
323
+ });
324
+
325
+ const paginatedData = computed(() => {
326
+ const start = (currentPage.value - 1) * pageSize.value;
327
+ const end = start + pageSize.value;
328
+ return filteredData.value.slice(start, end);
329
+ });
330
+
331
+ const totalPages = computed(() => {
332
+ return Math.ceil(filteredData.value.length / pageSize.value);
333
+ });
334
+
335
+ const visiblePages = computed(() => {
336
+ const pages = [];
337
+ const maxVisible = 5;
338
+ let start = Math.max(1, currentPage.value - Math.floor(maxVisible / 2));
339
+ let end = Math.min(totalPages.value, start + maxVisible - 1);
340
+
341
+ if (end - start + 1 < maxVisible) {
342
+ start = Math.max(1, end - maxVisible + 1);
343
+ }
344
+
345
+ for (let i = start; i <= end; i++) {
346
+ pages.push(i);
347
+ }
348
+
349
+ return pages;
350
+ });
351
+
352
+ // Methods
353
+ const fetchData = async () => {
354
+ try {
355
+ loading.value = true;
356
+
357
+ // Construct the API URL
358
+ let url = `https://sheets.googleapis.com/v4/spreadsheets/${sheetId.value}/values/${sheetName.value}?`;
359
+
360
+ if (apiKey.value) {
361
+ url += `key=${apiKey.value}`;
362
+ } else {
363
+ // For public sheets, we can use a proxy to avoid CORS issues
364
+ url = `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`;
365
+ }
366
+
367
+ const response = await fetch(url);
368
+ const json = await response.json();
369
+
370
+ // Handle proxy response
371
+ const result = apiKey.value ? json : JSON.parse(json.contents);
372
+
373
+ if (result.values && result.values.length > 0) {
374
+ headers.value = result.values[0];
375
+ data.value = result.values.slice(1);
376
+ } else {
377
+ throw new Error('No data found in the sheet');
378
+ }
379
+
380
+ showConnectDialog.value = false;
381
+ } catch (error) {
382
+ console.error('Error fetching sheet data:', error);
383
+ alert('Failed to load sheet data. Please check your Sheet ID and try again.');
384
+ } finally {
385
+ loading.value = false;
386
+ }
387
+ };
388
+
389
+ const sortBy = (column) => {
390
+ if (sortColumn.value === column) {
391
+ sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc';
392
+ } else {
393
+ sortColumn.value = column;
394
+ sortDirection.value = 'asc';
395
+ }
396
+ currentPage.value = 1;
397
+ };
398
+
399
+ const exportToCSV = () => {
400
+ if (data.value.length === 0) return;
401
+
402
+ let csv = headers.value.join(',') + '\n';
403
+
404
+ filteredData.value.forEach(row => {
405
+ csv += row.map(cell => {
406
+ // Escape quotes and wrap in quotes if contains comma or quote
407
+ if (typeof cell === 'string' && (cell.includes(',') || cell.includes('"'))) {
408
+ return `"${cell.replace(/"/g, '""')}"`;
409
+ }
410
+ return cell;
411
+ }).join(',') + '\n';
412
+ });
413
+
414
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
415
+ const url = URL.createObjectURL(blob);
416
+ const link = document.createElement('a');
417
+ link.href = url;
418
+ link.setAttribute('download', `${sheetName.value || 'sheet'}_export.csv`);
419
+ document.body.appendChild(link);
420
+ link.click();
421
+ document.body.removeChild(link);
422
+ };
423
+
424
+ // Initialize with an example sheet if needed
425
+ const initExample = () => {
426
+ if (data.value.length === 0 && !sheetId.value) {
427
+ sheetId.value = '1XqFG1RzO4Tuo6v3i2e_Ca6Q5u1B2tX7dL0b3J2m3m3k';
428
+ sheetName.value = 'Sheet1';
429
+ }
430
+ };
431
+
432
+ initExample();
433
+
434
+ return {
435
+ sheetId,
436
+ sheetName,
437
+ apiKey,
438
+ showConnectDialog,
439
+ showHelp,
440
+ loading,
441
+ data,
442
+ headers,
443
+ searchQuery,
444
+ sortColumn,
445
+ sortDirection,
446
+ currentPage,
447
+ pageSize,
448
+ filteredData,
449
+ paginatedData,
450
+ totalPages,
451
+ visiblePages,
452
+ fetchData,
453
+ sortBy,
454
+ exportToCSV
455
+ };
456
+ },
457
+ mounted() {
458
+ feather.replace();
459
+ },
460
+ updated() {
461
+ feather.replace();
462
+ }
463
+ }).mount('#app');
464
+ </script>
465
+ </body>
466
  </html>