Improve the the cropping process, UI and UX

This commit is contained in:
Roman Krček
2025-07-18 09:11:17 +02:00
parent c77c96c1c7
commit 9bbd02dd67
8 changed files with 2146 additions and 1881 deletions

View File

@@ -1,251 +1,281 @@
<script lang="ts">
import { availableSheets, selectedSheet, currentStep } from '$lib/stores';
import { searchSheets } from '$lib/google';
import { onMount } from 'svelte';
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();
});
let searchQuery = $state('');
let isLoading = $state(false);
let error = $state('');
let searchResults = $state<any[]>([]);
let hasSearched = $state(false);
let recentSheets = $state<any[]>([]);
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;
}
}
const RECENT_SHEETS_KEY = 'esn-recent-sheets';
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 = [];
}
}
onMount(() => {
loadRecentSheets();
});
function handleSelectSheet(sheet) {
const sheetData = {
spreadsheetId: sheet.spreadsheetId || sheet.id,
name: sheet.name,
url: sheet.webViewLink || sheet.url
};
selectedSheet.set(sheetData);
}
async function handleSearch() {
if (!searchQuery.trim()) return;
let canProceed = $derived($selectedSheet !== null);
function handleContinue() {
if (!canProceed) return;
currentStep.set(3); // Move to the column mapping step
}
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="max-w-2xl mx-auto">
<div class="mb-6">
<h2 class="text-xl font-semibold text-gray-900 mb-2">
Select Google Sheet
</h2>
<p class="text-sm text-gray-700 mb-4">
Search for and select the Google Sheet containing your member data.
</p>
</div>
<div class="mb-6">
<h2 class="mb-2 text-xl font-semibold text-gray-900">Select Google Sheet</h2>
<!-- Search input -->
<div class="mb-6">
<label for="sheet-search" class="block text-sm font-medium text-gray-700 mb-2">
Search sheets
</label>
<div class="flex">
<input
id="sheet-search"
type="text"
bind:value={searchQuery}
placeholder="Type sheet name..."
class="flex-grow px-4 py-2 border border-gray-300 rounded-l-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent"
onkeydown={e => { if (e.key === 'Enter') handleSearch(); }}
/>
<button
onclick={handleSearch}
disabled={isLoading || !searchQuery.trim()}
class="px-4 py-2 bg-blue-600 text-white rounded-r-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{#if isLoading}
<div class="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
{:else}
Search
{/if}
</button>
</div>
</div>
<p class="mb-4 text-sm text-gray-700">
Search for and select the Google Sheet containing your member data.
</p>
</div>
{#if error}
<div class="bg-red-50 border border-red-300 rounded-lg p-4 mb-6">
<p class="text-sm text-red-800">{error}</p>
</div>
{/if}
<!-- Search input -->
<div class="mb-6">
<label for="sheet-search" class="mb-2 block text-sm font-medium text-gray-700">
Search sheets
</label>
<!-- Results -->
{#if hasSearched}
<div class="mb-6">
<h3 class="text-sm font-medium text-gray-700 mb-3">
{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="border rounded-lg p-4 cursor-pointer 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="text-xs text-gray-500 mt-1">ID: {sheet.id}</p>
</div>
<div class="flex items-center">
{#if sheet.iconLink}
<img src={sheet.iconLink} alt="Sheet icon" class="w-5 h-5 mr-2" />
{/if}
{#if $selectedSheet?.spreadsheetId === (sheet.spreadsheetId || sheet.id)}
<svg class="w-5 h-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="text-center py-8 bg-gray-50 rounded-lg border border-gray-200">
<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="text-sm font-medium text-gray-700 mb-3">
Recent sheets
</h3>
<div class="space-y-3">
{#each recentSheets as sheet}
<div
class="border rounded-lg p-4 cursor-pointer 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="text-xs text-gray-500 mt-1">Recently used</p>
</div>
<div class="flex items-center">
{#if sheet.iconLink}
<img src={sheet.iconLink} alt="Sheet icon" class="w-5 h-5 mr-2" />
{/if}
{#if $selectedSheet?.spreadsheetId === (sheet.spreadsheetId || sheet.id)}
<svg class="w-5 h-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="border-t border-gray-200 mt-4 pt-4">
<p class="text-xs text-gray-500">
Or search for a different sheet above
</p>
</div>
</div>
{:else}
<div class="text-center py-12 bg-gray-50 rounded-lg border border-gray-200 mb-6">
<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}
<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();
}}
/>
<!-- Navigation -->
<div class="flex justify-between">
<button
onclick={() => currentStep.set(1)}
class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg font-medium hover:bg-gray-300"
>
← Back to Auth
</button>
<button
onclick={handleContinue}
disabled={!canProceed}
class="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{canProceed
? 'Continue →'
: 'Select a sheet to continue'}
</button>
</div>
</div>
<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>