Files
card-forge/src/lib/components/wizard/StepSheetSearch.svelte
2025-07-18 09:11:17 +02:00

282 lines
8.1 KiB
Svelte

<script lang="ts">
import { availableSheets, selectedSheet, currentStep } from '$lib/stores';
import { searchSheets } from '$lib/google';
import { onMount } from 'svelte';
let searchQuery = $state('');
let isLoading = $state(false);
let error = $state('');
let searchResults = $state<any[]>([]);
let hasSearched = $state(false);
let recentSheets = $state<any[]>([]);
const RECENT_SHEETS_KEY = 'esn-recent-sheets';
onMount(() => {
loadRecentSheets();
});
async function handleSearch() {
if (!searchQuery.trim()) return;
isLoading = true;
error = '';
try {
searchResults = await searchSheets(searchQuery);
availableSheets.set(
searchResults.map((sheet) => ({
spreadsheetId: sheet.spreadsheetId || sheet.id,
name: sheet.name,
url: sheet.webViewLink
}))
);
hasSearched = true;
} catch (err) {
console.error('Error searching sheets:', err);
error = 'Failed to search sheets. Please check your connection and try again.';
searchResults = [];
availableSheets.set([]);
} finally {
isLoading = false;
}
}
function loadRecentSheets() {
try {
const saved = localStorage.getItem(RECENT_SHEETS_KEY);
if (saved) {
recentSheets = JSON.parse(saved);
}
} catch (err) {
console.error('Error loading recent sheets:', err);
// If there's an error, clear the stored value
localStorage.removeItem(RECENT_SHEETS_KEY);
recentSheets = [];
}
}
function handleSelectSheet(sheet) {
const sheetData = {
spreadsheetId: sheet.spreadsheetId || sheet.id,
name: sheet.name,
url: sheet.webViewLink || sheet.url
};
selectedSheet.set(sheetData);
}
let canProceed = $derived($selectedSheet !== null);
function handleContinue() {
if (!canProceed) return;
currentStep.set(3); // Move to the column mapping step
}
</script>
<div class="p-6">
<div class="mb-6">
<h2 class="mb-2 text-xl font-semibold text-gray-900">Select Google Sheet</h2>
<p class="mb-4 text-sm text-gray-700">
Search for and select the Google Sheet containing your member data.
</p>
</div>
<!-- Search input -->
<div class="mb-6">
<label for="sheet-search" class="mb-2 block text-sm font-medium text-gray-700">
Search sheets
</label>
<div class="flex">
<input
id="sheet-search"
type="text"
bind:value={searchQuery}
placeholder="Type sheet name..."
class="flex-grow rounded-l-lg border border-gray-300 px-4 py-2 focus:border-transparent focus:ring-2 focus:ring-blue-600"
onkeydown={(e) => {
if (e.key === 'Enter') handleSearch();
}}
/>
<button
onclick={handleSearch}
disabled={isLoading || !searchQuery.trim()}
class="rounded-r-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-400"
>
{#if isLoading}
<div
class="h-5 w-5 animate-spin rounded-full border-2 border-white border-t-transparent"
></div>
{:else}
Search
{/if}
</button>
</div>
</div>
{#if error}
<div class="mb-6 rounded-lg border border-red-300 bg-red-50 p-4">
<p class="text-sm text-red-800">{error}</p>
</div>
{/if}
<!-- Results -->
{#if hasSearched}
<div class="mb-6">
<h3 class="mb-3 text-sm font-medium text-gray-700">
{searchResults.length
? `Found ${searchResults.length} matching sheets`
: 'No matching sheets found'}
</h3>
{#if searchResults.length}
<div class="space-y-3">
{#each searchResults as sheet}
<div
class="cursor-pointer rounded-lg border p-4 transition-colors hover:bg-gray-50 {$selectedSheet?.spreadsheetId ===
(sheet.spreadsheetId || sheet.id)
? 'border-blue-500 bg-blue-50'
: 'border-gray-200'}"
onclick={() => handleSelectSheet(sheet)}
tabindex="0"
role="button"
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') handleSelectSheet(sheet);
}}
>
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900">{sheet.name}</p>
<p class="mt-1 text-xs text-gray-500">ID: {sheet.id}</p>
</div>
<div class="flex items-center">
{#if sheet.iconLink}
<img src={sheet.iconLink} alt="Sheet icon" class="mr-2 h-5 w-5" />
{/if}
{#if $selectedSheet?.spreadsheetId === (sheet.spreadsheetId || sheet.id)}
<svg class="h-5 w-5 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
/>
</svg>
{/if}
</div>
</div>
</div>
{/each}
</div>
{:else}
<div class="rounded-lg border border-gray-200 bg-gray-50 py-8 text-center">
<svg
class="mx-auto h-12 w-12 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<p class="mt-2 text-sm text-gray-500">Try a different search term</p>
</div>
{/if}
</div>
{:else}
<!-- If we have recent sheets and haven't searched yet, show them -->
{#if recentSheets.length > 0 && !hasSearched}
<div class="mb-6">
<h3 class="mb-3 text-sm font-medium text-gray-700">Recent sheets</h3>
<div class="space-y-3">
{#each recentSheets as sheet}
<div
class="cursor-pointer rounded-lg border p-4 transition-colors hover:bg-gray-50 {$selectedSheet?.spreadsheetId ===
(sheet.spreadsheetId || sheet.id)
? 'border-blue-500 bg-blue-50'
: 'border-gray-200'}"
onclick={() => handleSelectSheet(sheet)}
tabindex="0"
role="button"
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') handleSelectSheet(sheet);
}}
>
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900">{sheet.name}</p>
<p class="mt-1 text-xs text-gray-500">Recently used</p>
</div>
<div class="flex items-center">
{#if sheet.iconLink}
<img src={sheet.iconLink} alt="Sheet icon" class="mr-2 h-5 w-5" />
{/if}
{#if $selectedSheet?.spreadsheetId === (sheet.spreadsheetId || sheet.id)}
<svg class="h-5 w-5 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
/>
</svg>
{/if}
</div>
</div>
</div>
{/each}
</div>
<div class="mt-4 border-t border-gray-200 pt-4">
<p class="text-xs text-gray-500">Or search for a different sheet above</p>
</div>
</div>
{:else}
<div class="mb-6 rounded-lg border border-gray-200 bg-gray-50 py-12 text-center">
<svg
class="mx-auto h-16 w-16 text-gray-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<h3 class="mt-2 text-lg font-medium text-gray-900">Search for your sheet</h3>
<p class="mt-1 text-sm text-gray-500">Enter a name or keyword to find your Google Sheets</p>
</div>
{/if}
{/if}
<!-- Navigation -->
<div class="flex justify-between">
<button
onclick={() => currentStep.set(1)}
class="rounded-lg bg-gray-200 px-4 py-2 font-medium text-gray-700 hover:bg-gray-300"
>
← Back to Auth
</button>
<button
onclick={handleContinue}
disabled={!canProceed}
class="rounded-lg bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-400"
>
{canProceed ? 'Continue →' : 'Select a sheet to continue'}
</button>
</div>
</div>