Skip to content

Commit f7ae38d

Browse files
Merge pull request #17 from gitcoder89431/copilot/fix-13
🔄 Implement JSONL Data Export/Import System for Pet Data Backup
2 parents 81c0ee5 + f7b5e56 commit f7ae38d

File tree

5 files changed

+487
-0
lines changed

5 files changed

+487
-0
lines changed

src/lib/components/panels/GuardianPanel.svelte

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import { onMount } from 'svelte';
33
import ThemeSelector from '../ui/ThemeSelector.svelte';
4+
import DataManager from '../ui/DataManager.svelte';
45
import { User, Key, Settings, CheckCircle, AlertCircle } from 'lucide-svelte';
56
import { guardianStore, guardianHelpers } from '$lib/stores/guardian.js';
67
import { aiAnalysisHelpers } from '$lib/stores/ai-analysis.js';
@@ -14,6 +15,7 @@
1415
};
1516
1617
let apiKeyStatus = 'unchecked'; // unchecked, checking, valid, invalid
18+
let showDataManager = false;
1719
1820
onMount(() => {
1921
// Load saved guardian data
@@ -199,5 +201,22 @@
199201
</div>
200202
</div>
201203
</div>
204+
205+
<!-- Data Management Section -->
206+
<div class="section">
207+
<div class="flex items-center justify-between mb-2">
208+
<span class="text-sm font-medium" style="color: var(--petalytics-subtle);">Data Management</span>
209+
<button
210+
on:click={() => showDataManager = !showDataManager}
211+
class="text-xs px-2 py-1 rounded bg-gray-100 hover:bg-gray-200 text-gray-700 transition-colors"
212+
>
213+
{showDataManager ? 'Hide' : 'Show'} Export/Import
214+
</button>
215+
</div>
216+
217+
{#if showDataManager}
218+
<DataManager />
219+
{/if}
220+
</div>
202221
</div>
203222
</div>
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
<script lang="ts">
2+
import { Download, Upload, FileText, Database } from 'lucide-svelte';
3+
import { DataExporter } from '$lib/utils/data-export';
4+
import { petStore } from '$lib/stores/pets';
5+
import type { PetPanelData } from '$lib/types/Pet';
6+
7+
let isExporting = false;
8+
let isImporting = false;
9+
let importMessage = '';
10+
let importSuccess = false;
11+
let fileInput: HTMLInputElement;
12+
let pets: PetPanelData[] = [];
13+
14+
petStore.subscribe(value => { pets = value; });
15+
16+
async function exportAllData() {
17+
isExporting = true;
18+
try {
19+
await DataExporter.exportAllData();
20+
} catch (error) {
21+
alert('Export failed: ' + (error as Error).message);
22+
} finally {
23+
isExporting = false;
24+
}
25+
}
26+
27+
async function exportSinglePet(pet: PetPanelData) {
28+
isExporting = true;
29+
try {
30+
await DataExporter.exportPet(pet);
31+
} catch (error) {
32+
alert('Export failed: ' + (error as Error).message);
33+
} finally {
34+
isExporting = false;
35+
}
36+
}
37+
38+
async function handleImport(event: Event) {
39+
const target = event.target as HTMLInputElement;
40+
const file = target.files?.[0];
41+
if (!file) return;
42+
43+
isImporting = true;
44+
importMessage = '';
45+
46+
try {
47+
const result = await DataExporter.importFromFile(file);
48+
importMessage = result.message;
49+
importSuccess = result.success;
50+
51+
if (result.success) {
52+
// Refresh data after import
53+
setTimeout(() => {
54+
window.location.reload();
55+
}, 2000);
56+
}
57+
} catch (error) {
58+
importMessage = 'Import failed: ' + (error as Error).message;
59+
importSuccess = false;
60+
} finally {
61+
isImporting = false;
62+
if (fileInput) {
63+
fileInput.value = '';
64+
}
65+
}
66+
}
67+
</script>
68+
69+
<div class="data-manager space-y-6">
70+
<!-- Export Section -->
71+
<div class="export-section">
72+
<h3 class="text-lg font-semibold mb-4 flex items-center" style="color: var(--petalytics-text);">
73+
<Download size={20} class="mr-2" style="color: var(--petalytics-accent);" />
74+
Export Data
75+
</h3>
76+
77+
<div class="space-y-3">
78+
<!-- Export All -->
79+
<div class="export-item p-4 rounded-lg border" style="background: var(--petalytics-surface); border-color: var(--petalytics-border);">
80+
<div class="flex items-center justify-between">
81+
<div>
82+
<h4 class="font-medium" style="color: var(--petalytics-text);">Complete Backup</h4>
83+
<p class="text-sm" style="color: var(--petalytics-subtle);">
84+
Export all pets, settings, and journal entries
85+
</p>
86+
</div>
87+
<button
88+
on:click={exportAllData}
89+
disabled={isExporting || pets.length === 0}
90+
class="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white px-4 py-2 rounded-lg flex items-center space-x-2 transition-colors"
91+
>
92+
{#if isExporting}
93+
<div class="animate-spin w-4 h-4 border-2 border-current border-t-transparent rounded-full"></div>
94+
{:else}
95+
<Database size={16} />
96+
{/if}
97+
<span>Export All</span>
98+
</button>
99+
</div>
100+
</div>
101+
102+
<!-- Export Individual Pets -->
103+
{#if pets.length > 0}
104+
<div class="individual-exports">
105+
<h4 class="font-medium mb-2" style="color: var(--petalytics-text);">Export Individual Pets</h4>
106+
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
107+
{#each pets as pet}
108+
<div class="pet-export-item p-3 rounded-lg border" style="background: var(--petalytics-overlay); border-color: var(--petalytics-border);">
109+
<div class="flex items-center justify-between">
110+
<div class="flex items-center space-x-2">
111+
<img
112+
src={pet.profileImageUrl || '/images/default-pet.png'}
113+
alt={pet.name}
114+
class="w-8 h-8 rounded-full object-cover"
115+
on:error={(e) => {
116+
const target = e.target as HTMLImageElement;
117+
target.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iMTYiIGZpbGw9IiNGMzRGNEYiLz4KPHN2ZyB4PSI4IiB5PSI4IiB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIGZpbGw9IndoaXRlIj4KICA8cGF0aCBkPSJNNS4yNSA0QzQuNTU5NjQgNCA0IDQuNTU5NjQgNCA1LjI1VjEwLjc1QzQgMTEuNDQwNCA0LjU1OTY0IDEyIDUuMjUgMTJIMTAuNzVDMTEuNDQwNCAxMiAxMiAxMS40NDA0IDEyIDEwLjc1VjUuMjVDMTIgNC41NTk2NCAxMS40NDA0IDQgMTAuNzUgNEg1LjI1WiIvPgo8L3N2Zz4KPC9zdmc+';
118+
}}
119+
/>
120+
<div>
121+
<p class="font-medium text-sm" style="color: var(--petalytics-text);">{pet.name}</p>
122+
<p class="text-xs" style="color: var(--petalytics-subtle);">
123+
{pet.journalEntries?.length || 0} entries
124+
</p>
125+
</div>
126+
</div>
127+
<button
128+
on:click={() => exportSinglePet(pet)}
129+
disabled={isExporting}
130+
class="bg-gray-100 hover:bg-gray-200 disabled:bg-gray-50 disabled:cursor-not-allowed text-gray-700 text-xs px-2 py-1 rounded flex items-center space-x-1 transition-colors"
131+
>
132+
<FileText size={12} />
133+
<span>Export</span>
134+
</button>
135+
</div>
136+
</div>
137+
{/each}
138+
</div>
139+
</div>
140+
{/if}
141+
</div>
142+
</div>
143+
144+
<!-- Import Section -->
145+
<div class="import-section">
146+
<h3 class="text-lg font-semibold mb-4 flex items-center" style="color: var(--petalytics-text);">
147+
<Upload size={20} class="mr-2" style="color: var(--petalytics-accent);" />
148+
Import Data
149+
</h3>
150+
151+
<div class="import-area p-6 rounded-lg border-2 border-dashed text-center"
152+
style="border-color: var(--petalytics-border);">
153+
<Upload size={32} style="color: var(--petalytics-subtle);" class="mx-auto mb-3" />
154+
<p class="mb-3" style="color: var(--petalytics-text);">
155+
Select a JSONL backup file to import
156+
</p>
157+
<input
158+
bind:this={fileInput}
159+
type="file"
160+
accept=".jsonl"
161+
on:change={handleImport}
162+
class="hidden"
163+
/>
164+
<button
165+
on:click={() => fileInput?.click()}
166+
disabled={isImporting}
167+
class="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-white px-4 py-2 rounded-lg flex items-center space-x-2 mx-auto transition-colors"
168+
>
169+
{#if isImporting}
170+
<div class="animate-spin w-4 h-4 border-2 border-current border-t-transparent rounded-full"></div>
171+
<span>Importing...</span>
172+
{:else}
173+
<Upload size={16} />
174+
<span>Choose File</span>
175+
{/if}
176+
</button>
177+
178+
{#if importMessage}
179+
<div class="mt-4 p-3 rounded"
180+
class:bg-green-100={importSuccess}
181+
class:bg-red-100={!importSuccess}>
182+
<p class="text-sm"
183+
class:text-green-800={importSuccess}
184+
class:text-red-800={!importSuccess}>
185+
{importMessage}
186+
</p>
187+
</div>
188+
{/if}
189+
</div>
190+
191+
<!-- Data Format Info -->
192+
<div class="format-info mt-4 p-3 rounded" style="background: var(--petalytics-overlay);">
193+
<h4 class="font-medium text-sm mb-2" style="color: var(--petalytics-text);">Data Format</h4>
194+
<ul class="text-xs space-y-1" style="color: var(--petalytics-subtle);">
195+
<li>• JSONL files exported from Petalytics</li>
196+
<li>• Individual pet files or complete backups</li>
197+
<li>• All journal entries and AI analyses included</li>
198+
<li>• Import will merge with existing data</li>
199+
</ul>
200+
</div>
201+
</div>
202+
</div>

src/lib/stores/guardian.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ export const guardianHelpers = {
8080
localStorage.removeItem(STORAGE_KEY);
8181
}
8282
guardianStore.set(defaultGuardian);
83+
},
84+
85+
// Import guardian data
86+
importGuardian(guardianData: any) {
87+
guardianStore.update(current => {
88+
const updated = { ...current, ...guardianData };
89+
this.save(updated);
90+
return updated;
91+
});
8392
}
8493
};
8594

src/lib/stores/pets.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,47 @@ export const petHelpers = {
101101
}
102102
return pet;
103103
});
104+
this.save(updated);
105+
return updated;
106+
});
107+
},
108+
109+
// Get all pets
110+
getAll(callback?: (pets: PetPanelData[]) => void): (() => void) | undefined {
111+
if (callback) {
112+
return petStore.subscribe(callback);
113+
} else {
114+
let pets: PetPanelData[] = [];
115+
const unsubscribe = petStore.subscribe(value => { pets = value; });
116+
unsubscribe();
117+
return undefined;
118+
}
119+
},
120+
121+
// Get all pets synchronously
122+
getAllPets(): PetPanelData[] {
123+
let pets: PetPanelData[] = [];
124+
const unsubscribe = petStore.subscribe(value => { pets = value; });
125+
unsubscribe();
126+
return pets;
127+
},
128+
129+
// Import pet data
130+
importPet(petData: PetPanelData) {
131+
petStore.update(pets => {
132+
const existingIndex = pets.findIndex(p => p.id === petData.id);
133+
let updated: PetPanelData[];
134+
135+
if (existingIndex >= 0) {
136+
// Update existing pet
137+
updated = pets.map((pet, index) =>
138+
index === existingIndex ? petData : pet
139+
);
140+
} else {
141+
// Add new pet
142+
updated = [...pets, petData];
143+
}
144+
104145
this.save(updated);
105146
return updated;
106147
});

0 commit comments

Comments
 (0)