Google auth done
This commit is contained in:
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Your Google Cloud OAuth 2.0 Client ID
|
||||||
|
VITE_GOOGLE_CLIENT_ID="YOUR_GOOGLE_CLIENT_ID_HERE"
|
||||||
7
.github/copilot-instructions.md
vendored
Normal file
7
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Base directions
|
||||||
|
- You are a helpful AI assistant that helps developers write code.
|
||||||
|
- This code is written in Svelte 5
|
||||||
|
- It's important to only use modern Svelte 5 syntax, runes, and features.
|
||||||
|
- Use styling from ".github/styling.md" for any UI components.
|
||||||
|
- Refer to the ".github/core-instructions.md" for the overall structure of the application.
|
||||||
|
- Generate ".github/done.md" file to see what is done and what is not. Check it when you start and finish a task.
|
||||||
208
.github/core-instructions.md
vendored
Normal file
208
.github/core-instructions.md
vendored
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
# Copilot Instruction Set – **Google‑Sheet → Photo‑PDF** Wizard (SvelteKit)
|
||||||
|
|
||||||
|
> **Goal**
|
||||||
|
> A privacy‑first SvelteKit SPA that guides a user through importing a Google Sheet, mapping its columns, validating/culling rows, reviewing & cropping photos, and finally exporting two A4‑PDFs (text sheet + photo sheet). No personal data ever leaves the browser after the tab closes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Tech Stack & Design Constraints
|
||||||
|
|
||||||
|
| Area | Decision |
|
||||||
|
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Front‑end | **SvelteKit** (latest) – SPA mode (`prerender = false`) |
|
||||||
|
| State | **Svelte Stores** (writable & derived). No persistence beyond sessionStorage/IndexedDB. |
|
||||||
|
| Styling | **Tailwind CSS** + `@tailwind/typography`, base theme. Page background `bg-gray-50`. Use DaisyUI or shadcn‑ui components if needed. |
|
||||||
|
| Auth & API | Google Identity Services (popup flow). Scopes: `spreadsheets`, `drive` (read/write). |
|
||||||
|
| Data storage | • Structured data → **IndexedDB** via `idb` helper. |
|
||||||
|
| • Image blobs → **Cache Storage** (`caches.open'photos-v1') a | |
|
||||||
|
| Face detection | **Mediapipe BlazeFace via TF.js (lite)** in a Web Worker. Confidence ≥ 0.8. |
|
||||||
|
| Virtual list | `svelte-virtual` (react‑window equivalent). |
|
||||||
|
| PDF | **pdf-lib** (via ESM import). Embed custom font **Lato** (ttf provided in `/static/fonts`). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. User Stories & Acceptance Criteria
|
||||||
|
|
||||||
|
1. **US‑01 Splash** – As a visitor I see a logo + “Start” button (no data loaded). Clicking starts the wizard.
|
||||||
|
2. **US‑02 Sign‑In** – I authenticate with Google in a popup and grant the requested scopes. On success, token is cached in memory only.
|
||||||
|
3. **US‑03 Sheet Search** – I type part of a Sheet name; a fuzzy list of my sheets appears (max 20). I select one.
|
||||||
|
4. **US‑04 Column Mapping** – Columns A–F preview (first 3 data rows). Under each header is a dropdown: Name / Surname / Nationality / Birthday / Picture URL / Ignore. All five fields must be mapped exactly once. Previous mapping is remembered for future sessions in `localStorage` (key: `columnHints`).
|
||||||
|
5. **US‑05 Row Validate & Select** – I see rows in a virtualized table with validation states. Invalid rows (missing data, age < 18 or > 40) are pre‑flagged. I can tick checkboxes to include/exclude. No in‑app editing.
|
||||||
|
6. **US‑06 Photo Gallery** – Lazy virtual grid of thumbnails. Auto face‑crop overlay (dashed rectangle). Pictures with 0 or >1 faces are floated to top & flagged “Need manual crop”. I can drag/resize a fixed‑ratio rectangle to override. Cropped area is stored as `{x,y,w,h}` in store.
|
||||||
|
7. **US‑07 Progress** – While downloading or processing images / faces, I see a determinate progress bar (0‑100%). Each stage (download, face detect, PDF gen).
|
||||||
|
8. **US‑08 Generate PDFs** – I click “Generate”. Two PDFs download:
|
||||||
|
|
||||||
|
* `people_data.pdf` – text‑only; grid fills as many boxes per A4 as fit (auto calculate cols/rows). Each box uses provided coordinates (see §4).
|
||||||
|
* `people_photos.pdf` – same grid but with cropped headshot @ modest DPI, name beneath.
|
||||||
|
9. **US‑09 Cleanup** – On window/tab unload, all IndexedDB records and CacheStorage entries are cleared.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Component Tree (Svelte)
|
||||||
|
|
||||||
|
```
|
||||||
|
+Layout.svelte (Tailwind container)
|
||||||
|
├─ Splash.svelte
|
||||||
|
├─ Wizard.svelte (stateful)
|
||||||
|
├─ StepAuth.svelte
|
||||||
|
├─ StepSheetSearch.svelte
|
||||||
|
├─ StepColumnMap.svelte
|
||||||
|
├─ StepRowFilter.svelte
|
||||||
|
├─ StepGallery.svelte
|
||||||
|
└─ StepGenerate.svelte
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shared Stores (`src/lib/stores.ts`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const session = writable<{
|
||||||
|
token?: string;
|
||||||
|
user?: { name: string; email: string };
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
export const sheetData = writable<RowData[]>(); // after mapping
|
||||||
|
export const pictures = writable<Record<RowID, PictureBlobInfo>>();
|
||||||
|
export const cropRects = writable<Record<RowID, Crop>>();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Data Flow
|
||||||
|
|
||||||
|
1. **Auth** ⇒ `session.token` set.
|
||||||
|
2. **Sheet fetch** (Google Sheets API) ⇒ raw 2D array.
|
||||||
|
3. **Mapping** ⇒ transform to `RowData` objects.
|
||||||
|
4. **Validation** ⇒ add `valid` flag; excluded rows removed from downstream.
|
||||||
|
5. **Download IMG** (Drive/HTTP) ⇒ cache → `pictures` store.
|
||||||
|
6. **Face Detect** (Worker) ⇒ proposed crop rectangle.
|
||||||
|
7. **Manual Crop** ⇒ update `cropRects`.
|
||||||
|
8. **Generate PDFs** ⇒ read `RowData`, `pictures`, `cropRects`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. PDF Layout Spec (mm → points helper)
|
||||||
|
|
||||||
|
* **Font**: Lato Regular, 10 pt. Embed once per doc via `pdfDoc.embedFont()`.
|
||||||
|
* **Grid**: auto compute `colWidth = 70 mm`, `rowHeight = 35 mm` for text; `imageBox = 30 mm × 30 mm` centred.
|
||||||
|
* **Coordinates (text sheet)** – within each cell (0,0 is cell top‑left):
|
||||||
|
|
||||||
|
* Full Name – x 5 mm, y 5 mm
|
||||||
|
* Nationality – x 5 mm, y 12 mm
|
||||||
|
* Birthday – x 5 mm, y 19 mm
|
||||||
|
* **Coordinates (photo sheet)**
|
||||||
|
|
||||||
|
* Image top‑left x 5 mm, y 5 mm (square)
|
||||||
|
* Name text centre below at y (5 + imageBox + 3 mm)
|
||||||
|
* Fit rows top→bottom, left→right. When page full, add new page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Key Helpers
|
||||||
|
|
||||||
|
### `googleClient.ts`
|
||||||
|
|
||||||
|
* `initAuth(): Promise<Token>`
|
||||||
|
* `listSheets(query): Promise<SheetMeta[]>` (fuzzy)
|
||||||
|
* `getSheetValues(id, range): Promise<string[][]>`
|
||||||
|
|
||||||
|
### `faceWorker.ts` (Web Worker)
|
||||||
|
|
||||||
|
* Loads BlazeFace lite model once.
|
||||||
|
* `detectFaces(arrayBuffer) → {faces: Face[], blobURL}`
|
||||||
|
|
||||||
|
### `pdfGenerator.ts`
|
||||||
|
|
||||||
|
* `generateTextPDF(rows: RowData[]): Uint8Array`
|
||||||
|
* `generatePhotoPDF(rows: RowData[], crops: CropMap): Uint8Array`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Progress Bus (simple derived store)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const progress = writable<{ stage: string; done: number; total: number }>();
|
||||||
|
```
|
||||||
|
|
||||||
|
Set from download loop and face detect worker.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Error Handling
|
||||||
|
|
||||||
|
* Auth popup closed ⇒ toast “Sign‑in cancelled”.
|
||||||
|
* Sheet fetch 404 ⇒ return to StepSheetSearch with error banner.
|
||||||
|
* Image download error ⇒ mark row `pictureError = true`, bubble to top.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Folder Structure Skeleton
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
lib/
|
||||||
|
stores.ts
|
||||||
|
googleClient.ts
|
||||||
|
faceWorker.ts
|
||||||
|
pdfGenerator.ts
|
||||||
|
routes/
|
||||||
|
+layout.svelte
|
||||||
|
index.svelte (Splash)
|
||||||
|
wizard/
|
||||||
|
+page.svelte (mount Wizard.svelte)
|
||||||
|
components/ (each Step*.svelte)
|
||||||
|
workers/
|
||||||
|
faceWorker.js
|
||||||
|
static/
|
||||||
|
fonts/Lato-Regular.ttf
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Copilot Prompt Comments (templates)
|
||||||
|
|
||||||
|
### Example: `googleClient.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/**
|
||||||
|
* Google API wrapper – Browser‑only, no refresh tokens.
|
||||||
|
* Prereqs: `<script src="https://accounts.google.com/gsi/client" async defer></script>` in root.
|
||||||
|
*
|
||||||
|
* Helper responsibilities:
|
||||||
|
* 1. Launch popup OAuth (scopes: spreadsheets, drive).
|
||||||
|
* 2. List up to 20 spreadsheets matching fuzzy query.
|
||||||
|
* 3. Download a specified range (A:Z) as 2‑D array.
|
||||||
|
* 4. Use in‑memory token, auto‑refresh not required.
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: `StepGallery.svelte`
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Renders a virtual grid of downloaded photos.
|
||||||
|
* Highlights items needing manual crop (no face/multiple faces).
|
||||||
|
* When user drags the crop rectangle:
|
||||||
|
* – Persist to cropRects store.
|
||||||
|
* – Overlay rectangle turns green (valid).
|
||||||
|
*/
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy‑paste these comment headers into the top of each file to prime GitHub Copilot with context.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Outstanding TODOs (owner = you)
|
||||||
|
|
||||||
|
* [ ] Provide exact mm → point conversion util if different spec required.
|
||||||
|
* [ ] Supply precise image/text box sizes & page margins.
|
||||||
|
* [ ] Drop in `Lato-Regular.ttf` into `/static/fonts`.
|
||||||
|
* [ ] Decide on logo / brand in Splash.
|
||||||
|
* [ ] Accessibility pass once UI stabilises.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### End of base instruction set
|
||||||
|
|
||||||
|
Feel free to ask for tweaks or deeper code snippets!
|
||||||
40
.github/done.md
vendored
Normal file
40
.github/done.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# ESN Card Generator - Project Status
|
||||||
|
|
||||||
|
## ✅ Completed
|
||||||
|
- Basic SvelteKit project structure
|
||||||
|
- Package.json with SvelteKit and Tailwind setup
|
||||||
|
|
||||||
|
## 🔄 In Progress
|
||||||
|
- Scaffolding application structure
|
||||||
|
|
||||||
|
## ❌ To Do
|
||||||
|
- [ ] Install required dependencies (pdf-lib, idb, mediapipe, etc.)
|
||||||
|
- [ ] Create Svelte stores (stores.ts)
|
||||||
|
- [ ] Create component structure:
|
||||||
|
- [ ] Splash.svelte
|
||||||
|
- [ ] Wizard.svelte
|
||||||
|
- [ ] StepAuth.svelte
|
||||||
|
- [ ] StepSheetSearch.svelte
|
||||||
|
- [ ] StepColumnMap.svelte
|
||||||
|
- [ ] StepRowFilter.svelte
|
||||||
|
- [ ] StepGallery.svelte
|
||||||
|
- [ ] StepGenerate.svelte
|
||||||
|
- [ ] Setup Google Identity Services integration
|
||||||
|
- [ ] Implement Google Sheets API integration
|
||||||
|
- [ ] Implement Google Drive API integration
|
||||||
|
- [ ] Setup IndexedDB storage utilities
|
||||||
|
- [ ] Setup Cache Storage for images
|
||||||
|
- [ ] Implement face detection with MediaPipe
|
||||||
|
- [ ] Create PDF generation with pdf-lib
|
||||||
|
- [ ] Add Lato font assets
|
||||||
|
- [ ] Implement virtual scrolling for large datasets
|
||||||
|
- [ ] Add data validation logic
|
||||||
|
- [ ] Create image cropping interface
|
||||||
|
- [ ] Add progress indicators
|
||||||
|
- [ ] Implement cleanup on tab close
|
||||||
|
- [ ] Testing and polish
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Using Svelte 5 with modern runes syntax
|
||||||
|
- Following Tailwind CSS styling guidelines
|
||||||
|
- Privacy-first approach - no data persistence beyond session
|
||||||
363
.github/styling.md
vendored
Normal file
363
.github/styling.md
vendored
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
# Oveall Styling Guide
|
||||||
|
|
||||||
|
This document outlines the design system and styling conventions used in the application. Use this as a reference when creating new applications with similar visual design.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
- [Color Palette](#color-palette)
|
||||||
|
- [Typography](#typography)
|
||||||
|
- [Layout Patterns](#layout-patterns)
|
||||||
|
- [Component Patterns](#component-patterns)
|
||||||
|
- [Form Elements](#form-elements)
|
||||||
|
- [Buttons](#buttons)
|
||||||
|
- [Cards and Containers](#cards-and-containers)
|
||||||
|
- [Navigation](#navigation)
|
||||||
|
- [Tables](#tables)
|
||||||
|
- [Loading States](#loading-states)
|
||||||
|
- [Toast Notifications](#toast-notifications)
|
||||||
|
- [Responsive Design](#responsive-design)
|
||||||
|
|
||||||
|
## Color Palette
|
||||||
|
|
||||||
|
### Primary Colors
|
||||||
|
- **Blue**: Primary action color
|
||||||
|
- `bg-blue-600` / `text-blue-600` - Primary buttons, links
|
||||||
|
- `bg-blue-700` / `text-blue-700` - Hover states
|
||||||
|
- `bg-blue-50` / `text-blue-800` - Info notifications
|
||||||
|
- `border-blue-600` / `focus:ring-blue-600` - Focus states
|
||||||
|
|
||||||
|
### Gray Scale
|
||||||
|
- **Text Colors**:
|
||||||
|
- `text-gray-900` - Primary text (headings, important content)
|
||||||
|
- `text-gray-700` - Secondary text (labels, descriptions)
|
||||||
|
- `text-gray-500` - Tertiary text (metadata, placeholders)
|
||||||
|
|
||||||
|
- **Background Colors**:
|
||||||
|
- `bg-white` - Main content backgrounds
|
||||||
|
- `bg-gray-50` - Page backgrounds, subtle sections
|
||||||
|
- `bg-gray-100` - Disabled form fields
|
||||||
|
- `bg-gray-200` - Loading placeholders
|
||||||
|
|
||||||
|
- **Border Colors**:
|
||||||
|
- `border-gray-300` - Standard borders (cards, inputs)
|
||||||
|
- `border-gray-200` - Subtle borders (table rows)
|
||||||
|
|
||||||
|
### Status Colors
|
||||||
|
- **Success**: `bg-green-50 text-green-800 border-green-300`
|
||||||
|
- **Warning**: `bg-yellow-50 text-yellow-800 border-yellow-300`
|
||||||
|
- **Error**: `bg-red-50 text-red-800 border-red-300`
|
||||||
|
- **Info**: `bg-blue-50 text-blue-800 border-blue-300`
|
||||||
|
|
||||||
|
### Accent Colors
|
||||||
|
- **Red**: `text-red-600` / `hover:text-red-700` - Danger actions (sign out)
|
||||||
|
- **Green**: `text-green-600` - Success indicators
|
||||||
|
|
||||||
|
## Typography
|
||||||
|
|
||||||
|
### Headings
|
||||||
|
```html
|
||||||
|
<!-- Page titles -->
|
||||||
|
<h1 class="mb-6 text-2xl font-bold text-center text-gray-800">Page Title</h1>
|
||||||
|
|
||||||
|
<!-- Section headings -->
|
||||||
|
<h2 class="mb-4 text-xl font-semibold text-gray-900">Section Title</h2>
|
||||||
|
<h2 class="mb-2 text-lg font-semibold text-gray-900">Subsection Title</h2>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Body Text
|
||||||
|
```html
|
||||||
|
<!-- Primary text -->
|
||||||
|
<p class="text-sm text-gray-900">Important content</p>
|
||||||
|
|
||||||
|
<!-- Secondary text -->
|
||||||
|
<p class="text-sm text-gray-700">Regular content</p>
|
||||||
|
<p class="text-sm leading-relaxed text-gray-700">Longer content blocks</p>
|
||||||
|
|
||||||
|
<!-- Metadata/labels -->
|
||||||
|
<span class="text-sm font-medium text-gray-500">Label</span>
|
||||||
|
<span class="text-sm font-medium text-gray-700">Form Label</span>
|
||||||
|
|
||||||
|
<!-- Small text -->
|
||||||
|
<p class="text-xs text-gray-500">Helper text</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Text Utilities
|
||||||
|
- **Font Weight**: `font-bold`, `font-semibold`, `font-medium`
|
||||||
|
- **Text Alignment**: `text-center`, `text-left`
|
||||||
|
- **Line Height**: `leading-relaxed` for longer text blocks
|
||||||
|
|
||||||
|
## Layout Patterns
|
||||||
|
|
||||||
|
### Container Pattern
|
||||||
|
```html
|
||||||
|
<div class="container mx-auto max-w-2xl bg-white p-4">
|
||||||
|
<!-- Content -->
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Grid Layouts
|
||||||
|
```html
|
||||||
|
<!-- Dashboard grid -->
|
||||||
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||||
|
<div class="lg:col-span-1"><!-- Sidebar --></div>
|
||||||
|
<div class="lg:col-span-2"><!-- Main content --></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Two-column responsive -->
|
||||||
|
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<!-- Items -->
|
||||||
|
</dl>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spacing
|
||||||
|
- **Standard spacing**: `space-y-6`, `gap-6` - Between major sections
|
||||||
|
- **Component spacing**: `mb-4`, `mt-6`, `p-6` - Around components
|
||||||
|
- **Small spacing**: `gap-3`, `mb-2`, `mt-2` - Between related elements
|
||||||
|
- **Container padding**: `p-4`, `p-6` - Internal container spacing
|
||||||
|
|
||||||
|
## Component Patterns
|
||||||
|
|
||||||
|
### Card Structure
|
||||||
|
```html
|
||||||
|
<div class="rounded-lg border border-gray-300 bg-white p-6">
|
||||||
|
<div class="mb-4 flex justify-between items-center">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900">Title</h2>
|
||||||
|
<!-- Actions -->
|
||||||
|
</div>
|
||||||
|
<!-- Content -->
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Avatar/Profile Picture
|
||||||
|
```html
|
||||||
|
<div class="flex h-24 w-24 items-center justify-center rounded-full bg-gray-200 text-4xl font-bold text-gray-600">
|
||||||
|
{initials}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Form Elements
|
||||||
|
|
||||||
|
### Input Fields
|
||||||
|
```html
|
||||||
|
<!-- Standard input -->
|
||||||
|
<input
|
||||||
|
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-600 focus:ring-blue-600 focus:outline-none"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Disabled input -->
|
||||||
|
<input
|
||||||
|
class="w-full rounded-md border border-gray-300 px-3 py-2 disabled:cursor-default disabled:bg-gray-100"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Textarea
|
||||||
|
```html
|
||||||
|
<textarea
|
||||||
|
class="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-600 focus:ring-blue-600 focus:outline-none"
|
||||||
|
rows="6"
|
||||||
|
></textarea>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Select Dropdown
|
||||||
|
```html
|
||||||
|
<select class="w-full rounded-md border border-gray-300 p-2 focus:ring-2 focus:ring-blue-600 focus:outline-none">
|
||||||
|
<option>Option</option>
|
||||||
|
</select>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Form Labels
|
||||||
|
```html
|
||||||
|
<label class="block mb-1 text-sm font-medium text-gray-700">Label Text</label>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Buttons
|
||||||
|
|
||||||
|
### Primary Buttons
|
||||||
|
```html
|
||||||
|
<button class="rounded-md bg-blue-600 px-4 py-2 text-white font-medium transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50">
|
||||||
|
Primary Action
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Secondary/Outline Buttons
|
||||||
|
```html
|
||||||
|
<button class="rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-700 font-medium transition hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50">
|
||||||
|
Secondary Action
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Danger/Red Buttons
|
||||||
|
```html
|
||||||
|
<button class="rounded-md bg-red-600 px-4 py-2 text-white font-medium transition hover:bg-red-700">
|
||||||
|
Danger Action
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Button States
|
||||||
|
- **Loading**: Replace text with "Loading..." or "Saving..."
|
||||||
|
- **Disabled**: `disabled:cursor-not-allowed disabled:opacity-50`
|
||||||
|
|
||||||
|
## Cards and Containers
|
||||||
|
|
||||||
|
### Standard Card
|
||||||
|
```html
|
||||||
|
<div class="mb-6 rounded-md border border-gray-300 bg-white p-6">
|
||||||
|
<!-- Content -->
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Card with Header Actions
|
||||||
|
```html
|
||||||
|
<div class="rounded-md border border-gray-300 bg-white p-6">
|
||||||
|
<div class="mb-4 flex justify-between items-center">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900">Title</h2>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<!-- Action buttons -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Content -->
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Navigation
|
||||||
|
|
||||||
|
### Top Navigation
|
||||||
|
```html
|
||||||
|
<nav class="border-b border-gray-300 bg-gray-50 p-4 text-gray-900">
|
||||||
|
<div class="container mx-auto max-w-2xl">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<a href="/" class="text-lg font-bold">App Name</a>
|
||||||
|
<ul class="flex space-x-4">
|
||||||
|
<li><a href="/page" class="hover:text-blue-600 transition">Page</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tables
|
||||||
|
|
||||||
|
### Standard Table
|
||||||
|
```html
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left text-sm font-medium text-gray-700">Header</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-b border-gray-200 hover:bg-gray-50">
|
||||||
|
<td class="px-4 py-3 text-sm text-gray-900">Data</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Definition List (Key-Value Pairs)
|
||||||
|
```html
|
||||||
|
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<div class="sm:col-span-1">
|
||||||
|
<dt class="text-sm font-medium text-gray-500">Key</dt>
|
||||||
|
<dd class="mt-1 text-sm font-semibold text-gray-900">Value</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Loading States
|
||||||
|
|
||||||
|
### Skeleton Loading
|
||||||
|
```html
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="h-4 animate-pulse rounded-md bg-gray-200"></div>
|
||||||
|
<div class="h-10 w-full animate-pulse rounded-md bg-gray-200"></div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Loading Spinner
|
||||||
|
```html
|
||||||
|
<div class="flex h-10 items-center justify-center">
|
||||||
|
<div class="h-5 w-5 animate-spin rounded-full border-2 border-gray-300 border-t-blue-600"></div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Toast Notifications
|
||||||
|
|
||||||
|
### Toast Container Structure
|
||||||
|
```html
|
||||||
|
<div class="rounded-md border p-4 shadow-lg w-full {colorClasses}">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<!-- Icon -->
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-sm font-medium">{message}</p>
|
||||||
|
</div>
|
||||||
|
<button class="flex-shrink-0">
|
||||||
|
<!-- Close button -->
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Toast Color Variants
|
||||||
|
- **Success**: `border-green-300 bg-green-50 text-green-800`
|
||||||
|
- **Warning**: `border-yellow-300 bg-yellow-50 text-yellow-800`
|
||||||
|
- **Info**: `border-blue-300 bg-blue-50 text-blue-800`
|
||||||
|
- **Error**: `border-red-300 bg-red-50 text-red-800`
|
||||||
|
|
||||||
|
## Responsive Design
|
||||||
|
|
||||||
|
### Breakpoints
|
||||||
|
- **Mobile First**: Default styles for mobile
|
||||||
|
- **sm**: `sm:` prefix for small screens and up
|
||||||
|
- **lg**: `lg:` prefix for large screens and up
|
||||||
|
|
||||||
|
### Common Responsive Patterns
|
||||||
|
```html
|
||||||
|
<!-- Responsive grid -->
|
||||||
|
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||||
|
|
||||||
|
<!-- Responsive padding -->
|
||||||
|
<div class="p-4 sm:p-6">
|
||||||
|
|
||||||
|
<!-- Responsive columns -->
|
||||||
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Utility Classes
|
||||||
|
|
||||||
|
### Flexbox
|
||||||
|
- `flex items-center justify-between` - Header with title and actions
|
||||||
|
- `flex items-start gap-3` - Toast notification layout
|
||||||
|
- `flex flex-col` - Vertical stacking
|
||||||
|
- `flex-grow` - Fill available space
|
||||||
|
|
||||||
|
### Positioning
|
||||||
|
- `relative` / `absolute` - Positioning contexts
|
||||||
|
- `fixed bottom-6 left-1/2 -translate-x-1/2` - Centered fixed positioning
|
||||||
|
|
||||||
|
### Visibility
|
||||||
|
- `hidden` / `block` - Show/hide elements
|
||||||
|
- `overflow-hidden` - Clip content
|
||||||
|
- `overflow-x-auto` - Horizontal scroll for tables
|
||||||
|
|
||||||
|
### Borders and Shadows
|
||||||
|
- `rounded-md` - Standard border radius for all components
|
||||||
|
- `rounded-full` - Circular elements (avatars)
|
||||||
|
- `shadow-lg` - Toast notifications and elevated elements
|
||||||
|
- `shadow-none` - Remove default shadows when needed
|
||||||
|
|
||||||
|
## Design Tokens Summary
|
||||||
|
|
||||||
|
### Standardized Values
|
||||||
|
- **Border Radius**: `rounded-md` (6px) for all rectangular components
|
||||||
|
- **Border Colors**: `border-gray-300` (standard), `border-gray-200` (subtle)
|
||||||
|
- **Focus States**: `focus:border-blue-600 focus:ring-blue-600`
|
||||||
|
- **Spacing**: `gap-4` (1rem), `gap-6` (1.5rem), `p-4` (1rem), `p-6` (1.5rem)
|
||||||
|
- **Font Weights**: `font-medium` for buttons and emphasis, `font-semibold` for headings, `font-bold` for titles
|
||||||
|
- **Status Border**: All status colors use `-300` shade for borders (e.g., `border-green-300`)
|
||||||
|
|
||||||
|
This styling guide captures the core design patterns used throughout the ScanWave application. Follow these conventions to maintain visual consistency across your new applications.
|
||||||
846
package-lock.json
generated
846
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -27,5 +27,15 @@
|
|||||||
"typescript": "^5.0.0",
|
"typescript": "^5.0.0",
|
||||||
"vite": "^7.0.4",
|
"vite": "^7.0.4",
|
||||||
"vite-plugin-devtools-json": "^0.2.0"
|
"vite-plugin-devtools-json": "^0.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tensorflow/tfjs": "^4.22.0",
|
||||||
|
"@tensorflow/tfjs-backend-webgl": "^4.22.0",
|
||||||
|
"@types/gapi": "^0.0.47",
|
||||||
|
"@types/gapi.client.drive": "^3.0.15",
|
||||||
|
"@types/gapi.client.sheets": "^4.0.20201031",
|
||||||
|
"@types/google.accounts": "^0.0.17",
|
||||||
|
"idb": "^8.0.3",
|
||||||
|
"pdf-lib": "^1.17.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/hooks.server.ts
Normal file
12
src/hooks.server.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { Handle } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
|
const response = await resolve(event);
|
||||||
|
|
||||||
|
// Allow popups to use window.opener for Google Identity Services
|
||||||
|
response.headers.set('Cross-Origin-Opener-Policy', 'same-origin-allow-popups');
|
||||||
|
// Disable Cross-Origin-Embedder-Policy for this application
|
||||||
|
response.headers.set('Cross-Origin-Embedder-Policy', 'unsafe-none');
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
41
src/lib/components/Splash.svelte
Normal file
41
src/lib/components/Splash.svelte
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { currentStep } from '$lib/stores.js';
|
||||||
|
|
||||||
|
function startWizard() {
|
||||||
|
currentStep.set(1); // Move to auth step
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||||
|
<div class="container mx-auto max-w-2xl bg-white p-8 rounded-lg shadow-lg text-center">
|
||||||
|
<div class="mb-8">
|
||||||
|
<!-- ESN Logo placeholder -->
|
||||||
|
<div class="mx-auto mb-6 w-24 h-24 bg-blue-600 rounded-full flex items-center justify-center">
|
||||||
|
<span class="text-white text-2xl font-bold">ESN</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="mb-6 text-3xl font-bold text-gray-800">
|
||||||
|
ESN Card Generator
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p class="text-lg text-gray-700 leading-relaxed mb-6">
|
||||||
|
Transform your Google Sheets into professional ESN membership cards with photos.
|
||||||
|
Privacy-first: all processing happens in your browser.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="text-sm text-gray-500 mb-8">
|
||||||
|
<p class="mb-2">✓ Import data from Google Sheets</p>
|
||||||
|
<p class="mb-2">✓ Automatic face detection and cropping</p>
|
||||||
|
<p class="mb-2">✓ Generate text and photo PDFs</p>
|
||||||
|
<p>✓ No data stored on our servers</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={startWizard}
|
||||||
|
class="bg-blue-600 text-white px-8 py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Start Creating Cards
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
59
src/lib/components/Wizard.svelte
Normal file
59
src/lib/components/Wizard.svelte
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { currentStep } from '$lib/stores.js';
|
||||||
|
import StepAuth from './wizard/StepAuth.svelte';
|
||||||
|
// Additional steps to be added as they are implemented
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
StepAuth
|
||||||
|
];
|
||||||
|
|
||||||
|
const stepTitles = [
|
||||||
|
'Authenticate'
|
||||||
|
];
|
||||||
|
|
||||||
|
function goToPreviousStep() {
|
||||||
|
if ($currentStep > 1) {
|
||||||
|
currentStep.update(n => n - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="min-h-screen bg-gray-50">
|
||||||
|
<div class="container mx-auto max-w-4xl p-4">
|
||||||
|
<!-- Progress indicator -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-800">
|
||||||
|
{stepTitles[$currentStep - 1]}
|
||||||
|
</h1>
|
||||||
|
<span class="text-sm text-gray-500">
|
||||||
|
Step {$currentStep} of {steps.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress bar -->
|
||||||
|
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||||
|
style="width: {($currentStep / steps.length) * 100}%"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step content -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm">
|
||||||
|
<svelte:component this={steps[$currentStep - 1]} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation -->
|
||||||
|
<div class="flex justify-between mt-6">
|
||||||
|
<button
|
||||||
|
on:click={goToPreviousStep}
|
||||||
|
disabled={$currentStep <= 1}
|
||||||
|
class="px-4 py-2 text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
← Previous
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
80
src/lib/components/wizard/StepAuth.svelte
Normal file
80
src/lib/components/wizard/StepAuth.svelte
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { currentStep } from '$lib/stores.js';
|
||||||
|
import { isSignedIn, handleSignIn, handleSignOut, isGoogleApiReady } from '$lib/google';
|
||||||
|
|
||||||
|
function proceed() {
|
||||||
|
currentStep.set(2);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="max-w-md mx-auto text-center">
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="mx-auto mb-4 w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center">
|
||||||
|
<svg class="w-8 h-8 text-blue-600" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M12.017 11.215c-3.573-2.775-9.317-.362-9.317 4.686C2.7 21.833 6.943 24 12.017 24c5.076 0 9.319-2.167 9.319-8.099 0-5.048-5.744-7.461-9.319-4.686z"/>
|
||||||
|
<path d="M20.791 5.016c-1.395-1.395-3.61-1.428-5.024-.033l-1.984 1.984v-.002L12.017 8.73 10.25 6.965l-1.984-1.984c-1.414-1.395-3.629-1.362-5.024.033L1.498 6.758c-1.438 1.438-1.438 3.77 0 5.208l1.744 1.744c1.395 1.395 3.61 1.428 5.024.033l1.984-1.984v.002L12.017 9.996l1.767 1.765 1.984 1.984c1.414 1.395 3.629 1.362 5.024-.033l1.744-1.744c1.438-1.438 1.438-3.77 0-5.208L20.791 5.016z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 mb-2">
|
||||||
|
Connect to Google
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-sm text-gray-700 leading-relaxed mb-6">
|
||||||
|
Sign in with your Google account to access your Google Sheets and Google Drive for photo downloads.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="text-xs text-gray-500 mb-6 space-y-1">
|
||||||
|
<p>Required permissions:</p>
|
||||||
|
<p>• View your Google Spreadsheets</p>
|
||||||
|
<p>• View and manage the files in your Google Drive</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $isSignedIn}
|
||||||
|
<!-- Authenticated state -->
|
||||||
|
<div class="bg-green-50 border border-green-300 rounded-lg p-4 mb-4">
|
||||||
|
<div class="flex items-center justify-center mb-2">
|
||||||
|
<svg class="w-5 h-5 text-green-600 mr-2" 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>
|
||||||
|
<span class="text-sm font-medium text-green-800">Authenticated</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-sm text-green-800 mb-3">
|
||||||
|
You are signed in.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex space-x-3 justify-center">
|
||||||
|
<button
|
||||||
|
on:click={proceed}
|
||||||
|
class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Continue →
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={handleSignOut}
|
||||||
|
class="text-red-600 hover:text-red-700 px-4 py-2 text-sm font-medium"
|
||||||
|
>
|
||||||
|
Sign Out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Unauthenticated state -->
|
||||||
|
<button
|
||||||
|
on:click={handleSignIn}
|
||||||
|
disabled={!$isGoogleApiReady}
|
||||||
|
class="w-full bg-blue-600 text-white px-4 py-3 rounded-lg font-semibold hover:bg-blue-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{#if $isGoogleApiReady}
|
||||||
|
Sign In with Google
|
||||||
|
{:else}
|
||||||
|
Loading Google API...
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
4
src/lib/components/wizard/StepColumnMap.svelte
Normal file
4
src/lib/components/wizard/StepColumnMap.svelte
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<div class="p-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900">Map Columns</h2>
|
||||||
|
<p class="text-sm text-gray-700">Column mapping functionality will be implemented here.</p>
|
||||||
|
</div>
|
||||||
4
src/lib/components/wizard/StepGallery.svelte
Normal file
4
src/lib/components/wizard/StepGallery.svelte
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<div class="p-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900">Review Photos</h2>
|
||||||
|
<p class="text-sm text-gray-700">Photo gallery and review functionality will be implemented here.</p>
|
||||||
|
</div>
|
||||||
4
src/lib/components/wizard/StepGenerate.svelte
Normal file
4
src/lib/components/wizard/StepGenerate.svelte
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<div class="p-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900">Generate PDFs</h2>
|
||||||
|
<p class="text-sm text-gray-700">PDF generation functionality will be implemented here.</p>
|
||||||
|
</div>
|
||||||
4
src/lib/components/wizard/StepRowFilter.svelte
Normal file
4
src/lib/components/wizard/StepRowFilter.svelte
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<div class="p-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900">Filter Rows</h2>
|
||||||
|
<p class="text-sm text-gray-700">Row filtering functionality will be implemented here.</p>
|
||||||
|
</div>
|
||||||
0
src/lib/components/wizard/StepSheetSearch.svelte
Normal file
0
src/lib/components/wizard/StepSheetSearch.svelte
Normal file
112
src/lib/google.ts
Normal file
112
src/lib/google.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID;
|
||||||
|
|
||||||
|
export const isGoogleApiReady = writable(false);
|
||||||
|
export const isSignedIn = writable(false);
|
||||||
|
|
||||||
|
let tokenClient: google.accounts.oauth2.TokenClient;
|
||||||
|
|
||||||
|
const TOKEN_KEY = 'google_oauth_token';
|
||||||
|
export function initGoogleClient(callback: () => void) {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://apis.google.com/js/api.js';
|
||||||
|
script.onload = () => {
|
||||||
|
gapi.load('client', async () => {
|
||||||
|
await gapi.client.init({
|
||||||
|
// NOTE: API KEY IS NOT REQUIRED FOR THIS IMPLEMENTATION
|
||||||
|
// apiKey: 'YOUR_API_KEY',
|
||||||
|
discoveryDocs: [
|
||||||
|
'https://www.googleapis.com/discovery/v1/apis/drive/v3/rest',
|
||||||
|
'https://www.googleapis.com/discovery/v1/apis/sheets/v4/rest',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
isGoogleApiReady.set(true);
|
||||||
|
// Restore token from storage if available
|
||||||
|
const saved = localStorage.getItem(TOKEN_KEY);
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(saved);
|
||||||
|
if (data.access_token && data.expires_at && data.expires_at > Date.now()) {
|
||||||
|
gapi.client.setToken({ access_token: data.access_token });
|
||||||
|
isSignedIn.set(true);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
localStorage.removeItem(TOKEN_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
document.body.appendChild(script);
|
||||||
|
|
||||||
|
const scriptGsi = document.createElement('script');
|
||||||
|
scriptGsi.src = 'https://accounts.google.com/gsi/client';
|
||||||
|
scriptGsi.onload = () => {
|
||||||
|
tokenClient = google.accounts.oauth2.initTokenClient({
|
||||||
|
client_id: GOOGLE_CLIENT_ID,
|
||||||
|
scope: 'https://www.googleapis.com/auth/drive.readonly https://www.googleapis.com/auth/spreadsheets.readonly',
|
||||||
|
callback: (tokenResponse) => {
|
||||||
|
if (tokenResponse?.access_token) {
|
||||||
|
// Set token in gapi client
|
||||||
|
gapi.client.setToken({ access_token: tokenResponse.access_token });
|
||||||
|
isSignedIn.set(true);
|
||||||
|
// Persist token with expiration
|
||||||
|
const expiresInSeconds = tokenResponse.expires_in
|
||||||
|
? Number(tokenResponse.expires_in)
|
||||||
|
: 0;
|
||||||
|
const expiresInMs = expiresInSeconds * 1000;
|
||||||
|
const record = {
|
||||||
|
access_token: tokenResponse.access_token,
|
||||||
|
expires_at: expiresInMs ? Date.now() + expiresInMs : Date.now() + 3600 * 1000
|
||||||
|
};
|
||||||
|
localStorage.setItem(TOKEN_KEY, JSON.stringify(record));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
document.body.appendChild(scriptGsi);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleSignIn() {
|
||||||
|
if (gapi.client.getToken() === null) {
|
||||||
|
tokenClient.requestAccessToken({ prompt: 'consent' });
|
||||||
|
} else {
|
||||||
|
tokenClient.requestAccessToken({ prompt: '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleSignOut() {
|
||||||
|
const token = gapi.client.getToken();
|
||||||
|
if (token !== null) {
|
||||||
|
google.accounts.oauth2.revoke(token.access_token, () => {
|
||||||
|
gapi.client.setToken(null);
|
||||||
|
isSignedIn.set(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchSheets(query: string) {
|
||||||
|
if (!gapi.client.drive) {
|
||||||
|
throw new Error('Google Drive API not loaded');
|
||||||
|
}
|
||||||
|
const response = await gapi.client.drive.files.list({
|
||||||
|
q: `mimeType='application/vnd.google-apps.spreadsheet' and name contains '${query}'`,
|
||||||
|
fields: 'files(id, name, iconLink, webViewLink)',
|
||||||
|
pageSize: 20,
|
||||||
|
});
|
||||||
|
return response.result.files || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSheetData(spreadsheetId: string, range: string) {
|
||||||
|
if (!gapi.client.sheets) {
|
||||||
|
throw new Error('Google Sheets API not loaded');
|
||||||
|
}
|
||||||
|
const response = await gapi.client.sheets.spreadsheets.values.get({
|
||||||
|
spreadsheetId,
|
||||||
|
range,
|
||||||
|
});
|
||||||
|
return response.result.values || [];
|
||||||
|
}
|
||||||
142
src/lib/stores.ts
Normal file
142
src/lib/stores.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { writable, derived } from 'svelte/store';
|
||||||
|
|
||||||
|
// User session and authentication
|
||||||
|
export const session = writable<{
|
||||||
|
token?: string;
|
||||||
|
user?: { name: string; email: string };
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
// Raw sheet data after import
|
||||||
|
export const rawSheetData = writable<string[][]>([]);
|
||||||
|
|
||||||
|
// Column mapping configuration
|
||||||
|
export const columnMapping = writable<{
|
||||||
|
name?: number;
|
||||||
|
surname?: number;
|
||||||
|
nationality?: number;
|
||||||
|
birthday?: number;
|
||||||
|
pictureUrl?: number;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
// Processed row data after mapping and validation
|
||||||
|
export interface RowData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
surname: string;
|
||||||
|
nationality: string;
|
||||||
|
birthday: string;
|
||||||
|
pictureUrl: string;
|
||||||
|
valid: boolean;
|
||||||
|
included: boolean;
|
||||||
|
age?: number;
|
||||||
|
validationErrors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sheetData = writable<RowData[]>([]);
|
||||||
|
|
||||||
|
// Picture storage and metadata
|
||||||
|
export interface PictureBlobInfo {
|
||||||
|
id: string;
|
||||||
|
blob: Blob;
|
||||||
|
url: string;
|
||||||
|
downloaded: boolean;
|
||||||
|
faceDetected: boolean;
|
||||||
|
faceCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pictures = writable<Record<string, PictureBlobInfo>>({});
|
||||||
|
|
||||||
|
// Crop rectangles for each photo
|
||||||
|
export interface Crop {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cropRects = writable<Record<string, Crop>>({});
|
||||||
|
|
||||||
|
// Wizard state management
|
||||||
|
export const currentStep = writable<number>(0);
|
||||||
|
|
||||||
|
export const steps = [
|
||||||
|
'splash',
|
||||||
|
'auth',
|
||||||
|
'search',
|
||||||
|
'mapping',
|
||||||
|
'validation',
|
||||||
|
'gallery',
|
||||||
|
'generate'
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type WizardStep = typeof steps[number];
|
||||||
|
|
||||||
|
export const currentStepName = derived(
|
||||||
|
currentStep,
|
||||||
|
($currentStep) => steps[$currentStep]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Progress tracking
|
||||||
|
export interface ProgressState {
|
||||||
|
stage: string;
|
||||||
|
current: number;
|
||||||
|
total: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const progress = writable<ProgressState>({
|
||||||
|
stage: '',
|
||||||
|
current: 0,
|
||||||
|
total: 0,
|
||||||
|
message: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Google Sheets list for search
|
||||||
|
export interface SheetInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const availableSheets = writable<SheetInfo[]>([]);
|
||||||
|
|
||||||
|
// Selected sheet
|
||||||
|
export const selectedSheet = writable<SheetInfo | null>(null);
|
||||||
|
|
||||||
|
// Validation derived stores
|
||||||
|
export const validRowCount = derived(
|
||||||
|
sheetData,
|
||||||
|
($sheetData) => $sheetData.filter(row => row.valid && row.included).length
|
||||||
|
);
|
||||||
|
|
||||||
|
export const invalidRowCount = derived(
|
||||||
|
sheetData,
|
||||||
|
($sheetData) => $sheetData.filter(row => !row.valid).length
|
||||||
|
);
|
||||||
|
|
||||||
|
export const totalRowCount = derived(
|
||||||
|
sheetData,
|
||||||
|
($sheetData) => $sheetData.length
|
||||||
|
);
|
||||||
|
|
||||||
|
// Face detection status
|
||||||
|
export const faceDetectionProgress = writable<{
|
||||||
|
completed: number;
|
||||||
|
total: number;
|
||||||
|
currentImage: string;
|
||||||
|
}>({
|
||||||
|
completed: 0,
|
||||||
|
total: 0,
|
||||||
|
currentImage: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// PDF generation status
|
||||||
|
export const pdfGenerationStatus = writable<{
|
||||||
|
generating: boolean;
|
||||||
|
stage: 'preparing' | 'text-pdf' | 'photo-pdf' | 'complete';
|
||||||
|
progress: number;
|
||||||
|
}>({
|
||||||
|
generating: false,
|
||||||
|
stage: 'preparing',
|
||||||
|
progress: 0
|
||||||
|
});
|
||||||
@@ -1,7 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { initGoogleClient } from '$lib/google';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
initGoogleClient(() => {
|
||||||
|
// You can add any logic here to run after the client is initialized
|
||||||
|
console.log('Google API client initialized');
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|||||||
@@ -1,2 +1,11 @@
|
|||||||
<h1>Welcome to SvelteKit</h1>
|
<script lang="ts">
|
||||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
import Splash from '$lib/components/Splash.svelte';
|
||||||
|
import Wizard from '$lib/components/Wizard.svelte';
|
||||||
|
import { currentStep } from '$lib/stores';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $currentStep === 0}
|
||||||
|
<Splash />
|
||||||
|
{:else}
|
||||||
|
<Wizard />
|
||||||
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user